diff --git a/i18n/po/template.pot b/i18n/po/template.pot
index 9c0e3bdc6..f61012914 100644
--- a/i18n/po/template.pot
+++ b/i18n/po/template.pot
@@ -3849,4 +3849,28 @@ msgstr ""
#: src/js/services/incomingData.js:129
msgid "This invoice is no longer accepting payments"
+msgstr ""
+
+#: 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/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/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 ec787a5f4..9d306039f 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;
@@ -378,8 +357,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;
@@ -477,16 +454,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..c20c98b93
--- /dev/null
+++ b/src/js/services/bitcoin-uri.service.js
@@ -0,0 +1,356 @@
+'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 infoFromImport(data) {
+ var split = data.split('|');
+ // Copay seems to use extra parameter for coin.
+ if (split.length < 5 || split.length > 6) {
+ return null;
+ }
+
+ }
+
+ /*
+ For parsing:
+ BIP21
+ BIP72
+
+ returns:
+ {
+ amount: '',
+ amountInSatoshis: 0,
+ bareUrl: '',
+ coin: '',
+ copayInvitation: '',
+ 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])) {
+ addressAndParams = trimmed;
+
+ } else {
+ // Something with a colon in the middle that we don't recognise
+ return parsed;
+ }
+
+ // Remove erroneous leading slashes
+ 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 importRe = /^[123]|$/;
+ 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 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 {
+ 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..032255373
--- /dev/null
+++ b/src/js/services/bitcoin-uri.service.spec.js
@@ -0,0 +1,394 @@
+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');
+ });
+
+ // 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/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/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 @@