diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index d7078a4ba..e8e53209a 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) { + var _cardId; var unitToSatoshi; var satToUnit; var unitDecimals; @@ -16,6 +17,7 @@ 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; @@ -283,6 +285,7 @@ angular.module('copayApp.controllers').controller('amountController', function($ } else if ($scope.nextStep) { $state.transitionTo($scope.nextStep, { + id: _cardId, amount: _amount, currency: $scope.showAlternativeAmount ? $scope.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 d3a8e557b..e8257267c 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1052,17 +1052,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', @@ -1070,16 +1075,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/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/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 +
+
+ + + +