diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index d7078a4ba..539d01234 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -1,6 +1,7 @@ 'use strict'; -angular.module('copayApp.controllers').controller('amountController', function($scope, $filter, $timeout, $ionicScrollDelegate, $ionicHistory, gettextCatalog, platformInfo, lodash, configService, rateService, $stateParams, $window, $state, $log, txFormatService, ongoingProcess, bitpayCardService, popupService, bwcError, payproService, profileService, bitcore, amazonService) { +angular.module('copayApp.controllers').controller('amountController', function($scope, $filter, $timeout, $ionicScrollDelegate, $ionicHistory, gettextCatalog, platformInfo, lodash, configService, rateService, $stateParams, $window, $state, $log, txFormatService, ongoingProcess, popupService, bwcError, payproService, profileService, bitcore, amazonService) { + var _cardId; var unitToSatoshi; var satToUnit; var unitDecimals; @@ -16,24 +17,24 @@ angular.module('copayApp.controllers').controller('amountController', function($ $scope.$on("$ionicView.beforeEnter", function(event, data) { // Go to... + _cardId = data.stateParams.id; // Optional (BitPay Card ID) $scope.nextStep = data.stateParams.nextStep; $scope.currency = data.stateParams.currency; $scope.forceCurrency = data.stateParams.forceCurrency; - $scope.cardId = data.stateParams.cardId; $scope.showMenu = $ionicHistory.backView() && $ionicHistory.backView().stateName == 'tabs.send'; var isWallet = data.stateParams.isWallet || 'false'; $scope.isWallet = (isWallet.toString().trim().toLowerCase() == 'true' ? true : false); $scope.toAddress = data.stateParams.toAddress; $scope.toName = data.stateParams.toName; $scope.toEmail = data.stateParams.toEmail; - $scope.showAlternativeAmount = !!$scope.cardId || !!$scope.nextStep; + $scope.showAlternativeAmount = !!$scope.nextStep; $scope.toColor = data.stateParams.toColor; $scope.showSendMax = false; $scope.customAmount = data.stateParams.customAmount; - if (!$scope.cardId && !$scope.nextStep && !data.stateParams.toAddress) { + if (!$scope.nextStep && !data.stateParams.toAddress) { $log.error('Bad params at amount') throw ('bad params'); } @@ -65,8 +66,6 @@ angular.module('copayApp.controllers').controller('amountController', function($ $scope.unitName = config.unitName; if (data.stateParams.currency) { $scope.alternativeIsoCode = data.stateParams.currency; - } else { - $scope.alternativeIsoCode = !!$scope.cardId ? 'USD' : config.alternativeIsoCode; } $scope.specificAmount = $scope.specificAlternativeAmount = ''; $scope.isCordova = platformInfo.isCordova; @@ -216,73 +215,12 @@ angular.module('copayApp.controllers').controller('amountController', function($ return result.replace('x', '*'); }; - $scope.getRates = function() { - bitpayCardService.getRates($scope.alternativeIsoCode, function(err, res) { - if (err) { - $log.warn(err); - return; - } - if ($scope.unitName == 'bits') { - $scope.exchangeRate = '1,000,000 bits ~ ' + res.rate + ' ' + $scope.alternativeIsoCode; - } else { - $scope.exchangeRate = '1 BTC ~ ' + res.rate + ' ' + $scope.alternativeIsoCode; - } - }); - }; - $scope.finish = function() { var _amount = evaluate(format($scope.amount)); - if ($scope.cardId) { - var amountUSD = $scope.showAlternativeAmount ? _amount : $filter('formatFiatAmount')(toFiat(_amount)); - - var dataSrc = { - amount: amountUSD, - currency: 'USD' - }; - - ongoingProcess.set('Preparing transaction...', true); - $timeout(function() { - - bitpayCardService.topUp($scope.cardId, dataSrc, function(err, invoiceId) { - if (err) { - ongoingProcess.set('Preparing transaction...', false); - popupService.showAlert(gettextCatalog.getString('Error'), bwcError.msg(err)); - return; - } - - bitpayCardService.getInvoice(invoiceId, function(err, data) { - if (err) { - ongoingProcess.set('Preparing transaction...', false); - popupService.showAlert(gettextCatalog.getString('Error'), bwcError.msg(err)); - return; - } - var payProUrl = data.paymentUrls.BIP73; - - payproService.getPayProDetails(payProUrl, function(err, payProDetails) { - ongoingProcess.set('Preparing transaction...', false); - if (err) { - popupService.showAlert(gettextCatalog.getString('Error'), bwcError.msg(err)); - return; - } - var stateParams = { - cardId: $scope.cardId, - cardAmountUSD: amountUSD, - toName: $scope.toName, - toAmount: payProDetails.amount, - toAddress: payProDetails.toAddress, - description: payProDetails.memo, - paypro: payProDetails - }; - - $state.transitionTo('tabs.bitpayCard.confirm', stateParams); - }, true); - }); - }); - }); - - } else if ($scope.nextStep) { + if ($scope.nextStep) { $state.transitionTo($scope.nextStep, { + id: _cardId, amount: _amount, currency: $scope.showAlternativeAmount ? $scope.alternativeIsoCode : '' }); diff --git a/src/js/controllers/confirm.js b/src/js/controllers/confirm.js index 64e54a6d7..5d38db39a 100644 --- a/src/js/controllers/confirm.js +++ b/src/js/controllers/confirm.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('confirmController', function($rootScope, $scope, $interval, $filter, $timeout, $ionicScrollDelegate, gettextCatalog, walletService, platformInfo, lodash, configService, rateService, $stateParams, $window, $state, $log, profileService, bitcore, txFormatService, ongoingProcess, $ionicModal, popupService, $ionicHistory, $ionicConfig, payproService, feeService, bwcError, bitpayCardService) { +angular.module('copayApp.controllers').controller('confirmController', function($rootScope, $scope, $interval, $filter, $timeout, $ionicScrollDelegate, gettextCatalog, walletService, platformInfo, lodash, configService, rateService, $stateParams, $window, $state, $log, profileService, bitcore, txFormatService, ongoingProcess, $ionicModal, popupService, $ionicHistory, $ionicConfig, payproService, feeService, bwcError) { var cachedTxp = {}; var toAmount; var isChromeApp = platformInfo.isChromeApp; @@ -16,8 +16,6 @@ angular.module('copayApp.controllers').controller('confirmController', function( $scope.useSendMax = data.stateParams.useSendMax == 'true' ? true : false; var isWallet = data.stateParams.isWallet || 'false'; $scope.isWallet = (isWallet.toString().trim().toLowerCase() == 'true' ? true : false); - $scope.cardId = data.stateParams.cardId; - $scope.cardAmountUSD = data.stateParams.cardAmountUSD; $scope.toAddress = data.stateParams.toAddress; $scope.toName = data.stateParams.toName; $scope.toEmail = data.stateParams.toEmail; @@ -44,9 +42,7 @@ angular.module('copayApp.controllers').controller('confirmController', function( function applyButtonText(multisig) { $scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' '; - if ($scope.cardId) { - $scope.buttonText += gettextCatalog.getString('to complete'); - } else if ($scope.paypro) { + if ($scope.paypro) { $scope.buttonText += gettextCatalog.getString('to pay'); } else if (multisig) { $scope.buttonText += gettextCatalog.getString('to accept'); @@ -144,13 +140,9 @@ angular.module('copayApp.controllers').controller('confirmController', function( $scope.amountStr = txFormatService.formatAmountStr(toAmount); $scope.displayAmount = getDisplayAmount($scope.amountStr); $scope.displayUnit = getDisplayUnit($scope.amountStr); - if ($scope.cardAmountUSD) { - $scope.alternativeAmountStr = $filter('formatFiatAmount')($scope.cardAmountUSD) + ' USD'; - } else { - txFormatService.formatAlternativeStr(toAmount, function(v) { - $scope.alternativeAmountStr = v; - }); - } + txFormatService.formatAlternativeStr(toAmount, function(v) { + $scope.alternativeAmountStr = v; + }); }; function resetValues() { @@ -532,7 +524,6 @@ angular.module('copayApp.controllers').controller('confirmController', function( $scope.onSuccessConfirm = function() { var previousView = $ionicHistory.viewHistory().backView && $ionicHistory.viewHistory().backView.stateName; - var fromBitPayCard = previousView.match(/tabs.bitpayCard/) ? true : false; $ionicHistory.nextViewOptions({ disableAnimate: true @@ -540,22 +531,14 @@ angular.module('copayApp.controllers').controller('confirmController', function( $ionicHistory.removeBackView(); $scope.sendStatus = ''; - if (fromBitPayCard) { - $timeout(function() { - $state.transitionTo('tabs.bitpayCard', { - id: $stateParams.cardId - }); - }, 100); - } else { - $ionicHistory.nextViewOptions({ - disableAnimate: true, - historyRoot: true - }); - $ionicHistory.clearHistory(); - $state.go('tabs.send').then(function() { - $state.transitionTo('tabs.home'); - }); - } + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.send').then(function() { + $state.transitionTo('tabs.home'); + }); }; function publishAndSign(wallet, txp, onSendStatusChange) { @@ -572,22 +555,4 @@ angular.module('copayApp.controllers').controller('confirmController', function( if (err) return setSendError(err); }, onSendStatusChange); }; - - $scope.getRates = function() { - var config = configService.getSync().wallet.settings; - var unitName = config.unitName; - var alternativeIsoCode = config.alternativeIsoCode; - bitpayCardService.getRates(alternativeIsoCode, function(err, res) { - if (err) { - $log.warn(err); - return; - } - if (lodash.isEmpty(res)) return; - if (unitName == 'bits') { - $scope.exchangeRate = '1,000,000 bits ~ ' + res.rate + ' ' + alternativeIsoCode; - } else { - $scope.exchangeRate = '1 BTC ~ ' + res.rate + ' ' + alternativeIsoCode; - } - }); - }; }); diff --git a/src/js/controllers/topup.js b/src/js/controllers/topup.js new file mode 100644 index 000000000..1cf81ab4d --- /dev/null +++ b/src/js/controllers/topup.js @@ -0,0 +1,200 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('topUpController', function($scope, $log, $state, $timeout, $ionicHistory, lodash, popupService, profileService, ongoingProcess, walletService, configService, platformInfo, bitpayService, bitpayCardService, payproService) { + + var amount; + var currency; + var cardId; + + $scope.isCordova = platformInfo.isCordova; + + var showErrorAndBack = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err, function() { + $ionicHistory.goBack(); + }); + }; + + var showError = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err); + }; + + var publishAndSign = function (wallet, txp, onSendStatusChange, cb) { + if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { + var err = 'No signing proposal: No private key'; + $log.info(err); + return cb(err); + } + + walletService.publishAndSign(wallet, txp, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }, onSendStatusChange); + }; + + var statusChangeHandler = function (processName, showName, isOn) { + $log.debug('statusChangeHandler: ', processName, showName, isOn); + if ( processName == 'topup' && !isOn) { + $scope.sendStatus = 'success'; + $timeout(function() { + $scope.$digest(); + }, 100); + } else if (showName) { + $scope.sendStatus = showName; + } + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + $scope.isFiat = data.stateParams.currency ? true : false; + cardId = data.stateParams.id; + + if (!cardId) { + showErrorAndBack('No card selected'); + return; + } + + var parsedAmount = bitpayCardService.parseAmount( + data.stateParams.amount, + data.stateParams.currency); + + amount = parsedAmount.amount; + currency = parsedAmount.currency; + $scope.amountUnitStr = parsedAmount.amountUnitStr; + + $scope.network = bitpayService.getEnvironment().network; + $scope.wallets = profileService.getWallets({ + m: 1, // Only 1-signature wallet + onlyComplete: true, + network: $scope.network + }); + $scope.wallet = $scope.wallets[0]; // Default first wallet + + bitpayCardService.getRates(currency, function(err, data) { + if (err) $log.error(err); + $scope.rate = data.rate; + }); + + bitpayCardService.get({ cardId: cardId, noRefresh: true }, function(err, card) { + if (err) { + showErrorAndBack(err); + return; + } + $scope.cardInfo = card[0]; + }); + + }); + + $scope.topUpConfirm = function() { + + var config = configService.getSync(); + var configWallet = config.wallet; + var walletSettings = configWallet.settings; + + var message = 'Add ' + amount + ' ' + currency + ' to debit card'; + var okText = 'Confirm'; + var cancelText = 'Cancel'; + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + if (!ok) return; + + var dataSrc = { + amount: amount, + currency: currency + }; + ongoingProcess.set('topup', true, statusChangeHandler); + bitpayCardService.topUp(cardId, dataSrc, function(err, invoiceId) { + if (err) { + ongoingProcess.set('topup', false, statusChangeHandler); + showError(err); + return; + } + + bitpayCardService.getInvoice(invoiceId, function(err, invoice) { + if (err) { + ongoingProcess.set('topup', false, statusChangeHandler); + showError(err); + return; + } + + var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null; + + if (!payProUrl) { + ongoingProcess.set('topup', false, statusChangeHandler); + showError('Error fetching invoice'); + return; + } + + payproService.getPayProDetails(payProUrl, function(err, payProDetails) { + if (err) { + ongoingProcess.set('topup', false, statusChangeHandler); + showError(err); + return; + } + + var outputs = []; + var toAddress = payProDetails.toAddress; + var amountSat = payProDetails.amount; + var comment = 'Top up ' + amount + ' ' + currency + ' to Debit Card'; + + outputs.push({ + 'toAddress': toAddress, + 'amount': amountSat, + 'message': comment + }); + + var txp = { + toAddress: toAddress, + amount: amountSat, + outputs: outputs, + message: comment, + payProUrl: payProUrl, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: walletSettings.feeLevel || 'normal' + }; + + walletService.createTx($scope.wallet, txp, function(err, ctxp) { + if (err) { + ongoingProcess.set('topup', false, statusChangeHandler); + showError('Could not create transaction', bwcError.msg(err)); + return; + } + publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) { + ongoingProcess.set('topup', false, statusChangeHandler); + if (err) { + showError('Could not send transaction', err); + return; + } + }); + }); + }, true); // Disable loader + }); + }); + }); + }; + + $scope.showWalletSelector = function() { + $scope.walletSelectorTitle = 'From'; + $scope.showWallets = true; + }; + + $scope.onWalletSelect = function(wallet) { + $scope.wallet = wallet; + }; + + $scope.goBackHome = function() { + $scope.sendStatus = ''; + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.bitpayCard', {id: cardId}); + }); + }; + +}); diff --git a/src/js/routes.js b/src/js/routes.js index 0b160e193..a7c1e7c87 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1055,17 +1055,22 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }) .state('tabs.bitpayCard', { - url: '/bitpay-card/:id', + url: '/bitpay-card', views: { 'tab-home@tabs': { controller: 'bitpayCardController', controllerAs: 'bitpayCard', templateUrl: 'views/bitpayCard.html' } + }, + params: { + id: null, + currency: 'USD', + forceCurrency: true } }) .state('tabs.bitpayCard.amount', { - url: '/amount/:cardId/:toName', + url: '/amount/:nextStep', views: { 'tab-home@tabs': { controller: 'amountController', @@ -1073,16 +1078,13 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } } }) - .state('tabs.bitpayCard.confirm', { - url: '/confirm/:cardId/:cardAmountUSD/:toAddress/:toName/:toAmount/:toEmail/:description', + .state('tabs.bitpayCard.topup', { + url: '/topup/:amount', views: { 'tab-home@tabs': { - controller: 'confirmController', - templateUrl: 'views/confirm.html' + controller: 'topUpController', + templateUrl: 'views/topup.html' } - }, - params: { - paypro: null } }) .state('tabs.preferences.bitpayCard', { diff --git a/src/js/services/bitpayCardService.js b/src/js/services/bitpayCardService.js index 4f87bba50..531d27bde 100644 --- a/src/js/services/bitpayCardService.js +++ b/src/js/services/bitpayCardService.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.services').factory('bitpayCardService', function($log, $rootScope, lodash, storageService, bitauthService, platformInfo, moment, appIdentityService, bitpayService, nextStepsService) { +angular.module('copayApp.services').factory('bitpayCardService', function($log, $rootScope, $filter, lodash, storageService, bitauthService, platformInfo, moment, appIdentityService, bitpayService, nextStepsService, configService, txFormatService) { var root = {}; var _setError = function(msg, e) { @@ -39,6 +39,30 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log, return history; }; + root.parseAmount = function(amount, currency) { + var config = configService.getSync().wallet.settings; + var satToBtc = 1 / 100000000; + var unitToSatoshi = config.unitToSatoshi; + var amountUnitStr; + + // IF 'USD' + if (currency) { + amountUnitStr = $filter('formatFiatAmount')(amount) + ' ' + currency; + } else { + var amountSat = parseInt((amount * unitToSatoshi).toFixed(0)); + amountUnitStr = txFormatService.formatAmountStr(amountSat); + // convert unit to BTC + amount = (amountSat * satToBtc).toFixed(8); + currency = 'BTC'; + } + + return { + amount: amount, + currency: currency, + amountUnitStr: amountUnitStr + }; + }; + root.sync = function(apiContext, cb) { var json = { method: 'getDebitCards' diff --git a/src/js/services/onGoingProcess.js b/src/js/services/onGoingProcess.js index e8bda0082..e04ec2114 100644 --- a/src/js/services/onGoingProcess.js +++ b/src/js/services/onGoingProcess.js @@ -44,7 +44,8 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti 'updatingGiftCard': 'Updating Gift Card...', 'cancelingGiftCard': 'Canceling Gift Card...', 'creatingGiftCard': 'Creating Gift Card...', - 'buyingGiftCard': 'Buying Gift Card...' + 'buyingGiftCard': 'Buying Gift Card...', + 'topup': 'Top up in progress...' }; root.clear = function() { diff --git a/src/sass/views/bitpayCard.scss b/src/sass/views/bitpayCard.scss index 1a620522c..0003e9505 100644 --- a/src/sass/views/bitpayCard.scss +++ b/src/sass/views/bitpayCard.scss @@ -1,5 +1,149 @@ #bitpayCard { + + $item-lateral-padding: 20px; + $item-vertical-padding: 10px; + $item-border-color: #EFEFEF; + $item-label-color: #6C6C6E; + @extend .deflash-blue; background: white; + + .spinner svg { + stroke: #0067c8; + fill: #0067c8; + } + + .add-bottom-for-cta { + bottom: 92px; + } + .head { + padding: 30px $item-lateral-padding 4rem; + border-top: 0; + + .sending-label { + display: flex; + font-size: 18px; + align-items: center; + margin-bottom: 1.5rem; + + img { + margin-right: 1rem; + height: 35px; + width: 35px; + } + + span { + text-transform: capitalize; + } + } + .amount-label{ + line-height: 30px; + .amount-final{ + font-size: 38px; + margin-bottom: .5rem; + + > .unit { + font-family: "Roboto-Light"; + } + } + .alternative { + font-size: 16px; + font-family: "Roboto-Light"; + color: #9B9B9B; + } + } + } + .item { + border-color: $item-border-color; + } + .info { + .badge { + border-radius: 0; + padding: .5rem; + } + .item { + color: #4A4A4A; + padding-top: $item-vertical-padding; + padding-bottom: $item-vertical-padding; + padding-left: $item-lateral-padding; + + &:not(.item-icon-right) { + padding-right: $item-lateral-padding; + } + + .label { + font-size: 14px; + color: $item-label-color; + margin-bottom: 8px; + } + + .capitalized { + text-transform: capitalize; + } + + .wallet .big-icon-svg > .bg { + height: 24px; + width: 24px; + padding: 2px; + box-shadow: none; + vertical-align: middle; + } + + .total-amount { + font-weight: bold; + } + + &.single-line { + display: flex; + align-items: center; + padding-top: 17px; + padding-bottom: 17px; + + .label { + margin: 0; + flex-grow: 1; + } + } + } + .item-divider { + padding-top: 1.2rem; + color: $item-label-color; + font-size: 15px; + } + .wallet { + display: flex; + align-items: center; + padding: .2rem 0; + margin-bottom: 5px; + + ~ .bp-arrow-right { + top: 14px; + } + + > i { + padding: 0; + position: static; + + > img { + height: 24px; + width: 24px; + padding: 2px; + margin-right: .7rem; + box-shadow: none; + } + } + } + } + .disclosure { + color: $light-gray; + font-size: 12px; + text-align: left; + margin: 1rem; + } + + .icon-bitpay-card { + background-image: url("../img/icon-bitpay.svg"); + } + .bar-header { border: 0; background: #1e3186; diff --git a/www/views/amount.html b/www/views/amount.html index 438f7ae53..cd234b3a0 100644 --- a/www/views/amount.html +++ b/www/views/amount.html @@ -17,7 +17,7 @@
Recipient
-
+
@@ -25,10 +25,7 @@ - - -
-
+ {{toName || toAddress}}
@@ -40,7 +37,6 @@
Amount -
{{exchangeRate}}
Daily buy limit: diff --git a/www/views/bitpayCard.html b/www/views/bitpayCard.html index 8ea540d74..8044550e4 100644 --- a/www/views/bitpayCard.html +++ b/www/views/bitpayCard.html @@ -23,7 +23,7 @@ + ui-sref="tabs.bitpayCard.amount({nextStep: 'tabs.bitpayCard.topup'})"> {{'Add Funds'|translate}} diff --git a/www/views/confirm.html b/www/views/confirm.html index c1a22bb03..c4c1a1705 100644 --- a/www/views/confirm.html +++ b/www/views/confirm.html @@ -17,7 +17,6 @@
{{displayAmount || '...'}} {{displayUnit}}
-
{{exchangeRate}}
{{alternativeAmountStr || '...'}}
@@ -31,11 +30,7 @@
To - - - -
-
+
diff --git a/www/views/topup.html b/www/views/topup.html new file mode 100644 index 000000000..d5da30316 --- /dev/null +++ b/www/views/topup.html @@ -0,0 +1,97 @@ + + + + + Add funds + + + + +
+ +
+
+ +
+
+ BitPay Card - Visa ® Prepaid Debit +
+
+
{{amountUnitStr}}
+
+ @ {{rate | currency:'$':2}} per BTC +
+
+
+ +
+ +
+
From
+
+ + + + {{wallet ? wallet.name : '...'}} +
+ +
+ +
+ Deposit into +
+
+ Card + + xxxx-xxxx-xxxx-{{cardInfo.lastFourDigits}} + +
+
+ Account + + {{cardInfo.email}} + +
+
+
+ +
+ + + Add funds + + + Slide to confirm + + + Sent +
+ Funds were added to debit card +
+
+ + + +