diff --git a/src/js/controllers/topup.js b/src/js/controllers/topup.js index c177a11d5..a6261fd35 100644 --- a/src/js/controllers/topup.js +++ b/src/js/controllers/topup.js @@ -2,9 +2,13 @@ angular.module('copayApp.controllers').controller('topUpController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicConfig, lodash, popupService, profileService, ongoingProcess, walletService, configService, platformInfo, bitpayService, bitpayCardService, payproService, bwcError, txFormatService, sendMaxService, gettextCatalog) { - var dataSrc = {}; + $scope.isCordova = platformInfo.isCordova; var cardId; - var sendMax; + var useSendMax; + var amount; + var currency; + var createdTx; + var message; var configWallet = configService.getSync().wallet; var showErrorAndBack = function(title, msg) { @@ -17,12 +21,19 @@ angular.module('copayApp.controllers').controller('topUpController', function($s }); }; - var showError = function(title, msg) { + var showError = function(title, msg, cb) { + cb = cb || function() {}; title = title || gettextCatalog.getString('Error'); $scope.sendStatus = ''; $log.error(msg); msg = msg.errors ? msg.errors[0].message : msg; - popupService.showAlert(title, msg); + popupService.showAlert(title, msg, cb); + }; + + var satToFiat = function(sat, cb) { + txFormatService.toFiat(sat, $scope.currencyIsoCode, function(value) { + return cb(value); + }); }; var publishAndSign = function (wallet, txp, onSendStatusChange, cb) { @@ -40,7 +51,7 @@ angular.module('copayApp.controllers').controller('topUpController', function($s var statusChangeHandler = function (processName, showName, isOn) { $log.debug('statusChangeHandler: ', processName, showName, isOn); - if ( processName == 'sendingTx' && !isOn) { + if (processName == 'topup' && !isOn) { $scope.sendStatus = 'success'; $timeout(function() { $scope.$digest(); @@ -50,31 +61,165 @@ angular.module('copayApp.controllers').controller('topUpController', function($s } }; - var createInvoice = function() { - $scope.expirationTime = null; - ongoingProcess.set('creatingInvoice', true); - bitpayCardService.topUp(cardId, dataSrc, function(err, invoiceId) { + var setTotalAmount = function(amountSat, invoiceFeeSat, networkFeeSat) { + satToFiat(amountSat, function(a) { + $scope.amount = Number(a); + + satToFiat(invoiceFeeSat, function(i) { + $scope.invoiceFee = Number(i); + + satToFiat(networkFeeSat, function(n) { + $scope.networkFee = Number(n); + $scope.totalAmount = $scope.amount + $scope.invoiceFee + $scope.networkFee; + $timeout(function() { + $scope.$digest(); + }); + }); + }); + }); + }; + + var createInvoice = function(data, cb) { + bitpayCardService.topUp(cardId, data, function(err, invoiceId) { if (err) { - ongoingProcess.set('creatingInvoice', false); - showErrorAndBack(gettextCatalog.getString('Could not create the invoice'), err); - return; + return cb({ + title: gettextCatalog.getString('Could not create the invoice'), + message: err + }); } bitpayCardService.getInvoice(invoiceId, function(err, inv) { - ongoingProcess.set('creatingInvoice', false); if (err) { - showError(gettextCatalog.getString('Could not get the invoice'), err); - return; + return cb({ + title: gettextCatalog.getString('Could not get the invoice'), + message: err + }); } - $scope.invoice = inv; - $scope.expirationTime = ($scope.invoice.expirationTime - $scope.invoice.invoiceTime) / 1000; - $timeout(function() { - $scope.$digest(); - }, 1); + return cb(null, inv); }); }); }; + var createTx = function(wallet, invoice, message, cb) { + var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null; + + if (!payProUrl) { + return cb({ + title: gettextCatalog.getString('Error in Payment Protocol'), + message: gettextCatalog.getString('Invalid URL') + }); + } + + var outputs = []; + var toAddress = invoice.bitcoinAddress; + var amountSat = parseInt(invoice.btcDue * 100000000); // BTC to Satoshi + + outputs.push({ + 'toAddress': toAddress, + 'amount': amountSat, + 'message': message + }); + + var txp = { + toAddress: toAddress, + amount: amountSat, + outputs: outputs, + message: message, + payProUrl: payProUrl, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: configWallet.settings.feeLevel || 'normal' + }; + + walletService.createTx(wallet, txp, function(err, ctxp) { + if (err) { + return cb({ + title: gettextCatalog.getString('Could not create transaction'), + message: bwcError.msg(err) + }); + } + return cb(null, ctxp); + }); + }; + + var calculateAmount = function(wallet, cb) { + // Global variables defined beforeEnter + var a = amount; + var c = currency; + + if (useSendMax) { + sendMaxService.getInfo(wallet, function(err, maxValues) { + if (err) { + return cb({ + title: null, + message: err + }) + } + + if (maxValues.amount == 0) { + return cb({message: gettextCatalog.getString('Insufficient funds for fee')}); + } + + var maxAmountBtc = Number((maxValues.amount / 100000000).toFixed(8)); + + createInvoice({amount: maxAmountBtc, currency: 'BTC'}, function(err, inv) { + if (err) return cb(err); + + var invoiceFeeSat = parseInt((inv.buyerPaidBtcMinerFee * 100000000).toFixed()); + var newAmountSat = maxValues.amount - invoiceFeeSat; + + if (newAmountSat <= 0) { + return cb({message: gettextCatalog.getString('Insufficient funds for fee')}); + } + + return cb(null, newAmountSat, 'sat'); + }); + }); + } else { + return cb(null, a, c); + } + }; + + var initializeTopUp = function(wallet, parsedAmount) { + $scope.amountUnitStr = parsedAmount.amountUnitStr; + var dataSrc = { + amount: parsedAmount.amount, + currency: parsedAmount.currency + }; + ongoingProcess.set('loadingTxInfo', true); + createInvoice(dataSrc, function(err, invoice) { + if (err) { + ongoingProcess.set('loadingTxInfo', false); + showErrorAndBack(err.title, err.message); + return; + } + + var invoiceFeeSat = (invoice.buyerPaidBtcMinerFee * 100000000).toFixed(); + + message = gettextCatalog.getString("Top up {{amountStr}} to debit card ({{cardLastNumber}})", { + amountStr: $scope.amountUnitStr, + cardLastNumber: $scope.lastFourDigits + }); + + createTx(wallet, invoice, message, function(err, ctxp) { + ongoingProcess.set('loadingTxInfo', false); + if (err) { + showErrorAndBack(err.title, err.message); + return; + } + + // Save TX in memory + createdTx = ctxp; + + $scope.totalAmountStr = txFormatService.formatAmountStr(ctxp.amount); + + setTotalAmount(parsedAmount.amountSat, invoiceFeeSat, ctxp.fee); + + }); + + }); + + }; + $scope.$on("$ionicView.beforeLeave", function(event, data) { $ionicConfig.views.swipeBackEnabled(true); }); @@ -84,136 +229,68 @@ angular.module('copayApp.controllers').controller('topUpController', function($s }); $scope.$on("$ionicView.beforeEnter", function(event, data) { - $scope.wallet = null; - $scope.isCordova = platformInfo.isCordova; cardId = data.stateParams.id; - sendMax = data.stateParams.useSendMax; - - if (!cardId) { - showErrorAndBack(null, gettextCatalog.getString('No card selected')); - return; - } - - var parsedAmount = txFormatService.parseAmount( - data.stateParams.amount, - data.stateParams.currency); - - dataSrc['amount'] = parsedAmount.amount; - dataSrc['currency'] = parsedAmount.currency; - $scope.amountUnitStr = parsedAmount.amountUnitStr; - - $scope.network = bitpayService.getEnvironment().network; - $scope.wallets = profileService.getWallets({ - onlyComplete: true, - network: $scope.network, - hasFunds: true, - minAmount: parsedAmount.amountSat - }); - - if (lodash.isEmpty($scope.wallets)) { - showErrorAndBack(null, gettextCatalog.getString('Insufficient funds')); - return; - } - $scope.onWalletSelect($scope.wallets[0]); // Default first wallet + useSendMax = data.stateParams.useSendMax; + amount = data.stateParams.amount; + currency = data.stateParams.currency; bitpayCardService.get({ cardId: cardId, noRefresh: true }, function(err, card) { if (err) { showErrorAndBack(null, err); return; } - $scope.cardInfo = card[0]; - bitpayCardService.setCurrencySymbol($scope.cardInfo); - bitpayCardService.getRates($scope.cardInfo.currency, function(err, data) { - if (err) $log.error(err); - $scope.rate = data.rate; + bitpayCardService.setCurrencySymbol(card[0]); + $scope.lastFourDigits = card[0].lastFourDigits; + $scope.currencySymbol = card[0].currencySymbol; + $scope.currencyIsoCode = card[0].currency; + + $scope.wallets = profileService.getWallets({ + onlyComplete: true, + network: bitpayService.getEnvironment().network, + hasFunds: true }); - }); - }); - - $scope.topUpConfirm = function() { - var title; - var message = gettextCatalog.getString("Top up {{amountStr}} to debit card ({{cardLastNumber}})", { - amountStr: $scope.amountUnitStr, - cardLastNumber: $scope.cardInfo.lastFourDigits - }); - var okText = gettextCatalog.getString('Continue'); - var cancelText = gettextCatalog.getString('Cancel'); - popupService.showConfirm(title, message, okText, cancelText, function(ok) { - if (!ok) return; - - ongoingProcess.set('topup', true, statusChangeHandler); - - var payProUrl = ($scope.invoice && $scope.invoice.paymentUrls) ? $scope.invoice.paymentUrls.BIP73 : null; - - if (!payProUrl) { - ongoingProcess.set('topup', false, statusChangeHandler); - showError(gettextCatalog.getString('Error in Payment Protocol'), gettextCatalog.getString('Invalid URL')); + if (lodash.isEmpty($scope.wallets)) { + showErrorAndBack(null, gettextCatalog.getString('No wallets with funds')); return; } - payproService.getPayProDetails(payProUrl, function(err, payProDetails) { + bitpayCardService.getRates($scope.currencyIsoCode, function(err, r) { + if (err) $log.error(err); + $scope.rate = r.rate; + }); + + $scope.onWalletSelect($scope.wallets[0]); // Default first wallet + }); + }); + + $scope.topUpConfirm = function() { + + if (!createdTx) { + showError(null, gettextCatalog.getString('Transaction has not been created')); + return; + } + + var title = gettextCatalog.getString('Confirm'); + var okText = gettextCatalog.getString('OK'); + var cancelText = gettextCatalog.getString('Cancel'); + popupService.showConfirm(title, message, okText, cancelText, function(ok) { + if (!ok) { + $scope.sendStatus = ''; + return; + } + + ongoingProcess.set('topup', true, statusChangeHandler); + publishAndSign($scope.wallet, createdTx, function() {}, function(err, txSent) { if (err) { - ongoingProcess.set('topup', false, statusChangeHandler); - showError(gettextCatalog.getString('Error fetching invoice'), err); + ongoingProcess.set('topup', false); + $scope.sendStatus = ''; + showError(gettextCatalog.getString('Could not send transaction'), err); return; } - - var outputs = []; - var toAddress = payProDetails.toAddress; - var amountSat = payProDetails.amount; - - outputs.push({ - 'toAddress': toAddress, - 'amount': amountSat, - 'message': message - }); - - var txp = { - toAddress: toAddress, - amount: amountSat, - outputs: outputs, - message: message, - payProUrl: payProUrl, - excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, - feeLevel: configWallet.settings.feeLevel || 'normal' - }; - - walletService.createTx($scope.wallet, txp, function(err, ctxp) { - if (err) { - ongoingProcess.set('topup', false, statusChangeHandler); - showError(gettextCatalog.getString('Could not create transaction'), bwcError.msg(err)); - return; - } - - title = gettextCatalog.getString('Sending {{amountStr}} from {{walletName}}', { - amountStr: txFormatService.formatAmountStr(ctxp.amount, true), - walletName: $scope.wallet.name - }); - message = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", { - fee: txFormatService.formatAmountStr(ctxp.fee) - }); - okText = gettextCatalog.getString('Confirm'); - popupService.showConfirm(title, message, okText, cancelText, function(ok) { - ongoingProcess.set('topup', false, statusChangeHandler); - if (!ok) { - $scope.sendStatus = ''; - return; - } - - $scope.expirationTime = null; // Disable countdown - ongoingProcess.set('sendingTx', true, statusChangeHandler); - publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) { - ongoingProcess.set('sendingTx', false, statusChangeHandler); - if (err) { - showError(gettextCatalog.getString('Could not send transaction'), err); - return; - } - }); - }); - }); - }, true); // Disable loader + ongoingProcess.set('topup', false, statusChangeHandler); + }); }); }; @@ -223,38 +300,20 @@ angular.module('copayApp.controllers').controller('topUpController', function($s }; $scope.onWalletSelect = function(wallet) { - if ($scope.wallet && (wallet.id == $scope.wallet.id)) return; $scope.wallet = wallet; - if (sendMax) { - ongoingProcess.set('retrievingInputs', true); - sendMaxService.getInfo($scope.wallet, function(err, values) { - ongoingProcess.set('retrievingInputs', false); - if (err) { - showErrorAndBack(null, err); - return; - } - var unitName = configWallet.settings.unitName; - var amountUnit = txFormatService.satToUnit(values.amount); - var parsedAmount = txFormatService.parseAmount( - amountUnit, - unitName); - - dataSrc['amount'] = parsedAmount.amount; - dataSrc['currency'] = parsedAmount.currency; - $scope.amountUnitStr = parsedAmount.amountUnitStr; - createInvoice(); - $timeout(function() { - $scope.$digest(); - }, 100); - }); - } else { - createInvoice(); - } - }; - - $scope.invoiceExpired = function() { - $scope.sendStatus = ''; - showErrorAndBack(gettextCatalog.getString('Invoice Expired'), gettextCatalog.getString('This invoice has expired. An invoice is only valid for 15 minutes.')); + ongoingProcess.set('retrievingInputs', true); + calculateAmount(wallet, function(err, a, c) { + ongoingProcess.set('retrievingInputs', false); + if (err) { + createdTx = message = $scope.totalAmountStr = $scope.amountUnitStr = $scope.wallet = null; + showError(err.title, err.message, function() { + $scope.showWalletSelector(); + }); + return; + } + var parsedAmount = txFormatService.parseAmount(a, c); + initializeTopUp(wallet, parsedAmount); + }); }; $scope.goBackHome = function() { diff --git a/src/js/services/onGoingProcess.js b/src/js/services/onGoingProcess.js index 454d566f8..0e7db5bfd 100644 --- a/src/js/services/onGoingProcess.js +++ b/src/js/services/onGoingProcess.js @@ -45,8 +45,7 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti 'cancelingGiftCard': 'Canceling Gift Card...', 'creatingGiftCard': 'Creating Gift Card...', 'buyingGiftCard': 'Buying Gift Card...', - 'topup': gettext('Top up in progress...'), - 'creatingInvoice': gettext('Creating invoice...') + 'topup': gettext('Top up in progress...') }; root.clear = function() { diff --git a/src/js/services/sendMax.js b/src/js/services/sendMax.js index 142679f2a..a9c238a1e 100644 --- a/src/js/services/sendMax.js +++ b/src/js/services/sendMax.js @@ -10,7 +10,7 @@ angular.module('copayApp.services').service('sendMaxService', function(feeServic * */ this.getInfo = function(wallet, cb) { - feeService.getCurrentFeeRate(wallet.credentials.network, null, function(err, feePerKb) { + feeService.getCurrentFeeRate(wallet.credentials.network, function(err, feePerKb) { if (err) return cb(err); var config = configService.getSync().wallet; diff --git a/src/js/services/txFormatService.js b/src/js/services/txFormatService.js index fee503a18..0df46fe86 100644 --- a/src/js/services/txFormatService.js +++ b/src/js/services/txFormatService.js @@ -23,6 +23,26 @@ angular.module('copayApp.services').factory('txFormatService', function($filter, return root.formatAmount(satoshis) + ' ' + config.unitName; }; + root.toFiat = function(satoshis, code, cb) { + if (isNaN(satoshis)) return; + var val = function() { + var v1 = rateService.toFiat(satoshis, code); + if (!v1) return null; + + return v1.toFixed(2); + }; + + // Async version + if (cb) { + rateService.whenAvailable(function() { + return cb(val()); + }); + } else { + if (!rateService.isAvailable()) return null; + return val(); + }; + }; + root.formatToUSD = function(satoshis, cb) { if (isNaN(satoshis)) return; var val = function() { @@ -169,9 +189,15 @@ angular.module('copayApp.services').factory('txFormatService', function($filter, var alternativeIsoCode = config.alternativeIsoCode; // If fiat currency - if (currency != 'bits' && currency != 'BTC') { + if (currency != 'bits' && currency != 'BTC' && currency != 'sat') { amountUnitStr = $filter('formatFiatAmount')(amount) + ' ' + currency; amountSat = rateService.fromFiat(amount, currency).toFixed(0); + } else if (currency == 'sat') { + amountSat = amount; + amountUnitStr = root.formatAmountStr(amountSat); + // convert sat to BTC + amount = (amountSat * satToBtc).toFixed(8); + currency = 'BTC'; } else { amountSat = parseInt((amount * unitToSatoshi).toFixed(0)); amountUnitStr = root.formatAmountStr(amountSat); @@ -181,8 +207,8 @@ angular.module('copayApp.services').factory('txFormatService', function($filter, } return { - amount: amount, - currency: currency, + amount: amount, + currency: currency, alternativeIsoCode: alternativeIsoCode, amountSat: amountSat, amountUnitStr: amountUnitStr diff --git a/src/sass/views/bitpayCard.scss b/src/sass/views/bitpayCard.scss index 3d84f32a5..5c2df4425 100644 --- a/src/sass/views/bitpayCard.scss +++ b/src/sass/views/bitpayCard.scss @@ -55,6 +55,10 @@ span { text-transform: capitalize; } + + .big-icon-svg { + padding: 0 12px 0 0; + } } .amount-label{ line-height: 30px; diff --git a/www/views/topup.html b/www/views/topup.html index e5a5550e5..b033c88e5 100644 --- a/www/views/topup.html +++ b/www/views/topup.html @@ -2,25 +2,27 @@ - Add funds + + {{'Add funds' | translate}} + -
+
- BitPay Card - Visa ® Prepaid Debit + BitPay Visa® Card ({{lastFourDigits}})
{{amountUnitStr}}
-
+
@ - {{rate | currency:cardInfo.currencySymbol:2}} per BTC + {{rate | currency:currencySymbol:2}} {{currencyIsoCode}} per BTC ...
@@ -39,45 +41,39 @@
-
- Deposit into +
+
+ Details +
+
+ Funds to be added + + {{amount | currency:currencySymbol:2}} {{currencyIsoCode}} + ... + +
+
+ Invoice Fee + + {{invoiceFee | currency:currencySymbol:2}} {{currencyIsoCode}} + ... + +
+
+ Network Fee + + {{networkFee | currency:currencySymbol:2}} {{currencyIsoCode}} + ... + +
+
+ Total + + {{totalAmount | currency:currencySymbol:2}} {{currencyIsoCode}} + ({{totalAmountStr}}) + +
-
- Card - - xxxx-xxxx-xxxx-{{cardInfo.lastFourDigits}} - -
-
- Account - - {{cardInfo.email}} - -
- -
- Invoice -
-
- Expire in - - {{formatted}} - -
-
- Fee - - {{invoice.buyerPaidBtcMinerFee}} - -
-
- Total - - {{invoice.buyerTotalBtcAmount}} - -
-
@@ -85,16 +81,16 @@ + is-disabled="!wallet || !totalAmountStr"> Add funds + is-disabled="!wallet || !totalAmountStr"> Slide to confirm