diff --git a/Gruntfile.js b/Gruntfile.js
index eb4bb2eb0..8771b85eb 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -9,7 +9,7 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON('package.json'),
exec: {
get_nwjs_for_pkg: {
- command: 'if [ ! -d ./cache/0.19.5-pkg/osx64/nwjs.app ]; then cd ./cache; curl https://dl.nwjs.io/v0.19.5-mas-beta/nwjs-mas-v0.19.5-osx-x64.zip --output nwjs.zip; unzip nwjs.zip; mkdir -p ./0.19.5-pkg/osx64; cp -R ./nwjs-mas-v0.19.5-osx-x64/nwjs.app ./0.19.5-pkg/osx64/; fi'
+ command: 'if [ ! -d ./cache/0.19.4/osx64/nwjs.app ]; then mkdir -p ./cache/0.19.4/osx64; curl https://dl.nwjs.io/v0.19.5-mas-beta/nwjs-mas-v0.19.5-osx-x64.zip --output ./cache/nwjs.zip; unzip ./cache/nwjs.zip -d ./cache; cp -R ./cache/nwjs-mas-v0.19.5-osx-x64/nwjs.app ./cache/0.19.4/osx64/; fi'
},
create_others_dist: {
command: 'sh webkitbuilds/create-others-dist.sh "<%= pkg.name %>" "<%= pkg.fullVersion %>" "<%= pkg.nameCaseNoSpace %>" "<%= pkg.title %>"'
@@ -296,10 +296,10 @@ module.exports = function(grunt) {
},
pkg: {
options: {
- appName: '<%= pkg.nameCaseNoSpace %>',
+ appName: '<%= pkg.title %>',
platforms: ['osx64'],
buildDir: './webkitbuilds/pkg',
- version: '0.19.5',
+ version: '0.19.4',
macIcns: './resources/<%= pkg.name %>/mac/pkg/app.icns',
exeIco: './www/img/app/logo.ico',
macPlist: {
diff --git a/app-template/config-template.xml b/app-template/config-template.xml
index 2f8e3db04..8686b7b36 100644
--- a/app-template/config-template.xml
+++ b/app-template/config-template.xml
@@ -85,6 +85,12 @@
+
+
+
+
+
+
@@ -102,6 +108,7 @@
+
diff --git a/app-template/create-pkg-dist.sh b/app-template/create-pkg-dist.sh
index c0b4d266d..66fe589d0 100644
--- a/app-template/create-pkg-dist.sh
+++ b/app-template/create-pkg-dist.sh
@@ -24,8 +24,9 @@ ln -s ../resources/bitcoin.com/mac/pkg/build.cfg build.cfg
rm build_mas.py
ln -s ../resources/bitcoin.com/mac/pkg/build_mas.py build_mas.py
-echo "Signing ${APP_NAME}"
-export APP_PATH="pkg/${APP_NAME}/osx64/${APP_NAME}"
+echo "Signing ${APP_FULLNAME}"
+export CURRENT_PATH=`pwd`
+export APP_PATH="pkg/${APP_FULLNAME}/osx64/${APP_FULLNAME}"
export TMP_PATH="tmp"
export DIST_PATH="dist"
@@ -36,10 +37,16 @@ if [ ! -d $DIST_PATH ]; then
mkdir $DIST_PATH
fi
-python build_mas.py -C build.cfg -O "${TMP_PATH}/${APP_NAME}.app" -I "${APP_PATH}.app" -P "$DIST_PATH/${APP_PACKAGE}-wallet-${APP_VERSION}-osx.pkg"
+cd "${APP_PATH}.app/Contents/Versions"
+ln -s "55.0.2883.87" "Current"
+
+cd $CURRENT_PATH
+chmod -vR 777 "${APP_PATH}.app/Contents"
+
+python build_mas.py -C build.cfg -O "${TMP_PATH}/${APP_FULLNAME}.app" -I "${APP_PATH}.app" -P "$DIST_PATH/${APP_PACKAGE}-wallet-${APP_VERSION}-osx.pkg"
echo "Signing Done"
echo "Done."
-exit
+exit
\ No newline at end of file
diff --git a/i18n/po/template.pot b/i18n/po/template.pot
index 9c0e3bdc6..56318008c 100644
--- a/i18n/po/template.pot
+++ b/i18n/po/template.pot
@@ -511,7 +511,7 @@ msgid "Cannot Create Wallet"
msgstr ""
#: src/js/services/profileService.js:442
-msgid "Cannot join the same wallet more that once"
+msgid "Cannot join the same wallet more than once"
msgstr ""
#: www/views/includes/bitpayCardsCard.html:2
@@ -2916,10 +2916,15 @@ msgid "Sweep"
msgstr ""
#: www/views/includes/incomingDataMenu.html:89
-#: www/views/paperWallet.html:3
+msgctxt "List item"
msgid "Sweep paper wallet"
msgstr ""
+#: www/views/paperWallet.html:3
+msgctxt "Page title"
+msgid "Sweep Paper Wallet"
+msgstr ""
+
#: src/js/services/onGoingProcess.js:33
msgid "Sweeping Wallet..."
msgstr ""
@@ -3783,6 +3788,10 @@ msgstr ""
msgid "Bitcoin Cash Games"
msgstr ""
+#: www/views/includes/community.html:29
+msgid "Share the Wallet App"
+msgstr ""
+
#: src/js/services/bitcoincomService.js:28
msgid "News"
msgstr ""
@@ -3849,4 +3858,47 @@ msgstr ""
#: src/js/services/incomingData.js:129
msgid "This invoice is no longer accepting payments"
+msgstr ""
+
+#: www/views/paperWallet.html:48
+msgid "No Bitcoin Cash wallet to transfer funds to found."
+msgstr ""
+
+#: www/views/paperWallet.html:54
+msgid "No Bitcoin Cash found."
+msgstr ""
+
+#: www/views/paperWallet.html:60
+msgid "Bitcoin Core found:"
+msgstr ""
+
+#: www/views/paperWallet.html:98
+msgid "No Bitcoin Core wallet to transfer funds to found."
+msgstr ""
+
+#: www/views/paperWallet.html:104
+msgid "No Bitcoin Core found."
+
+#: src/js/controllers/tab-scan.js:120
+msgid "Scan Failed"
+msgstr ""
+
+#: src/js/controllers/tab-scan.js:121
+msgid "Data not recognised."
+msgstr ""
+
+#: src/js/controllers/tab-scan.js:121
+msgid "Unsupported"
+msgstr ""
+
+#: src/js/controllers/tab-scan.js:121
+msgid "Testnet is not supported."
+msgstr ""
+
+#: www/views/includes/incomingDataMenu.html:81
+msgid "URL"
+msgstr ""
+
+#: www/views/includes/incomingDataMenu.html:90
+msgid "Open in web browser"
msgstr ""
\ No newline at end of file
diff --git a/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png b/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png
new file mode 100644
index 000000000..67dddbb02
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png differ
diff --git a/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png b/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png
new file mode 100644
index 000000000..0033e910b
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png differ
diff --git a/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png b/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png
new file mode 100644
index 000000000..ac78920fc
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png differ
diff --git a/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png b/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png
new file mode 100644
index 000000000..5a0e05b71
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png differ
diff --git a/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png b/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png
new file mode 100644
index 000000000..a0aaf7bd7
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png differ
diff --git a/resources/bitcoin.com/ios/icon/icon-1024.png b/resources/bitcoin.com/ios/icon/icon-1024.png
new file mode 100644
index 000000000..2950ff8de
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/icon-1024.png differ
diff --git a/resources/bitcoin.com/ios/icon/icon-20.png b/resources/bitcoin.com/ios/icon/icon-20.png
new file mode 100644
index 000000000..323da6465
Binary files /dev/null and b/resources/bitcoin.com/ios/icon/icon-20.png differ
diff --git a/resources/bitcoin.com/mac/pkg/build_mas.py b/resources/bitcoin.com/mac/pkg/build_mas.py
index d067abacd..90f7ad5f0 100755
--- a/resources/bitcoin.com/mac/pkg/build_mas.py
+++ b/resources/bitcoin.com/mac/pkg/build_mas.py
@@ -178,11 +178,17 @@ def codesign_app(config, args):
plistlib.writePlist(child_entitlements, tmp_child_entitlements)
info('Child entitlements: %s' % tmp_child_entitlements)
- framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True)
- system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework))
- helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True)
- system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp))
+
+ libffmpeg = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libffmpeg.dylib', returnOnFound=True)
+ system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libffmpeg))
+ libnode = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libnode.dylib', returnOnFound=True)
+ system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libnode))
+ helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True)
+ system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp))
+ framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True)
+ system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework))
+
## sign parent app
(_, tmp_parent_entitlements) = tempfile.mkstemp()
if config.has_option('Sign', 'ParentEntitlements'):
diff --git a/resources/bitcoin.com/mac/pkg/entitlements-parent.plist b/resources/bitcoin.com/mac/pkg/entitlements-parent.plist
index 12d6997e3..b39edb569 100644
--- a/resources/bitcoin.com/mac/pkg/entitlements-parent.plist
+++ b/resources/bitcoin.com/mac/pkg/entitlements-parent.plist
@@ -5,7 +5,9 @@
com.apple.security.app-sandbox
com.apple.security.application-groups
- $GROUPID
+
+ 299HJ3G3BP.com.bitcoin.mwallet.mac
+
com.apple.security.files.user-selected.read-only
com.apple.security.network.client
diff --git a/src/js/app.js b/src/js/app.js
index 745ceef50..503da9f52 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -19,7 +19,8 @@ var modules = [
'copayApp.controllers',
'copayApp.directives',
'copayApp.addons',
- 'bitcoincom.directives'
+ 'bitcoincom.directives',
+ 'bitcoincom.services'
];
var copayApp = window.copayApp = angular.module('copayApp', modules);
@@ -30,3 +31,4 @@ angular.module('copayApp.controllers', []);
angular.module('copayApp.directives', []);
angular.module('copayApp.addons', []);
angular.module('bitcoincom.directives', []);
+angular.module('bitcoincom.services', []);
diff --git a/src/js/controllers/addressbookView.js b/src/js/controllers/addressbookView.js
index 89c1cd924..16df9e559 100644
--- a/src/js/controllers/addressbookView.js
+++ b/src/js/controllers/addressbookView.js
@@ -21,28 +21,14 @@ angular.module('copayApp.controllers').controller('addressbookViewController', f
});
$scope.sendTo = function() {
- $ionicHistory.removeBackView();
- sendFlowService.clear();
- $state.go('tabs.send');
- $timeout(function() {
- var to = '';
- if ($scope.addressbookEntry.coin == 'bch') {
- var a = 'bitcoincash:' + $scope.addressbookEntry.address;
- to = bitcoinCashJsService.readAddress(a).legacy;
- } else {
- to = $scope.addressbookEntry.address;
- }
+ var stateParams = {
+ data: $scope.addressbookEntry.address,
+ toName: $scope.addressbookEntry.name,
+ toEmail: $scope.addressbookEntry.email,
+ coin: $scope.addressbookEntry.coin
+ };
- var stateParams = {
- toAddress: to,
- toName: $scope.addressbookEntry.name,
- toEmail: $scope.addressbookEntry.email,
- coin: $scope.addressbookEntry.coin
- };
-
- sendFlowService.pushState(stateParams);
- $state.transitionTo('tabs.send.origin');
- }, 100);
+ sendFlowService.start(stateParams);
};
$scope.remove = function(addressbookEntry) {
diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js
index f796f9559..e861b36ff 100644
--- a/src/js/controllers/amount.js
+++ b/src/js/controllers/amount.js
@@ -68,13 +68,14 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
function onBeforeEnter(event, data) {
if (data.direction == "back") {
- sendFlowService.popState();
+ sendFlowService.state.pop();
}
- console.log('amount onBeforeEnter after back sendflow ', sendFlowService.state);
initCurrencies();
- passthroughParams = sendFlowService.getStateClone();
+ passthroughParams = sendFlowService.state.getClone();
+
+ console.log('amount onBeforeEnter after back sendflow ', passthroughParams);
vm.fromWalletId = passthroughParams.fromWalletId;
vm.toWalletId = passthroughParams.toWalletId;
@@ -214,7 +215,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
}
function goBack() {
- $ionicHistory.goBack();
+ sendFlowService.router.goBack();
}
function paste(value) {
@@ -467,11 +468,10 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
confirmData.thirdParty = vm.thirdParty;
}
- sendFlowService.pushState(confirmData);
if (!confirmData.fromWalletId) {
$state.transitionTo('tabs.paymentRequest.confirm', confirmData);
} else {
- $state.transitionTo('tabs.send.review', confirmData);
+ sendFlowService.goNext(confirmData);
$scope.useSendMax = null;
}
}
diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js
index ed64da836..20b403a4d 100644
--- a/src/js/controllers/amount.spec.js
+++ b/src/js/controllers/amount.spec.js
@@ -7,6 +7,8 @@ describe('amountController', function(){
platformInfo,
profileService,
rateService,
+ sendFlowService,
+ shapeshiftService,
$stateParams;
@@ -39,9 +41,11 @@ describe('amountController', function(){
isIos: true
};
- profileService = jasmine.createSpyObj(['getWallets']);
+ profileService = jasmine.createSpyObj(['getWallet', 'getWallets']);
rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']);
+ sendFlowService = jasmine.createSpyObj(['getStateClone']);
+ shapeshiftService = jasmine.createSpyObj(['shiftIt']);
$stateParams = {};
@@ -61,6 +65,11 @@ describe('amountController', function(){
stateName: 'ignoreme'
};
$ionicHistory.backView.and.returnValue(backView);
+
+ var wallet = {
+
+ };
+ profileService.getWallet.and.returnValue(wallet);
profileService.getWallets.and.returnValue([{}]);
rateService.fromFiat.and.returnValue(12); // satoshis or coins?
@@ -80,22 +89,25 @@ describe('amountController', function(){
popupService: {},
rateService: rateService,
$scope: $scope,
+ sendFlowService: sendFlowService,
+ shapeshiftService: shapeshiftService,
$state: {},
$stateParams: $stateParams,
txFormatService: {},
walletService: {}
});
- var data = {
- stateParams: {
- fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b',
- toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'
- }
+ var sendFlowState = {
+ fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b',
+ toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'
};
- $scope.$emit('$ionicView.beforeEnter', data);
- expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b');
- expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s');
+ sendFlowService.getStateClone.and.returnValue(sendFlowState);
+
+ $scope.$emit('$ionicView.beforeEnter', {});
+
+ //expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b');
+ //expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s');
});
});
\ No newline at end of file
diff --git a/src/js/controllers/onboarding/tour.js b/src/js/controllers/onboarding/tour.js
index 47261006b..f6cf0afe2 100644
--- a/src/js/controllers/onboarding/tour.js
+++ b/src/js/controllers/onboarding/tour.js
@@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tourController',
- function($scope, $state, $log, $timeout, $filter, ongoingProcess, profileService, rateService, popupService, gettextCatalog, startupService, storageService, walletService, $q) {
+ function ($scope, $state, $log, $timeout, $filter, ongoingProcess, configService, profileService, rateService, popupService, gettextCatalog, lodash, startupService, storageService, uxLanguage, walletService, $q) {
$scope.data = {
index: 0
@@ -46,62 +46,90 @@ angular.module('copayApp.controllers').controller('tourController',
creatingWallet = true;
ongoingProcess.set('creatingWallet', true);
$timeout(function() {
- profileService.createDefaultWallet(function(err, walletClients) {
- if (err) {
- $log.warn(err);
+ uxLanguage.init(function(lang) {
+ var rateCode = uxLanguage.getRateCode(lang);
+ console.log("When Available: rateService");
+ rateService.whenAvailable(function() {
+ var alternatives = rateService.listAlternatives(true);
- return $timeout(function() {
- $log.warn('Retrying to create default wallet.....:' + ++retryCount);
- if (retryCount > 3) {
- ongoingProcess.set('creatingWallet', false);
- popupService.showAlert(
- gettextCatalog.getString('Cannot Create Wallet'), err,
- function() {
- retryCount = 0;
- return $scope.createDefaultWallet();
- }, gettextCatalog.getString('Retry'));
- } else {
- return $scope.createDefaultWallet();
- }
- }, 2000);
- };
-
- ongoingProcess.set('creatingWallet', false);
- var bchWallet = walletClients[0];
- var btcWallet = walletClients[1];
- var bchWalletId = bchWallet.credentials.walletId;
- var btcWalletId = btcWallet.credentials.walletId;
-
- function createAddressPromise(wallet) {
- return $q(function(resolve, reject) {
- walletService.getAddress(wallet, true, function(e, addr) {
- if (e) reject(e);
- resolve(addr);
+ var newAltCurrency = lodash.find(alternatives, {
+ 'isoCode': rateCode
});
+
+ configService.whenAvailable(function(config) {
+ var opts = {
+ wallet: {
+ settings: {
+ alternativeName: newAltCurrency.name,
+ alternativeIsoCode: newAltCurrency.isoCode,
+ }
+ }
+ };
+ configService.set(opts, function(err) {
+ if (err) $log.warn(err);
+
+ profileService.createDefaultWallet(function(err, walletClients) {
+ if (err) {
+ $log.warn(err);
+
+ return $timeout(function() {
+ $log.warn('Retrying to create default wallet.....:' + ++retryCount);
+ if (retryCount > 3) {
+ ongoingProcess.set('creatingWallet', false);
+ popupService.showAlert(
+ gettextCatalog.getString('Cannot Create Wallet'), err,
+ function() {
+ retryCount = 0;
+ return $scope.createDefaultWallet();
+ }, gettextCatalog.getString('Retry'));
+ } else {
+ return $scope.createDefaultWallet();
+ }
+ }, 2000);
+ }
+ ;
+
+ ongoingProcess.set('creatingWallet', false);
+ var bchWallet = walletClients[0];
+ var btcWallet = walletClients[1];
+ var bchWalletId = bchWallet.credentials.walletId;
+ var btcWalletId = btcWallet.credentials.walletId;
+
+ function createAddressPromise(wallet) {
+ return $q(function (resolve, reject) {
+ walletService.getAddress(wallet, true, function (e, addr) {
+ if (e) reject(e);
+ resolve(addr);
+ });
+ });
+ }
+
+ function goToCollectEmail() {
+ $state.go('onboarding.collectEmail', {
+ bchWalletId: bchWalletId,
+ btcWalletId: btcWalletId
+ });
+ }
+
+ var bchAddressPromise = createAddressPromise(bchWallet);
+ var btcAddressPromise = createAddressPromise(btcWallet);
+ ongoingProcess.set('generatingNewAddress', true);
+
+ $q.all([bchAddressPromise, btcAddressPromise]).then(function (addresses) {
+ ongoingProcess.set('generatingNewAddress', false);
+ $state.go('tabs.home');
+ }, function (e) {
+ ongoingProcess.set('generatingNewAddress', false);
+ $log.warn(e);
+ popupService.showAlert(gettextCatalog.getString('Error'), e);
+ $state.go('tabs.home');
+ });
+ });
+ });
+ });
+ $log.debug('Setting default currency : ' + newAltCurrency);
});
- }
-
- function goToCollectEmail() {
- $state.go('onboarding.collectEmail', {
- bchWalletId: bchWalletId,
- btcWalletId: btcWalletId
- });
- }
-
- var bchAddressPromise = createAddressPromise(bchWallet);
- var btcAddressPromise = createAddressPromise(btcWallet);
- ongoingProcess.set('generatingNewAddress', true);
-
- $q.all([bchAddressPromise, btcAddressPromise]).then(function(addresses) {
- ongoingProcess.set('generatingNewAddress', false);
- $state.go('tabs.home');
- }, function(e) {
- ongoingProcess.set('generatingNewAddress', false);
- $log.warn(e);
- popupService.showAlert(gettextCatalog.getString('Error'), e);
- $state.go('tabs.home');
- });
- });
- }, 300);
- };
- });
+ })
+ }, 300);
+ };
+ });
diff --git a/src/js/controllers/review.controller.js b/src/js/controllers/review.controller.js
index b377bef58..dbf14937f 100644
--- a/src/js/controllers/review.controller.js
+++ b/src/js/controllers/review.controller.js
@@ -80,7 +80,7 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
function onBeforeEnter(event, data) {
console.log('walletSelector onBeforeEnter sendflow ', sendFlowService.state);
defaults = configService.getDefaults();
- sendFlowData = sendFlowService.getStateClone();
+ sendFlowData = sendFlowService.state.getClone();
originWalletId = sendFlowData.fromWalletId;
satoshis = parseInt(sendFlowData.amount, 10);
toAddress = sendFlowData.toAddress;
@@ -403,7 +403,7 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
}
function goBack() {
- $ionicHistory.goBack();
+ sendFlowService.router.goBack();
}
function handleDestinationAsAddress(address, originCoin) {
@@ -766,8 +766,12 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
((processName === 'signingTx') && vm.originWallet.m > 1) ||
(processName == 'sendingTx' && !vm.originWallet.canSign() && !vm.originWallet.isPrivKeyExternal())
) && !isOn) {
+ // Show the popup
vm.sendStatus = 'success';
+ // Clear the send flow service state
+ sendFlowService.state.clear();
+
if ($state.current.name === "tabs.send.review") { // XX SP: Otherwise all open wallets on other devices play this sound if you have been in a send flow before on that device.
soundService.play('misc/payment_sent.mp3');
}
diff --git a/src/js/controllers/shapeshift.js b/src/js/controllers/shapeshift.js
index 43e0790d1..0dac21a11 100644
--- a/src/js/controllers/shapeshift.js
+++ b/src/js/controllers/shapeshift.js
@@ -6,22 +6,6 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi
$scope.showMyAddress = showMyAddress;
- function generateAddress(wallet, cb) {
- if (!wallet) return;
- walletService.getAddress(wallet, false, function(err, addr) {
- if (err) {
- popupService.showAlert(err);
- }
- return cb(addr);
- });
- }
-
- function showToWallets() {
- $scope.toWallets = $scope.fromWallet.coin === 'btc' ? walletsBch : walletsBtc;
- $scope.onToWalletSelect($scope.toWallets[0]);
- $scope.singleToWallet = $scope.toWallets.length === 1;
- }
-
$scope.$on("$ionicView.beforeEnter", function(event, data) {
walletsBtc = profileService.getWallets({coin: 'btc'});
walletsBch = profileService.getWallets({coin: 'bch'});
@@ -62,18 +46,7 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi
id: 'shapeshift'
}
};
-
- // Starting new send flow, so ensure everything is reset
- sendFlowService.clear();
- $state.go('tabs.home').then(function() {
- $ionicHistory.clearHistory();
- $state.go('tabs.send').then(function() {
- $timeout(function () {
- sendFlowService.pushState(stateParams);
- $state.transitionTo('tabs.send.origin');
- }, 60);
- });
- });
+ sendFlowService.start(stateParams);
}
function showMyAddress() {
diff --git a/src/js/controllers/tab-home.js b/src/js/controllers/tab-home.controller.js
similarity index 99%
rename from src/js/controllers/tab-home.js
rename to src/js/controllers/tab-home.controller.js
index 318fcece2..229848df8 100644
--- a/src/js/controllers/tab-home.js
+++ b/src/js/controllers/tab-home.controller.js
@@ -122,8 +122,7 @@ angular.module('copayApp.controllers').controller('tabHomeController',
};
$scope.startFreshSend = function() {
- sendFlowService.clear();
- $state.go('tabs.send');
+ sendFlowService.start();
}
$scope.openExternalLink = function() {
diff --git a/src/js/controllers/tab-receive.js b/src/js/controllers/tab-receive.js
index 66d1799f8..320afe320 100644
--- a/src/js/controllers/tab-receive.js
+++ b/src/js/controllers/tab-receive.js
@@ -18,10 +18,10 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
$scope.displayBalanceAsFiat = true;
$scope.requestSpecificAmount = function() {
- sendFlowService.pushState({
- toWalletId: $scope.wallet.credentials.walletId
+ sendFlowService.start({
+ toWalletId: $scope.wallet.credentials.walletId,
+ isRequestAmount: true
});
- $state.go('tabs.paymentRequest.amount');
};
$scope.setAddress = function(newAddr, copyAddress) {
diff --git a/src/js/controllers/tab-scan.js b/src/js/controllers/tab-scan.controller.js
similarity index 89%
rename from src/js/controllers/tab-scan.js
rename to src/js/controllers/tab-scan.controller.js
index 4a654d91d..14368ee1c 100644
--- a/src/js/controllers/tab-scan.js
+++ b/src/js/controllers/tab-scan.controller.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.controllers').controller('tabScanController', function($scope, $log, $timeout, scannerService, incomingData, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) {
+angular.module('copayApp.controllers').controller('tabScanController', function(gettextCatalog, popupService, $scope, $log, $timeout, scannerService, incomingDataService, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) {
var scannerStates = {
unauthorized: 'unauthorized',
@@ -111,7 +111,18 @@ angular.module('copayApp.controllers').controller('tabScanController', function(
// Sometimes (testing in Chrome, when reading QR Code) data is an object
// that has a string data.result.
contents = contents.result || contents;
- incomingData.redir(contents);
+ incomingDataService.redir(contents, function onError(err) {
+ if (err) {
+ var title = gettextCatalog.getString('Scan Failed');
+ popupService.showAlert(title, err.message, function onAlertShown() {
+ // Enable another scan since we won't receive incomingDataMenu.menuHidden
+ activate();
+ });
+ } else {
+ scannerService.resumePreview();
+
+ }
+ });
}
$rootScope.$on('incomingDataMenu.menuHidden', function() {
diff --git a/src/js/controllers/tab-send.js b/src/js/controllers/tab-send.controller.js
similarity index 80%
rename from src/js/controllers/tab-send.js
rename to src/js/controllers/tab-send.controller.js
index 9ac6c35cb..03a9562e8 100644
--- a/src/js/controllers/tab-send.js
+++ b/src/js/controllers/tab-send.controller.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.controllers').controller('tabSendController', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, $ionicLoading, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, sendFlowService, bwcError, gettextCatalog, scannerService, configService, bitcoinCashJsService, $ionicPopup, $ionicNavBarDelegate, clipboardService) {
+angular.module('copayApp.controllers').controller('tabSendController', function(bitcoinUriService, $scope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, platformInfo, sendFlowService, gettextCatalog, configService, $ionicPopup, $ionicNavBarDelegate, clipboardService, incomingDataService) {
var clipboardHasAddress = false;
var clipboardHasContent = false;
var originalList;
@@ -29,7 +29,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$scope.$on("$ionicView.enter", function(event, data) {
- var stateParams = sendFlowService.getStateClone();
+ var stateParams = sendFlowService.state.getClone();
$scope.fromWallet = profileService.getWallet(stateParams.fromWalletId);
clipboardService.readFromClipboard(function(text) {
@@ -39,7 +39,9 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$scope.clipboardHasAddress = false;
$scope.clipboardHasContent = false;
- if ((text.indexOf('bitcoincash:') === 0 || text[0] === 'C' || text[0] === 'H' || text[0] === 'p' || text[0] === 'q') && text.replace('bitcoincash:', '').length === 42) { // CashAddr
+ var parsed = bitcoinUriService.parse(text);
+ console.log('parsed', parsed);
+ if (parsed.isValid && parsed.publicAddress && parsed.coin === 'bch' && !parsed.testnet) { // CashAddr
$scope.clipboardHasAddress = true;
} else if ((text[0] === "1" || text[0] === "3" || text.substring(0, 3) === "bc1") && text.length >= 26 && text.length <= 35) { // Legacy Addresses
$scope.clipboardHasAddress = true;
@@ -60,11 +62,6 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
});
$scope.findContact = function(search) {
-
- if (incomingData.redir(search)) {
- return;
- }
-
if (!search || search.length < 1) {
$scope.list = originalList;
$timeout(function() {
@@ -73,12 +70,16 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
return;
}
- var result = lodash.filter(originalList, function(item) {
- var val = item.name;
- return lodash.startsWith(val.toLowerCase(), search.toLowerCase());
+ var params = sendFlowService.state.getClone();
+ params.data = search;
+ sendFlowService.start(params, function onError() {
+ var result = lodash.filter(originalList, function(item) {
+ var val = item.name;
+ return lodash.startsWith(val.toLowerCase(), search.toLowerCase());
+ });
+
+ $scope.list = result;
});
-
- $scope.list = result;
};
var hasWallets = function() {
@@ -184,27 +185,18 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$log.debug('Got toAddress:' + toAddress + ' | ' + item.name);
- var stateParams = sendFlowService.getStateClone();
- stateParams.toAddress = toAddress,
+ var stateParams = sendFlowService.state.getClone();
+ stateParams.toAddress = toAddress;
stateParams.coin = item.coin;
- sendFlowService.pushState(stateParams);
-
- if (!stateParams.fromWalletId) { // If we have no toAddress or fromWallet
- $state.transitionTo('tabs.send.origin');
- } else {
- $state.transitionTo('tabs.send.amount');
- }
-
+ sendFlowService.start(stateParams);
});
};
$scope.startWalletToWalletTransfer = function() {
console.log('startWalletToWalletTransfer()');
- var params = sendFlowService.getStateClone();
- sendFlowService.pushState(params);
- $state.transitionTo('tabs.send.wallet-to-wallet', {
- fromWalletId: sendFlowService.fromWalletId
- });
+ var params = sendFlowService.state.getClone();
+ params.isWalletTransfer = true;
+ sendFlowService.start(params);
}
// This could probably be enhanced refactoring the routes abstract states
@@ -238,7 +230,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
});
if (data.direction == "back") {
- sendFlowService.clear();
+ sendFlowService.state.clear();
}
});
diff --git a/src/js/controllers/tabsController.js b/src/js/controllers/tabsController.js
index b3de6c70f..b78274ecb 100644
--- a/src/js/controllers/tabsController.js
+++ b/src/js/controllers/tabsController.js
@@ -1,11 +1,13 @@
'use strict';
-angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingData, lodash, popupService, gettextCatalog, scannerService, sendFlowService) {
+angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingDataService, lodash, popupService, gettextCatalog, scannerService, sendFlowService) {
$scope.onScan = function(data) {
- if (!incomingData.redir(data)) {
- popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid data'));
- }
+ incomingDataService.redir(data, function onError(err) {
+ if (err) {
+ popupService.showAlert(gettextCatalog.getString('Error'), err.message);
+ }
+ });
};
$scope.setScanFn = function(scanFn) {
@@ -16,8 +18,7 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
};
$scope.startFreshSend = function() {
- sendFlowService.clear();
- $state.go('tabs.send');
+ sendFlowService.start();
};
$scope.importInit = function() {
@@ -28,7 +29,6 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
};
$scope.chooseScanner = function() {
- sendFlowService.clear();
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
@@ -38,10 +38,14 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
scannerService.useOldScanner(function(err, contents) {
if (err) {
- popupService.showAlert(gettextCatalog.getString('Error'), err);
- return;
+ popupService.showAlert(gettextCatalog.getString('Error'), err.message);
+ } else {
+ incomingDataService.redir(contents, function onError(err) {
+ if (err) {
+ popupService.showAlert(gettextCatalog.getString('Error'), err.message);
+ }
+ });
}
- incomingData.redir(contents);
});
};
diff --git a/src/js/controllers/walletDetails.js b/src/js/controllers/wallet-details.controller.js
similarity index 94%
rename from src/js/controllers/walletDetails.js
rename to src/js/controllers/wallet-details.controller.js
index f984a9209..365ce86a3 100644
--- a/src/js/controllers/walletDetails.js
+++ b/src/js/controllers/wallet-details.controller.js
@@ -26,27 +26,6 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
};
var setPendingTxps = function(txps) {
-
- /* Uncomment to test multiple outputs */
-
- // var txp = {
- // message: 'test multi-output',
- // fee: 1000,
- // createdOn: new Date() / 1000,
- // outputs: [],
- // wallet: $scope.wallet
- // };
- //
- // function addOutput(n) {
- // txp.outputs.push({
- // amount: 600,
- // toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK',
- // message: 'output #' + (Number(n) + 1)
- // });
- // };
- // lodash.times(15, addOutput);
- // txps.push(txp);
-
if (!txps) {
$scope.txps = [];
return;
@@ -386,8 +365,6 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
});
$scope.$on("$ionicView.beforeEnter", function(event, data) {
- sendFlowService.clear();
-
configService.whenAvailable(function (config) {
$scope.selectedPriceDisplay = config.wallet.settings.priceDisplay;
@@ -485,16 +462,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
}
$scope.goToSend = function() {
- sendFlowService.startSend({
+ sendFlowService.start({
fromWalletId: $scope.wallet.id
});
- // Go home first so that the Home tab works properly
- $state.go('tabs.home').then(function () {
- $ionicHistory.clearHistory();
- $state.go('tabs.send');
- });
-
};
$scope.goToReceive = function() {
$state.go('tabs.home', {
diff --git a/src/js/controllers/walletSelectorController.js b/src/js/controllers/wallet-selector.controller.js
similarity index 81%
rename from src/js/controllers/walletSelectorController.js
rename to src/js/controllers/wallet-selector.controller.js
index 777871e44..06e6179da 100644
--- a/src/js/controllers/walletSelectorController.js
+++ b/src/js/controllers/wallet-selector.controller.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $rootScope, $state, $log, $ionicHistory, sendFlowService, configService, gettextCatalog, profileService, txFormatService) {
+angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $state, sendFlowService, configService, gettextCatalog, profileService, txFormatService) {
var fromWalletId = '';
var priceDisplayAsFiat = false;
@@ -12,31 +12,22 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
function onBeforeEnter(event, data) {
if (data.direction == "back") {
- sendFlowService.popState();
+ sendFlowService.state.pop();
}
- console.log('walletSelector onBeforeEnter after back sendflow', sendFlowService.state);
- $scope.params = sendFlowService.getStateClone();
+ $scope.params = sendFlowService.state.getClone();
+
+ console.log('walletSelector onBeforeEnter after back sendflow', $scope.params);
var config = configService.getSync().wallet.settings;
priceDisplayAsFiat = config.priceDisplay === 'fiat';
unitDecimals = config.unitDecimals;
unitsFromSatoshis = 1 / config.unitToSatoshi;
- switch($state.current.name) {
- case 'tabs.send.wallet-to-wallet':
- $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
- break;
- case 'tabs.send.destination':
- if ($scope.params.fromWalletId && !$scope.params.thirdParty) {
- $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
- }
- break;
- default:
- if (!$scope.params.thirdParty) {
- $scope.sendFlowTitle = gettextCatalog.getString('Send');
- }
- // nop
+ if ($scope.params.isWalletTransfer) {
+ $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
+ } else if (!$scope.params.thirdParty) {
+ $scope.sendFlowTitle = gettextCatalog.getString('Send');
}
$scope.coin = false; // Wallets to show (for destination screen or contacts)
@@ -99,21 +90,12 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
$scope.requestAmountSecondary = fiatAmount;
$scope.requestCurrencySecondary = fiatCurrrency;
}
+ $scope.$apply();
}
});
}
}
- function getNextStep(params) {
- if (!params.toWalletId && !params.toAddress) { // If we have no toAddress or fromWallet
- return 'tabs.send.destination';
- } else if (!params.amount) { // If we have no amount
- return 'tabs.send.amount';
- } else { // If we do have them
- return 'tabs.send.review';
- }
- }
-
function handleThirdPartyIfShapeshift() {
console.log($scope.thirdParty, $scope.coin);
if ($scope.thirdParty.id === 'shapeshift' && $scope.type === 'destination') { // Shapeshift wants to know the
@@ -191,20 +173,17 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
$scope.useWallet = function(wallet) {
- var params = sendFlowService.getStateClone();
+ var params = sendFlowService.state.getClone();
if ($scope.type === 'origin') { // we're on the origin screen, set wallet to send from
params.fromWalletId = wallet.id;
} else { // we're on the destination screen, set wallet to send to
params.toWalletId = wallet.id;
}
- sendFlowService.pushState(params);
- var nextStep = getNextStep(params);
- console.log('walletSelector nextStep', nextStep);
- $state.transitionTo(nextStep, $scope.params);
+ sendFlowService.goNext(params);
};
$scope.goBack = function() {
- $ionicHistory.goBack();
+ sendFlowService.router.goBack();
}
});
\ No newline at end of file
diff --git a/src/js/directives/incomingDataMenu.js b/src/js/directives/incomingDataMenu.js
index 21478102b..78856e62f 100644
--- a/src/js/directives/incomingDataMenu.js
+++ b/src/js/directives/incomingDataMenu.js
@@ -1,23 +1,28 @@
'use strict';
angular.module('copayApp.directives')
- .directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService) {
+ .directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService, sendFlowService, bitcoinCashJsService) {
return {
restrict: 'E',
templateUrl: 'views/includes/incomingDataMenu.html',
link: function(scope, element, attrs) {
$rootScope.$on('incomingDataMenu.showMenu', function(event, data) {
$timeout(function() {
- scope.data = data.data;
- scope.type = data.type;
- scope.showMenu = true;
- scope.https = false;
+ scope.data = data;
- if (scope.type === 'url') {
- if (scope.data.indexOf('https://') === 0) {
- scope.https = true;
- }
+ if (scope.data.parsed.privateKey) {
+ scope.type = "privateKey";
+ } else if (scope.data.parsed.url) {
+ scope.type = "url";
+ } else if (scope.data.parsed.publicAddress) {
+ scope.type = "bitcoinAddress";
+ var prefix = scope.data.parsed.isTestnet ? 'bchtest:' : 'bitcoincash:';
+ scope.data.toAddress = (prefix + scope.data.parsed.publicAddress.cashAddr) || scope.data.parsed.publicAddress.legacy || scope.data.parsed.publicAddress.bitpay;
+ } else {
+ scope.type = "text";
}
+
+ scope.showMenu = true;
});
});
scope.hide = function() {
@@ -28,18 +33,9 @@ angular.module('copayApp.directives')
externalLinkService.open(url);
};
scope.sendPaymentToAddress = function(bitcoinAddress) {
- var noPrefixInAddress = 0;
- if (bitcoinAddress.toLowerCase().indexOf('bitcoin') < 0) {
- noPrefixInAddress = 1;
- }
scope.showMenu = false;
- $state.go('tabs.send').then(function() {
- $timeout(function() {
- $state.transitionTo('tabs.send.amount', {
- toAddress: bitcoinAddress,
- noPrefix: noPrefixInAddress
- });
- }, 50);
+ sendFlowService.start({
+ data: bitcoinAddress
});
};
scope.addToAddressBook = function(bitcoinAddress) {
diff --git a/src/js/directives/shapeshiftCoinTrader.js b/src/js/directives/shapeshiftCoinTrader.js
index 60cc66bdf..793f380fb 100644
--- a/src/js/directives/shapeshiftCoinTrader.js
+++ b/src/js/directives/shapeshiftCoinTrader.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingData, ongoingProcess) {
+angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingDataService, ongoingProcess) {
return {
restrict: 'E',
transclude: true,
@@ -111,7 +111,8 @@ angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function
orderId: $scope.depositInfo.orderId
};
- if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) {
+ // How to handle this
+ if (incomingDataService.redir(sendAddress, 'shapeshift', shapeshiftData)) {
ongoingProcess.set('connectingShapeshift', false);
return;
}
diff --git a/src/js/services/bitcoin-uri.service.js b/src/js/services/bitcoin-uri.service.js
new file mode 100644
index 000000000..3da84f3da
--- /dev/null
+++ b/src/js/services/bitcoin-uri.service.js
@@ -0,0 +1,416 @@
+'use strict';
+
+// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+// https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki
+
+(function(){
+
+ angular
+ .module('bitcoincom.services')
+ .factory('bitcoinUriService', bitcoinUriService);
+
+ function bitcoinUriService(bitcoinCashJsService, bwcService, $log) {
+ var bch = bitcoinCashJsService.getBitcoinCashJs();
+ var bitcore = bwcService.getBitcore();
+
+ var service = {
+ parse: parse
+ };
+
+ return service;
+
+ function bitpayAddrOnMainnet(address) {
+ var Address = bch.Address;
+ var BitpayFormat = Address.BitpayFormat;
+
+ var mainnet = bch.Networks.mainnet;
+
+ var result = null;
+ if (address[0] == 'C') {
+ try {
+ result = Address.fromString(address, mainnet, 'pubkeyhash', BitpayFormat);
+ } catch (e) {};
+
+ } else if (address[0] == 'H') {
+ try {
+ result = Address.fromString(address, mainnet, 'scripthash', BitpayFormat);
+ } catch (e) {};
+
+ }
+ return result;
+ }
+
+ function cashAddrOnMainnet(address) {
+ var Address = bch.Address;
+ var CashAddrFormat = Address.CashAddrFormat;
+
+ var mainnet = bch.Networks.mainnet;
+
+ var prefixed = 'bitcoincash:' + address;
+ var result = null;
+ if (address[0] == 'q') {
+ try {
+ result = Address.fromString(prefixed, mainnet, 'pubkeyhash', CashAddrFormat);
+ } catch (e) {};
+
+ } else if (address[0] == 'p') {
+ try {
+ result = Address.fromString(prefixed, mainnet, 'scripthash', CashAddrFormat);
+ } catch (e) {};
+
+ }
+ return result;
+ }
+
+ function cashAddrOnTestnet(address) {
+ var Address = bch.Address;
+ var CashAddrFormat = Address.CashAddrFormat;
+
+ var testnet = bch.Networks.testnet;
+
+ var prefixed = 'bchtest:' + address;
+ var result = null;
+ if (address[0] == 'q') {
+ try {
+ result = Address.fromString(prefixed, testnet, 'pubkeyhash', CashAddrFormat);
+ } catch (e) {};
+
+ } else if (address[0] == 'p') {
+ try {
+ result = Address.fromString(prefixed, testnet, 'scripthash', CashAddrFormat);
+ } catch (e) {};
+
+ }
+ return result;
+ }
+
+ function infoFromWalletImportText(data) {
+ var split = data.split('|');
+ // Copay seems to use extra parameter for coin.
+ if (split.length < 5 || split.length > 6) {
+ return null;
+ }
+
+ var type = parseInt(split[0], 10);
+ if (isNaN(type)) {
+ return null;
+ }
+
+ var data = split[1];
+ var network = split[2];
+ if (!(network === 'livenet' || network === 'testnet')) {
+ return null;
+ }
+ var isTestnet = network === 'testnet';
+
+ var derivationPath = split[3];
+ if (!/^m\/\d+'\/\d+'\/\d+'$/.test(derivationPath)) {
+ return null;
+ }
+
+ var hasPassphraseText = split[4];
+ if (!(hasPassphraseText === 'true' || hasPassphraseText === 'false')) {
+ return null;
+ }
+ var hasPassphrase = hasPassphraseText === 'true';
+
+ var coin; // Intentionally undefined as may not be present
+ if (split.length > 5) {
+ var coinText = split[5];
+ if (!(coinText === 'bch' || coinText === 'btc')) {
+ return null;
+ }
+ coin = coinText;
+ }
+
+ return {
+ type: type,
+ data: data,
+ isTestnet: isTestnet,
+ derivationPath: derivationPath,
+ hasPassphrase: hasPassphrase,
+ coin: coin
+ };
+ }
+
+ /*
+ For parsing:
+ BIP21
+ BIP72
+
+ returns:
+ {
+ amount: '',
+ amountInSatoshis: 0,
+ bareUrl: '',
+ coin: '',
+ copayInvitation: '',
+ import: { // testnet info in root, coin info in root if available
+ data: '',
+ derivationPath: '',
+ hasPassphrase: false,
+ type: 1,
+ },
+ isValid: false,
+ label: '',
+ message: '',
+ other: {
+ somethingIDontUnderstand: 'Its value'
+ },
+ privateKey: {
+ encrypted: '',
+ wif: ''
+ }'',
+ publicAddress: {
+ bitpay: '',
+ cashAddr: '',
+ legacy: '',
+ },
+ req: {
+ "req-param0": '',
+ "req-param1": ''
+ },
+ testnet: false,
+ url: '' // For BIP70
+ }
+
+ Only fields that are present in the data are defined in the returned object. Both privateKey and publicAddress only have 1 field defined, if they exist at all.
+ The exception to this is the coin property, which is determined from other data, such as the prefix or address type.
+
+ */
+
+ function parse(data) {
+ var parsed = {
+ isValid: false
+ };
+
+ if (typeof data !== 'string') {
+ return parsed;
+ }
+
+ // Identify prefix
+ var trimmed = data.trim();
+ var colonSplit = /^([\w-]*):?(.*)$/.exec(trimmed);
+ if (!colonSplit) {
+ return parsed;
+ }
+
+ var addressAndParams = '';
+ var preColonLower = colonSplit[1].toLowerCase();
+ if (preColonLower === 'bitcoin') {
+ parsed.coin = 'btc';
+ addressAndParams = colonSplit[2].trim();
+ console.log('Is btc');
+
+ } else if (/^(?:bitcoincash)|(?:bitcoin-cash)$/.test(preColonLower)) {
+ parsed.coin = 'bch';
+ parsed.test = false;
+ addressAndParams = colonSplit[2].trim();
+ console.log('Is bch');
+
+ } else if (/^(?:bchtest)$/.test(preColonLower)) {
+ parsed.coin = 'bch';
+ parsed.isTestnet = true;
+ addressAndParams = colonSplit[2].trim();
+ console.log('Is bch');
+
+ } else if (colonSplit[2] === '') {
+ // No colon and no coin specifier.
+ addressAndParams = colonSplit[1].trim();
+ console.log('No prefix.');
+
+ } else if (/^https?$/.test(colonSplit[1])) { // Plain URL
+ addressAndParams = trimmed;
+
+ } else if (colonSplit[2].indexOf('|') == 0) { // Import
+ addressAndParams = trimmed
+ } else {
+ // Something we don't recognise
+ return parsed;
+ }
+
+ // Remove erroneous leading slashes
+ //var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams);
+ var leadingSlashes = /^\/*(.*)$/.exec(addressAndParams);
+ if (!leadingSlashes) {
+ return parsed;
+ }
+ addressAndParams = leadingSlashes[1];
+
+ var questionMarkSplit = /^([^\?]*)\??([^\?]*)$/.exec(addressAndParams);
+ if (!questionMarkSplit) {
+ return parsed;
+ }
+
+ var address = questionMarkSplit[1];
+ var params = questionMarkSplit[2];
+
+ if (params.length > 0) {
+ var paramsSplit = params.split('&');
+ var others;
+ var req;
+ var paramCount = paramsSplit.length;
+ for(var i = 0; i < paramCount; i++) {
+ var param = paramsSplit[i];
+ var valueSplit = param.split('=');
+ if (valueSplit.length !== 2) {
+ return parsed;
+ }
+
+ var key = valueSplit[0];
+ var value = valueSplit[1];
+ var decodedValue = decodeURIComponent(value);
+ switch(key) {
+ case 'amount':
+ var amount = parseFloat(decodedValue);
+ if (amount) { // Checking for NaN, or no numbers at all etc. & convert to satoshi
+ parsed.amount = decodedValue; // Need to check if a currency is precised
+ parsed.amountInSatoshis = amount * 100000000
+ } else {
+ return parsed;
+ }
+ break;
+
+ case 'label':
+ parsed.label = decodedValue;
+ break;
+
+ case 'message':
+ parsed.message = decodedValue;
+ break;
+
+ case 'r':
+ // Could use a more comprehesive regex to test URL validity, but then how would we know
+ // which part of the validation it failed?
+ if (decodedValue.startsWith('https://')) {
+ parsed.url = decodedValue;
+ } else {
+ return parsed;
+ }
+ break;
+
+ default:
+ if (key.startsWith('req-')) {
+ req = req || {};
+ req[key] = decodedValue;
+ } else {
+ others = others || {};
+ others[key] = decodedValue;
+ }
+ }
+
+ };
+ }
+
+ parsed.others = others;
+ parsed.req = req;
+
+
+ if (address) {
+ var addressLowerCase = address.toLowerCase();
+ var copayInvitationRe = /^[0-9A-HJ-NP-Za-km-z]{70,80}$/;
+ //var legacyRe = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ //var legacyTestnetRe = /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ var privateKeyEncryptedRe = /^6P[1-9A-HJ-NP-Za-km-z]{56}$/;
+ var privateKeyForUncompressedPublicKeyRe = /^5[1-9A-HJ-NP-Za-km-z]{50}$/;
+ var privateKeyForUncompressedPublicKeyTestnetRe = /^9[1-9A-HJ-NP-Za-km-z]{50}$/;
+ var privateKeyForCompressedPublicKeyRe = /^[KL][1-9A-HJ-NP-Za-km-z]{51}$/;
+ var privateKeyForCompressedPublicKeyTestnetRe = /^[c][1-9A-HJ-NP-Za-km-z]{51}$/;
+ var urlRe = /^https?:\/\/.+/;
+
+ var bitpayAddrMainnet = bitpayAddrOnMainnet(address);
+ var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase);
+ var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase);
+ var importInfo = infoFromWalletImportText(address);
+ var privateKey = '';
+
+ if (parsed.isTestnet && cashAddrTestnet) {
+ parsed.address = addressLowerCase;
+ parsed.coin = 'bch';
+ parsed.publicAddress = {
+ cashAddr: addressLowerCase
+ };
+ parsed.isValid = true;
+
+ } else if (cashAddrMainnet) {
+ parsed.coin = 'bch';
+ parsed.publicAddress = {
+ cashAddr: addressLowerCase
+ };
+ parsed.isTestnet = false;
+ parsed.isValid = true;
+
+ } else if (bitcore.Address.isValid(address, 'livenet')) {
+ parsed.publicAddress = {
+ legacy: address
+ };
+ parsed.isTestnet = false;
+ parsed.isValid = true;
+
+ } else if (bitcore.Address.isValid(address, 'testnet')) {
+ parsed.publicAddress = {
+ legacy: address
+ };
+ parsed.isTestnet = true;
+ parsed.isValid = true;
+
+ } else if (bitpayAddrMainnet) {
+ parsed.coin = 'bch';
+ parsed.publicAddress = {
+ bitpay: address
+ };
+ parsed.isTestnet = false;
+ parsed.isValid = true;
+
+ } else if (copayInvitationRe.test(address) ) {
+ parsed.copayInvitation = address;
+ parsed.isValid = true;
+
+ } else if (privateKeyForUncompressedPublicKeyRe.test(address) || privateKeyForCompressedPublicKeyRe.test(address)) {
+ privateKey = address;
+ try {
+ new bitcore.PrivateKey(privateKey, 'livenet');
+ parsed.privateKey = { wif: privateKey };
+ parsed.isTestnet = false;
+ parsed.isValid = true;
+ } catch (e) {}
+
+ } else if (privateKeyForUncompressedPublicKeyTestnetRe.test(address) || privateKeyForCompressedPublicKeyTestnetRe.test(address)) {
+ privateKey = address;
+ try {
+ new bitcore.PrivateKey(privateKey, 'testnet');
+ parsed.privateKey = { wif: privateKey };
+ parsed.isTestnet = true;
+ parsed.isValid = true;
+ } catch (e) {}
+
+ } else if (privateKeyEncryptedRe.test(address)) {
+ parsed.privateKey = { encrypted: address };
+ parsed.isValid = true;
+
+ } else if (urlRe.test(address)) {
+ parsed.bareUrl = trimmed;
+ parsed.isValid = true;
+
+ } else if (importInfo) {
+ parsed.import = {
+ type: importInfo.type,
+ data: importInfo.data,
+ derivationPath: importInfo.derivationPath,
+ hasPassphrase: importInfo.hasPassphrase
+ };
+ parsed.coin = importInfo.coin;
+ parsed.isTestnet = importInfo.isTestnet;
+ parsed.isValid = true;
+ }
+
+ } else {
+ parsed.isValid = !!parsed.url; // BIP72
+ }
+
+ return parsed;
+ }
+
+ }
+
+})();
diff --git a/src/js/services/bitcoin-uri.service.spec.js b/src/js/services/bitcoin-uri.service.spec.js
new file mode 100644
index 000000000..2ddbd0d2e
--- /dev/null
+++ b/src/js/services/bitcoin-uri.service.spec.js
@@ -0,0 +1,429 @@
+describe('bitcoinUriService', function() {
+ var bitcoinUriService;
+
+ beforeEach(function() {
+ module('bitcoinCashJsModule');
+ module('bitcoincom.services');
+ module('bwcModule');
+
+ inject(function($injector){
+ bitcoinUriService = $injector.get('bitcoinUriService');
+ });
+ });
+
+
+ it('Bitcoin BIP72', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.isTestnet).toBeUndefined();
+ expect(parsed.publicAddress).toBeUndefined();
+ expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
+ });
+
+ it('Bitcoin Cash BIP72', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:?r=https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress).toBeUndefined();
+ expect(parsed.isTestnet).toBeUndefined();
+ expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
+ });
+
+ it('Bitcoin Cash prefix with legacy address', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.legacy).toBe('1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin Cash prefix with legacy address on testnet', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.legacy).toBe('mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('Bitcoin Cash uri with extended params', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc?unknown=something&mystery=Melton%20probang&req-one=ichi&req-beta=Ni%20san');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.others.mystery).toBe('Melton probang');
+ expect(parsed.others.unknown).toBe('something');
+ expect(parsed.publicAddress.cashAddr).toBe('qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc');
+ expect(parsed.req['req-beta']).toBe('Ni san');
+ expect(parsed.req['req-one']).toBe('ichi');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin Cash uri with invalid amount', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:qq0knhwj4d5zy3kdph24w6etq58vwzua6sm7lhcmuk?amount=three');
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+
+ it('Bitcoin testnet address', function() {
+
+ var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBeUndefined();
+ expect(parsed.publicAddress.legacy).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('Bitcoin uri', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin:15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.publicAddress.legacy).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin uri with encoded label', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin:1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT?label=Mr.%20Smith');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.label).toBe('Mr. Smith');
+ expect(parsed.publicAddress.legacy).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin uri with params', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin:12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu?amount=20.3&label=Luke-Jr&message=Donation%20for%20project%20xyz');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.amount).toBe('20.3');
+ expect(parsed.amountInSatoshis).toBe(2030000000);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.label).toBe('Luke-Jr');
+ expect(parsed.publicAddress.legacy).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu');
+ expect(parsed.message).toBe('Donation for project xyz');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin uri with slash', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin:/1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.publicAddress.legacy).toBe('1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin uri with slashes', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin://18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.publicAddress.legacy).toBe('18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitcoin uri with space', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin: 19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('btc');
+ expect(parsed.publicAddress.legacy).toBe('19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('Bitpay without prefix', function() {
+
+ var parsed = bitcoinUriService.parse('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.bitpay).toBe('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('legacy address', function() {
+
+ var parsed = bitcoinUriService.parse('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBeUndefined();
+ expect(parsed.publicAddress.legacy).toBe('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr testnet with prefix', function() {
+
+ var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('cashAddr uppercase', function() {
+
+ var parsed = bitcoinUriService.parse('BITCOINCASH:QZZG9NMC5VX8GAP6XFATX3TWNSDN2YRMCSSULSMY44');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qzzg9nmc5vx8gap6xfatx3twnsdn2yrmcssulsmy44');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr with dash', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoin-cash:qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr with prefix', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr with slash', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash:/qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr with slashes', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash://qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('cashAddr with space', function() {
+
+ var parsed = bitcoinUriService.parse('bitcoincash: qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+
+ it('cashAddr with space on testnet', function() {
+
+ var parsed = bitcoinUriService.parse('bchtest: qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('cashAddr without prefix', function() {
+
+ var parsed = bitcoinUriService.parse('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.coin).toBe('bch');
+ expect(parsed.publicAddress.cashAddr).toBe('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('copay invitation', function() {
+
+ var parsed = bitcoinUriService.parse('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.copayInvitation).toBe('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
+ });
+
+
+ it ('import BCH wallet no password', function() {
+ var parsed = bitcoinUriService.parse("1|suggest route obvious broccoli good position hidden tone history around final lobster|livenet|m/44'/0'/0'|false");
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.import.type).toBe(1);
+ expect(parsed.import.data).toBe('suggest route obvious broccoli good position hidden tone history around final lobster');
+ expect(parsed.isTestnet).toBe(false);
+ expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
+ expect(parsed.import.hasPassphrase).toBe(false);
+ });
+
+ it ('import BCH wallet with passphrase', function() {
+ var parsed = bitcoinUriService.parse("1|fringe hazard all hobby trap myth fire stand sock empty soon east|livenet|m/44'/0'/0'|true");
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.import.type).toBe(1);
+ expect(parsed.import.data).toBe('fringe hazard all hobby trap myth fire stand sock empty soon east');
+ expect(parsed.isTestnet).toBe(false);
+ expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
+ expect(parsed.import.hasPassphrase).toBe(true);
+ });
+
+ it ('import BTC wallet testnet', function() {
+ // From copay
+ var parsed = bitcoinUriService.parse("1|cat wealth column firm wet sauce tornado era feature monster click eyebrow|testnet|m/44'/1'/0'|false|btc");
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.import.type).toBe(1);
+ expect(parsed.import.data).toBe('cat wealth column firm wet sauce tornado era feature monster click eyebrow');
+ expect(parsed.isTestnet).toBe(true);
+ expect(parsed.import.derivationPath).toBe("m/44'/1'/0'");
+ expect(parsed.import.hasPassphrase).toBe(false);
+ });
+
+ // Invalid addresses from https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md
+ it('invalid cashAddr style 1', function() {
+ var parsed = bitcoinUriService.parse('prefix:x64nx6hz');
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('invalid cashAddr style 2', function() {
+ var parsed = bitcoinUriService.parse('p:gpf8m4h7');
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('invalid cashAddr style 3', function() {
+ var parsed = bitcoinUriService.parse('bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn');
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('invalid cashAddr style 4', function() {
+ var parsed = bitcoinUriService.parse('bchtest:testnetaddress4d6njnut');
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('invalid cashAddr style 5', function() {
+ var parsed = bitcoinUriService.parse('bchreg:555555555555555555555555555555555555555555555udxmlmrz');
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('non-string', function() {
+
+ var parsed = bitcoinUriService.parse([1, 2, 3, 4]);
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('private key encrypted with BIP38', function() {
+
+ var parsed = bitcoinUriService.parse('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.privateKey.encrypted).toBe('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
+ });
+
+ it('private key for compressed pubkey mainnet', function() {
+
+ var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.privateKey.wif).toBe('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('private key for compressed pubkey mainnet with wrong checksum', function() {
+
+ var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTu');
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('private key for compressed pubkey testnet', function() {
+
+ var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.privateKey.wif).toBe('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('private key for compressed pubkey testnet with wrong checksum', function() {
+
+ var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMM');
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('private key for uncompressed pubkey mainnet', function() {
+
+ var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.privateKey.wif).toBe('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
+ expect(parsed.isTestnet).toBe(false);
+ });
+
+ it('private key for uncompressed pubkey mainnet with wrong checksum', function() {
+
+ var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTTwx');
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('private key for uncompressed pubkey testnet', function() {
+
+ var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.privateKey.wif).toBe('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
+ expect(parsed.isTestnet).toBe(true);
+ });
+
+ it('private key for uncompressed pubkey testnet with wrong checksum', function() {
+
+ var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcC');
+
+ expect(parsed.isValid).toBe(false);
+ });
+
+ it('URL only, http', function() {
+
+ var parsed = bitcoinUriService.parse('http://paperwallet.bitcoin.com');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.bareUrl).toBe('http://paperwallet.bitcoin.com');
+ });
+
+ it('URL only, https with query', function() {
+
+ var parsed = bitcoinUriService.parse('https://purse.io/?one=two&three=four');
+
+ expect(parsed.isValid).toBe(true);
+ expect(parsed.bareUrl).toBe('https://purse.io/?one=two&three=four');
+ });
+
+});
\ No newline at end of file
diff --git a/src/js/services/incoming-data.service.js b/src/js/services/incoming-data.service.js
new file mode 100644
index 000000000..eece6d17c
--- /dev/null
+++ b/src/js/services/incoming-data.service.js
@@ -0,0 +1,79 @@
+'use strict';
+
+/**
+ * incomingDataService is an intermediate to redirect either to the sendFlow
+ * or to import/join a wallet.
+ */
+angular.module('copayApp.services').factory('incomingDataService', function(bitcoinUriService, $log, $state, $rootScope, scannerService, sendFlowService, gettextCatalog) {
+
+ var root = {};
+
+ root.showMenu = function(data) {
+ $rootScope.$broadcast('incomingDataMenu.showMenu', data);
+ };
+
+ root.redir = function(data, cbError) {
+ var parsed = bitcoinUriService.parse(data);
+
+ console.log(parsed);
+ $log.debug(parsed);
+
+
+ if (parsed.isValid) {
+ if (parsed.isTestnet) {
+ if (cbError) {
+ var errorMessage = gettextCatalog.getString('Testnet is not supported.');
+ cbError(new Error(errorMessage));
+ }
+ } else {
+ scannerService.pausePreview();
+
+ /**
+ * Strategy for the action
+ */
+ if (parsed.copayInvitation) {
+ $state.go('tabs.home').then(function() {
+ $state.transitionTo('tabs.add.join', {
+ url: data
+ });
+ });
+ } else if (parsed.import) {
+ $state.go('tabs.home').then(function() {
+ $state.transitionTo('tabs.add.import', {
+ code: data
+ });
+ });
+ } else if (
+ !parsed.isValid
+ || parsed.privateKey
+ || (sendFlowService.state.isEmpty() && !parsed.url && !parsed.amount)
+ ) {
+ root.showMenu({
+ original: data,
+ parsed: parsed
+ });
+ } else {
+ var state = sendFlowService.state.getClone();
+ state.data = data;
+
+ sendFlowService.start(state, function onError(err) {
+ /**
+ * OnError, open the menu (link not validated)
+ */
+ root.showMenu({
+ original: data,
+ parsed: parsed
+ });
+ });
+ }
+ }
+ } else {
+ if (cbError) {
+ var errorMessage = gettextCatalog.getString('Data not recognised.');
+ cbError(new Error(errorMessage));
+ }
+ }
+ };
+
+ return root;
+});
diff --git a/src/js/services/incomingData.js b/src/js/services/incomingData.js
deleted file mode 100644
index 0bf708d8a..000000000
--- a/src/js/services/incomingData.js
+++ /dev/null
@@ -1,475 +0,0 @@
-'use strict';
-
-angular.module('copayApp.services').factory('incomingData', function($log, $state, $timeout, $ionicHistory, bitcore, bitcoreCash, $rootScope, payproService, scannerService, sendFlowService, appConfigService, popupService, gettextCatalog, bitcoinCashJsService) {
-
- var root = {};
-
- root.showMenu = function(data) {
- $rootScope.$broadcast('incomingDataMenu.showMenu', data);
- };
-
- root.redir = function(data, serviceId, serviceData) {
- var originalAddress = null;
- var noPrefixInAddress = 0;
-
- if (data.toLowerCase().indexOf('bitcoin') < 0) {
- noPrefixInAddress = 1;
- }
-
- if (typeof(data) == 'string' && !(/^bitcoin(cash)?:\?r=[\w+]/).exec(data) && (data.toLowerCase().indexOf('bitcoincash:') >= 0 || data[0] == 'q' || data[0] == 'p' || data[0] == 'C' || data[0] == 'H')) {
- try {
- noPrefixInAddress = 0;
-
- if (data[0] == 'p' || data[0] == 'q') {
- data = 'bitcoincash:' + data;
- }
- var paramString = '';
- if (data.indexOf('?') >= 0) {
- paramString = data.substring(data.indexOf('?'));
- data = data.substring(0, data.indexOf('?'));
- }
-
- if (data.indexOf('BITCOINCASH:') >= 0) {
- data = data.toLowerCase();
- }
- originalAddress = data.replace('bitcoincash:', '');
- var legacyAddress = bitcoinCashJsService.readAddress(data).legacy;
- data = 'bitcoincash:' + legacyAddress + paramString;
- } catch (ex) {}
- }
-
- $log.debug('Processing incoming data: ' + data);
-
- function sanitizeUri(data) {
- // Fixes when a region uses comma to separate decimals
- var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i;
- var match = regex.exec(data);
- if (!match || match.length === 0) {
- return data;
- }
- var value = match[0].replace(',', '.');
- var newUri = data.replace(regex, value);
-
- // mobile devices, uris like copay://glidera
- newUri.replace('://', ':');
-
- return newUri;
- }
-
- function getParameterByName(name, url) {
- if (!url) return;
- name = name.replace(/[\[\]]/g, "\\$&");
- var regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)"),
- results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeURIComponent(results[2].replace(/\+/g, " "));
- }
-
- function checkPrivateKey(privateKey) {
- try {
- new bitcore.PrivateKey(privateKey, 'livenet');
- } catch (err) {
- return false;
- }
- return true;
- }
-
- function goSend(addr, amount, message, coin, serviceId, serviceData) {
- $state.go('tabs.send', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.send' ? false : true
- });
- // Timeout is required to enable the "Back" button
- $timeout(function() {
- var params = sendFlowService.getStateClone();
-
- if (amount) {
- params.amount = amount;
- }
-
- if (addr) {
- params.toAddress = addr;
- params.displayAddress = originalAddress ? originalAddress : addr;
- }
-
- if (coin) {
- params.coin = coin;
- }
-
- if (noPrefixInAddress) {
- params.noPrefixInAddress = noPrefixInAddress;
- }
-
- if (serviceId) {
- params.thirdParty = [];
- params.thirdParty.id = serviceId;
- params.thirdParty.data = serviceData;
- sendFlowService.pushState(params);
- $state.transitionTo('tabs.send.amount');
- } else {
- sendFlowService.pushState(params);
- $state.transitionTo('tabs.send.origin');
- }
- }, 100);
- }
- // data extensions for Payment Protocol with non-backwards-compatible request
- if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
- var coin = data.indexOf('bitcoincash') >= 0 ? 'bch' : 'btc';
- data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
- if (coin == 'bch') {
- payproService.getPayProDetailsViaHttp(data, function onGetPayProDetailsViaHttp(err, details) {
- if (err) {
- var message = err.toString();
- if (typeof err.data === 'string') {
- // i.e. 'This invoice is no longer accepting payments'
- message = gettextCatalog.getString(err.data);
- }
- popupService.showAlert(gettextCatalog.getString('Error'), message)
- } else {
- handlePayPro(details, coin);
- }
- });
- } else {
- payproService.getPayProDetails(data, coin, function onGetPayProDetails(err, details) {
- if (err) {
- popupService.showAlert(gettextCatalog.getString('Error'), err);
- } else {
- handlePayPro(details, coin);
- }
- });
- }
- return true;
- }
-
- data = sanitizeUri(data);
-
- // Bitcoin URL
- if (bitcore.URI.isValid(data)) {
- var coin = 'btc';
- var parsed = new bitcore.URI(data);
-
- var addr = parsed.address ? parsed.address.toString() : '';
- var message = parsed.message;
-
- var amount = parsed.amount ? parsed.amount : '';
-
- if (parsed.r) {
- payproService.getPayProDetails(parsed.r, coin, function(err, details) {
- if (err) {
- if (addr && amount) goSend(addr, amount, message, coin, serviceId, serviceData);
- else popupService.showAlert(gettextCatalog.getString('Error'), err);
- } else handlePayPro(details, coin);
- });
- } else {
- goSend(addr, amount, message, coin, serviceId, serviceData);
- }
- return true;
- // Cash URI
- } else if (bitcoreCash.URI.isValid(data)) {
- var coin = 'bch';
- var parsed = new bitcoreCash.URI(data);
-
- var addr = parsed.address ? parsed.address.toString() : '';
- var message = parsed.message;
-
- var amount = parsed.amount ? parsed.amount : '';
-
- // paypro not yet supported on cash
- if (parsed.r) {
- payproService.getPayProDetails(parsed.r, coin, function(err, details) {
- if (err) {
- if (addr && amount)
- goSend(addr, amount, message, coin, serviceId, serviceData);
- else
- popupService.showAlert(gettextCatalog.getString('Error'), err);
- }
- handlePayPro(details, coin);
- });
- } else {
- goSend(addr, amount, message, coin, serviceId, serviceData);
- }
- return true;
-
- // Cash URI with bitcoin (btc) address version number?
- } else if (bitcore.URI.isValid(data.replace(/^bitcoincash:/,'bitcoin:'))) {
- $log.debug('Handling bitcoincash URI with legacy address');
- var coin = 'bch';
- var parsed = new bitcore.URI(data.replace(/^bitcoincash:/,'bitcoin:'));
-
- var oldAddr = parsed.address ? parsed.address.toString() : '';
- if (!oldAddr) return false;
-
- var addr = '';
-
- var a = bitcore.Address(oldAddr).toObject();
- addr = bitcoreCash.Address.fromObject(a).toString();
-
- // Translate address
- $log.debug('address transalated to:' + addr);
- popupService.showConfirm(
- gettextCatalog.getString('Bitcoin cash Payment'),
- gettextCatalog.getString('Payment address was translated to new Bitcoin Cash address format: ' + addr),
- gettextCatalog.getString('OK'),
- gettextCatalog.getString('Cancel'),
- function(ret) {
- if (!ret) return false;
-
- var message = parsed.message;
- var amount = parsed.amount ? parsed.amount : '';
-
- // paypro not yet supported on cash
- if (parsed.r) {
- payproService.getPayProDetails(parsed.r, coin, function(err, details) {
- if (err) {
- if (addr && amount)
- goSend(addr, amount, message, coin, serviceId, serviceData);
- else
- popupService.showAlert(gettextCatalog.getString('Error'), err);
- }
- handlePayPro(details, coin);
- });
- } else {
- goSend(addr, amount, message, coin, serviceId, serviceData);
- }
- }
- );
- return true;
- // Plain URL
- } else if (/^https?:\/\//.test(data)) {
- payproService.getPayProDetails(data, coin, function(err, details) {
- if (err) {
- if ($state.includes('tabs.scan')) {
- root.showMenu({
- data: data,
- type: 'url'
- });
- }
- return;
- }
- handlePayPro(details);
- return true;
- });
- // Plain Address
- } else if (bitcore.Address.isValid(data, 'livenet') || bitcore.Address.isValid(data, 'testnet')) {
- if ($state.includes('tabs.scan')) {
- root.showMenu({
- data: data,
- type: 'bitcoinAddress'
- });
- } else {
- goToAmountPage(data);
- }
- } else if (bitcoreCash.Address.isValid(data, 'livenet')) {
- if ($state.includes('tabs.scan')) {
- root.showMenu({
- data: data,
- type: 'bitcoinAddress',
- coin: 'bch',
- });
- } else {
- goToAmountPage(data, 'bch');
- }
- } else if (data && data.indexOf(appConfigService.name + '://glidera') === 0) {
- var code = getParameterByName('code', data);
- $ionicHistory.nextViewOptions({
- disableAnimate: true
- });
- $state.go('tabs.home', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.home' ? false : true
- }).then(function() {
- $ionicHistory.nextViewOptions({
- disableAnimate: true
- });
- $state.transitionTo('tabs.buyandsell.glidera', {
- code: code
- });
- });
- return true;
-
- } else if (data && data.indexOf(appConfigService.name + '://coinbase') === 0) {
- var code = getParameterByName('code', data);
- $ionicHistory.nextViewOptions({
- disableAnimate: true
- });
- $state.go('tabs.home', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.home' ? false : true
- }).then(function() {
- $ionicHistory.nextViewOptions({
- disableAnimate: true
- });
- $state.transitionTo('tabs.buyandsell.coinbase', {
- code: code
- });
- });
- return true;
-
- // BitPayCard Authentication
- } else if (data && data.indexOf(appConfigService.name + '://') === 0) {
-
- // Disable BitPay Card
- if (!appConfigService._enabledExtensions.debitcard) return false;
-
- var secret = getParameterByName('secret', data);
- var email = getParameterByName('email', data);
- var otp = getParameterByName('otp', data);
- var reason = getParameterByName('r', data);
-
- $state.go('tabs.home', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.home' ? false : true
- }).then(function() {
- switch (reason) {
- default:
- case '0':
- /* For BitPay card binding */
- $state.transitionTo('tabs.bitpayCardIntro', {
- secret: secret,
- email: email,
- otp: otp
- });
- break;
- }
- });
- return true;
-
- // Join
- } else if (data && data.match(/^copay:[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
- $state.go('tabs.home', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.home' ? false : true
- }).then(function() {
- $state.transitionTo('tabs.add.join', {
- url: data
- });
- });
- return true;
-
- // Old join
- } else if (data && data.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
- $state.go('tabs.home', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.home' ? false : true
- }).then(function() {
- $state.transitionTo('tabs.add.join', {
- url: data
- });
- });
- return true;
- } else if (data && (data.substring(0, 2) == '6P' || checkPrivateKey(data))) {
- root.showMenu({
- data: data,
- type: 'privateKey'
- });
- } else if (data && ((data.substring(0, 2) == '1|') || (data.substring(0, 2) == '2|') || (data.substring(0, 2) == '3|'))) {
- $state.go('tabs.home').then(function() {
- $state.transitionTo('tabs.add.import', {
- code: data
- });
- });
- return true;
-
- } else {
- if ($state.includes('tabs.scan')) {
- root.showMenu({
- data: data,
- type: 'text'
- });
- }
- }
- return false;
- };
-
- function goToAmountPage(toAddress, coin) {
- $state.go('tabs.send', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.send' ? false : true
- });
- $timeout(function() {
- var stateParams = {
- toAddress: toAddress,
- displayAddress: toAddress,
- coin: coin,
- noPrefix: 1
- };
- sendFlowService.pushState(stateParams);
- $state.transitionTo('tabs.send.origin');
- }, 100);
- }
-
- function handlePayPro(payProData, coin) {
-
- console.log(payProData);
-
- var toAddr = payProData.toAddress;
- var amount = payProData.amount;
- var paymentUrl = payProData.url;
- var expires = payProData.expires;
- var time = payProData.time;
-
- if (coin === 'bch') {
- var displayAddr = payProData.outputs[0].address;
- toAddr = bitcoinCashJsService.readAddress('bitcoincash:' + displayAddr).legacy;
- amount = payProData.outputs[0].amount;
- paymentUrl = payProData.paymentUrl;
- expires = Math.floor(new Date(expires).getTime() / 1000)
- time = Math.ceil(new Date(time).getTime() / 1000)
- }
-
- var name = payProData.domain;
-
- if (payProData.memo.indexOf('eGifter') > -1) {
- name = 'eGifter'
- } else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
- name = 'BitPay';
- }
-
- var thirdPartyData = {
- id: 'bip70',
- amount: amount,
- caTrusted: true,
- name: name,
- domain: payProData.domain,
- expires: expires,
- memo: payProData.memo,
- network: 'livenet',
- requiredFeeRate: payProData.requiredFeeRate,
- selfSigned: 0,
- time: time,
- displayAddress: displayAddr,
- toAddress: toAddr,
- url: paymentUrl,
- verified: true
- };
-
- var stateParams = {
- amount: thirdPartyData.amount,
- toAddress: thirdPartyData.toAddress,
- coin: coin,
- thirdParty: thirdPartyData
- };
-
- // fee
- if (thirdPartyData.requiredFeeRate) {
- stateParams.requiredFeeRate = thirdPartyData.requiredFeeRate * 1024;
- }
-
- // This does not make sense, thirdPartyData gets added by stateParams below
- //sendFlowService.pushState(thirdPartyData);
-
- scannerService.pausePreview();
- $state.go('tabs.send', {}, {
- 'reload': true,
- 'notify': $state.current.name == 'tabs.send' ? false : true
- }).then(function() {
- $timeout(function() {
- sendFlowService.pushState(stateParams); // Need to do more here
- $state.transitionTo('tabs.send.origin');
- });
- });
- }
-
- return root;
-});
diff --git a/src/js/services/openURL.js b/src/js/services/openURL.js
index 0f4d6c666..2cf8d95a5 100644
--- a/src/js/services/openURL.js
+++ b/src/js/services/openURL.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingData, appConfigService) {
+angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingDataService, appConfigService) {
var root = {};
var handleOpenURL = function(args) {
@@ -23,9 +23,12 @@ angular.module('copayApp.services').factory('openURLService', function($rootScop
document.addEventListener('handleopenurl', handleOpenURL, false);
- if (!incomingData.redir(url)) {
- $log.warn('Unknown URL! : ' + url);
- }
+ incomingDataService.redir(url, function onError(err) {
+ if (err) {
+ $log.warn('Unknown URL! : ' + url);
+ popupService.showAlert(gettextCatalog.getString('Error'), err.message);
+ }
+ });
};
var handleResume = function() {
diff --git a/src/js/services/send-flow-router.service.js b/src/js/services/send-flow-router.service.js
new file mode 100644
index 000000000..32aa8420b
--- /dev/null
+++ b/src/js/services/send-flow-router.service.js
@@ -0,0 +1,85 @@
+'use strict';
+
+(function(){
+
+angular
+ .module('bitcoincom.services')
+ .factory('sendFlowRouterService', sendFlowRouterService);
+
+ function sendFlowRouterService(
+ sendFlowStateService
+ , $state, $ionicHistory, $timeout
+ ) {
+
+ var service = {
+ // Functions
+ start: start,
+ goNext: goNext,
+ goBack: goBack,
+ };
+
+ return service;
+
+ /**
+ * Start new send flow
+ */
+ function start() {
+ var state = sendFlowStateService.state;
+
+ if (state.isRequestAmount) {
+ $state.go('tabs.paymentRequest.amount');
+ } else {
+ if ($state.current.name != 'tabs.send') {
+ $state.go('tabs.home').then(function () {
+ $ionicHistory.clearHistory();
+ $state.go('tabs.send').then(function () {
+ $timeout(function () {
+ goNext();
+ }, 60);
+ });
+ });
+ } else {
+ goNext();
+ }
+ }
+ }
+
+ /**
+ * Go to the next page
+ * Routing strategy : https://bitcoindotcom.atlassian.net/wiki/x/BQDWKQ
+ */
+ function goNext() {
+ var state = sendFlowStateService.state;
+
+ var needsDestination = !state.toWalletId && !state.toAddress;
+ var needsOrigin = !state.fromWalletId;
+ var needsAmount = !state.amount && !state.sendMax;
+
+ if (needsDestination) {
+ if (!state.isWalletTransfer && !state.thirdParty) {
+ $state.go('tabs.send');
+ return;
+ } else if (!needsOrigin) {
+ $state.go('tabs.send.destination');
+ return;
+ }
+ }
+
+ if (needsOrigin) {
+ $state.go('tabs.send.origin');
+ } else if (needsAmount) {
+ $state.go('tabs.send.amount');
+ } else {
+ $state.go('tabs.send.review');
+ }
+ }
+
+ /**
+ * Go to the previous page
+ */
+ function goBack() {
+ $ionicHistory.goBack();
+ }
+ };
+
+})();
\ No newline at end of file
diff --git a/src/js/services/sendFlowService.js b/src/js/services/send-flow-state.service.js
similarity index 50%
rename from src/js/services/sendFlowService.js
rename to src/js/services/send-flow-state.service.js
index 62989b3c5..bec2c8a3c 100644
--- a/src/js/services/sendFlowService.js
+++ b/src/js/services/send-flow-state.service.js
@@ -3,14 +3,13 @@
(function(){
angular
- .module('copayApp.services')
- .factory('sendFlowService', sendFlowService);
+ .module('bitcoincom.services')
+ .factory('sendFlowStateService', sendFlowStateService);
- function sendFlowService($log) {
+ function sendFlowStateService($log) {
var service = {
- // A separate state variable so we can ensure it is cleared of everything,
- // even other properties added that this service does not know about. (such as "coin")
+ // Variables
state: {
amount: '',
displayAddress: null,
@@ -18,29 +17,55 @@ angular
sendMax: false,
thirdParty: null,
toAddress: '',
- toWalletId: ''
+ toWalletId: '',
+ coin: '',
+ isRequestAmount: false,
+ isWalletTransfer: false
},
previousStates: [],
// Functions
+ init: init,
clear: clear,
- getStateClone: getStateClone,
+ getClone: getClone,
map: map,
- popState: popState,
- pushState: pushState,
- startSend: startSend
+ pop: pop,
+ push: push,
+ isEmpty: isEmpty
};
return service;
+ /**
+ * Init state & stack
+ * @param {Object} params
+ */
+ function init(params) {
+ $log.debug("send-flow-state init()");
+
+ clear();
+
+ if (params) {
+ push(params);
+ }
+ }
+
+ /**
+ * Clear a state & stack
+ */
function clear() {
- console.log("sendFlow clear()");
+ $log.debug("send-flow-state clear()");
+
clearCurrent();
service.previousStates = [];
}
+ /**
+ * Clear current state only
+ */
function clearCurrent() {
- console.log("sendFlow clearCurrent()");
+ $log.debug("send-flow-state clearCurrent()");
+
service.state = {
amount: '',
displayAddress: null,
@@ -48,14 +73,17 @@ angular
sendMax: false,
thirdParty: null,
toAddress: '',
- toWalletId: ''
+ toWalletId: '',
+ coin: '',
+ isRequestAmount: false,
+ isWalletTransfer: false
}
}
/**
- * Handy for debugging
+ * Get a clone of the current state
*/
- function getStateClone() {
+ function getClone() {
var currentState = {};
Object.keys(service.state).forEach(function forCurrentParam(key) {
if (typeof service.state[key] !== 'function' && key !== 'previousStates') {
@@ -66,22 +94,21 @@ angular
}
/**
- * Clears all previous state
+ * Fill in the current state from the params
+ * @param {Object} params
*/
- function startSend(params) {
- console.log('startSend()');
- clear();
- map(params);
- }
-
function map(params) {
Object.keys(params).forEach(function forNewParam(key) {
service.state[key] = params[key];
});
};
- function popState() {
- console.log('sendFlow pop');
+ /**
+ * Pop state
+ */
+ function pop() {
+ $log.debug('send-flow-state pop');
+
if (service.previousStates.length) {
var params = service.previousStates.pop();
clearCurrent();
@@ -91,13 +118,25 @@ angular
}
};
- function pushState(params) {
- console.log('sendFlow push');
- var currentParams = getStateClone();
+ /**
+ * Push state
+ * @param {Object} params
+ */
+ function push(params) {
+ $log.debug('send-flow-state push');
+
+ var currentParams = getClone();
service.previousStates.push(currentParams);
clearCurrent();
map(params);
};
+
+ /**
+ * Is empty stack
+ */
+ function isEmpty() {
+ return service.previousStates.length == 0;
+ };
};
})();
\ No newline at end of file
diff --git a/src/js/services/send-flow.service.js b/src/js/services/send-flow.service.js
new file mode 100644
index 000000000..1b02c0d34
--- /dev/null
+++ b/src/js/services/send-flow.service.js
@@ -0,0 +1,149 @@
+'use strict';
+
+(function(){
+
+angular
+ .module('bitcoincom.services')
+ .factory('sendFlowService', sendFlowService);
+
+ function sendFlowService(
+ sendFlowStateService, sendFlowRouterService
+ , bitcoinUriService, payproService, bitcoinCashJsService
+ , popupService, gettextCatalog
+ , $state, $log
+ ) {
+
+ var service = {
+ // Variables
+ state: sendFlowStateService,
+ router: sendFlowRouterService,
+
+ // Functions
+ start: start,
+ goNext: goNext,
+ goBack: goBack
+ };
+
+ return service;
+
+ /**
+ * Start a new send flow
+ * @param {Object} params
+ * @param {Function} onError
+ */
+ function start(params, onError) {
+ $log.debug('send-flow start()');
+
+ if (params && params.data) {
+ var res = bitcoinUriService.parse(params.data);
+
+ if (res.isValid) {
+
+ // If BIP70 (url)
+ if (res.url) {
+ var url = res.url;
+ var coin = res.coin || '';
+ payproService.getPayProDetails(url, coin, function onGetPayProDetails(err, payProData) {
+ if (err) {
+ popupService.showAlert(gettextCatalog.getString('Error'), err);
+ } else {
+ var name = payProData.domain;
+
+ // Detect some merchant that we know
+ if (payProData.memo.indexOf('eGifter') > -1) {
+ name = 'eGifter'
+ } else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
+ name = 'BitPay';
+ }
+
+ // Init thirdParty
+ var thirdPartyData = {
+ id: 'bip70',
+ caTrusted: true,
+ name: name,
+ domain: payProData.domain,
+ expires: payProData.expires,
+ memo: payProData.memo,
+ network: 'livenet',
+ requiredFeeRate: payProData.requiredFeeRate,
+ selfSigned: 0,
+ time: payProData.time,
+ url: payProData.url,
+ verified: true
+ };
+
+ // Fill in params
+ params.amount = payProData.amount,
+ params.toAddress = payProData.toAddress,
+ params.coin = coin,
+ params.thirdParty = thirdPartyData
+ }
+
+ // Resolve
+ _next();
+ });
+ } else {
+ if (res.coin) {
+ params.coin = res.coin;
+ }
+
+ if (res.amountInSatoshis) {
+ params.amount = res.amountInSatoshis;
+ }
+
+ if (res.publicAddress) {
+ var prefix = res.isTestnet ? 'bchtest:' : 'bitcoincash:';
+ params.displayAddress = res.publicAddress.cashAddr || res.publicAddress.legacy || res.publicAddress.bitpay;
+ var formatAddress = res.publicAddress.cashAddr ? prefix + params.displayAddress : params.displayAddress;
+ params.toAddress = bitcoinCashJsService.readAddress(formatAddress).legacy;
+ }
+
+ _next();
+ }
+ } else {
+ if (onError) {
+ onError();
+ }
+ }
+ } else {
+ _next();
+ }
+
+
+ // Next used for sync the async task
+ function _next() {
+ sendFlowStateService.init(params);
+
+ // Routing strategy to -> send-flow-router.service
+ sendFlowRouterService.start();
+ }
+ }
+
+ /**
+ * Go to the next step
+ * @param {Object} state
+ */
+ function goNext(state) {
+ $log.debug('send-flow goNext()');
+
+ // Save the current route before leaving
+ state.route = $state.current.name;
+
+ // Save the state and redirect the user
+ sendFlowStateService.push(state);
+ sendFlowRouterService.goNext();
+ }
+
+ /**
+ * Go to the previous step
+ */
+ function goBack() {
+ $log.debug('send-flow goBack()');
+
+ // Remove the state on top and redirect the user
+ sendFlowStateService.pop();
+ sendFlowRouterService.goBack();
+ }
+ };
+
+})();
\ No newline at end of file
diff --git a/src/js/services/shapeshiftService.js b/src/js/services/shapeshiftService.js
index 1ce9672ce..b1d2f6e7d 100644
--- a/src/js/services/shapeshiftService.js
+++ b/src/js/services/shapeshiftService.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingData, platformInfo, servicesService) {
+angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingDataService, platformInfo, servicesService) {
var root = {};
root.ShiftState = 'Shift';
root.coinIn = '';
@@ -111,7 +111,7 @@ angular.module('copayApp.services').factory('shapeshiftService', function ($http
toAddress: txData.deposit
};
//
- // if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) {
+ // if (incomingDataService.redir(sendAddress, 'shapeshift', shapeshiftData)) {
ongoingProcess.set('connectingShapeshift', false);
// return;
// }
diff --git a/src/sass/buttons.scss b/src/sass/buttons.scss
index df0ce8945..886d730c7 100644
--- a/src/sass/buttons.scss
+++ b/src/sass/buttons.scss
@@ -1,5 +1,5 @@
%button-standard {
- width: 85%;
+ width: 90%;
max-width: 300px;
margin-left: auto;
margin-right: auto;
diff --git a/src/sass/components/fee-summary.scss b/src/sass/components/fee-summary.scss
index e09af4be3..5c9488b60 100644
--- a/src/sass/components/fee-summary.scss
+++ b/src/sass/components/fee-summary.scss
@@ -1,11 +1,11 @@
.fee-summary {
- position: relative;
+ background-color: #F2F2F2;
+ box-sizing: border-box;
display: flex;
flex-direction: column;
- width: 100%;
padding: 5px 12px 15px;
- box-sizing: border-box;
- background-color: #F2F2F2;
+ position: relative;
+ width: 100%;
&:before {
content: '';
diff --git a/src/sass/mixins/layout.scss b/src/sass/mixins/layout.scss
index 269a50320..e7a1af9da 100644
--- a/src/sass/mixins/layout.scss
+++ b/src/sass/mixins/layout.scss
@@ -59,6 +59,8 @@
.button {
font-weight: bold;
font-size: 19px;
+ line-height: 26px;
+ padding: 8px 6px;
}
}
.button-first-contact img {
diff --git a/src/sass/views/review.scss b/src/sass/views/review.scss
index c530a1cef..474e1ea3f 100644
--- a/src/sass/views/review.scss
+++ b/src/sass/views/review.scss
@@ -6,8 +6,10 @@
}
.fee-summary {
- position: absolute;
bottom: 92px;
+ bottom: calc(92px + constant(safe-area-inset-bottom)); /* iOS 11.0 */
+ bottom: calc(92px + env(safe-area-inset-bottom)); /* iOS 11.2 */
+ position: absolute;
}
.shapeshift-banner, .bitpay-banner, .egifter-banner {
diff --git a/src/sass/views/tab-home.scss b/src/sass/views/tab-home.scss
index 55080ab7b..f9ace4655 100644
--- a/src/sass/views/tab-home.scss
+++ b/src/sass/views/tab-home.scss
@@ -83,14 +83,14 @@
.button {
border: 2px solid;
border-radius: 47px;
- padding: 0 15px 0 15px;
+ padding: 8px 2px 8px 2px;
text-align: center;
width: 100%;
max-width: 300px;
font-size: 19px;
font-weight: bolder;
min-height: auto;
- line-height: 36px;
+ line-height: 19px;
}
}
.wallet-coin-logo {
diff --git a/src/sass/views/tab-send.scss b/src/sass/views/tab-send.scss
index bf77984fd..564e90d66 100644
--- a/src/sass/views/tab-send.scss
+++ b/src/sass/views/tab-send.scss
@@ -105,7 +105,7 @@
width: auto;
margin: 2px 0 4px;
}
- height: 60px;
+ min-height: 65px;
line-height: 16px;
margin-right: 0px;
width: 95%;
diff --git a/src/sass/views/walletDetails.scss b/src/sass/views/walletDetails.scss
index 30b5da430..4e94f6a29 100644
--- a/src/sass/views/walletDetails.scss
+++ b/src/sass/views/walletDetails.scss
@@ -209,14 +209,14 @@
.button {
border: 2px solid;
border-radius: 47px;
- padding: 0 15px 0 15px;
+ padding: 6px 2px 6px 2px;
text-align: center;
width: 100%;
max-width: 300px;
font-size: 19px;
font-weight: bolder;
min-height: auto;
- line-height: 36px;
+ line-height: 19px;
}
}
}
diff --git a/test/karma.conf.js b/test/karma.conf.js
index b4f64af73..22efcd1c8 100644
--- a/test/karma.conf.js
+++ b/test/karma.conf.js
@@ -17,7 +17,7 @@ module.exports = function(config) {
files: [
'node_modules/angular/angular.js',
- 'bitanalytics/bitanalytics-0.1.0.js',
+ 'bitanalytics/bitanalytics.js',
// From Gruntfile.js
'bower_components/qrcode-generator/js/qrcode.js',
diff --git a/www/views/includes/community.html b/www/views/includes/community.html
index 86841a77c..ad7dcb169 100644
--- a/www/views/includes/community.html
+++ b/www/views/includes/community.html
@@ -26,7 +26,7 @@
- Share the Wallet App
+ Share the Wallet App
diff --git a/www/views/includes/incomingDataMenu.html b/www/views/includes/incomingDataMenu.html
index ca4b78dfc..e60d7e956 100644
--- a/www/views/includes/incomingDataMenu.html
+++ b/www/views/includes/incomingDataMenu.html
@@ -9,21 +9,21 @@
-
-
-
-
+
+
+
+
+
+
+
- No Bitcoin Cash wallet to transfer funds to found.
+ No Bitcoin Cash wallet to transfer funds to found.
-
No Bitcoin Cash found
+ No Bitcoin Cash found.
-
Bitcoin found:
+
Bitcoin Core found:
{{btcBalanceText}}
@@ -95,13 +95,13 @@
- No Bitcoin wallet to transfer funds to found.
+ No Bitcoin Core wallet to transfer funds to found.
-
No Bitcoin found
+ No Bitcoin Core found.