Merge pull request #6274 from matiu/ref/confirm

Confirm controller refactor
This commit is contained in:
Gustavo Maximiliano Cortez 2017-06-22 15:24:27 -03:00 committed by GitHub
commit f6245652d9
19 changed files with 531 additions and 579 deletions

View file

@ -1,14 +1,37 @@
'use strict'; '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) { 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 feeLevel;
var feePerKb;
var toAmount;
var isChromeApp = platformInfo.isChromeApp;
var countDown = null; var countDown = null;
var cachedSendMax = {}; var CONFIRM_LIMIT_USD = 20;
$scope.isCordova = platformInfo.isCordova;
var tx = {};
// Config Related values
var config = configService.getSync();
var walletConfig = config.wallet;
var unitToSatoshi = walletConfig.settings.unitToSatoshi;
var unitDecimals = walletConfig.settings.unitDecimals;
var satToUnit = 1 / unitToSatoshi;
var configFeeLevel = walletConfig.settings.feeLevel ? walletConfig.settings.feeLevel : 'normal';
// Platform info
var isChromeApp = platformInfo.isChromeApp;
var isCordova = platformInfo.isCordova;
function refresh() {
$timeout(function() {
$scope.$apply();
}, 1);
}
$scope.showWalletSelector = function() {
$scope.walletSelector = true;
refresh();
};
$scope.$on("$ionicView.beforeLeave", function(event, data) { $scope.$on("$ionicView.beforeLeave", function(event, data) {
$ionicConfig.views.swipeBackEnabled(true); $ionicConfig.views.swipeBackEnabled(true);
@ -18,305 +41,338 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$ionicConfig.views.swipeBackEnabled(false); $ionicConfig.views.swipeBackEnabled(false);
}); });
function exitWithError(err) {
$log.info('Error setting wallet selector:' + err);
popupService.showAlert(gettextCatalog.getString(), bwcError.msg(err), function() {
$ionicHistory.nextViewOptions({
disableAnimate: true,
historyRoot: true
});
$ionicHistory.clearHistory();
$state.go('tabs.send');
});
};
function setNoWallet(msg) {
$scope.wallet = null;
$scope.noWalletMessage = gettextCatalog.getString(msg);
$log.warn('Not ready to make the payment:' + msg);
$timeout(function() {
$scope.$apply();
});
};
$scope.$on("$ionicView.beforeEnter", function(event, data) { $scope.$on("$ionicView.beforeEnter", function(event, data) {
toAmount = data.stateParams.toAmount; function setWalletSelector(network, minAmount, cb) {
cachedSendMax = {};
// no min amount? (sendMax) => look for no empty wallets
minAmount = minAmount || 1;
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: network
});
if (!$scope.wallets || !$scope.wallets.length) {
setNoWallet('No wallets available');
return cb();
}
var filteredWallets = [];
var index = 0;
var walletsUpdated = 0;
lodash.each($scope.wallets, function(w) {
walletService.getStatus(w, {}, function(err, status) {
if (err || !status) {
$log.error(err);
} else {
walletsUpdated++;
w.status = status;
if (!status.availableBalanceSat)
$log.debug('No balance available in: ' + w.name);
if (status.availableBalanceSat > minAmount) {
filteredWallets.push(w);
}
}
if (++index == $scope.wallets.length) {
if (!walletsUpdated)
return cb('Could not update any wallet');
if (lodash.isEmpty(filteredWallets)) {
setNoWallet('Insufficent funds');
}
$scope.wallets = lodash.clone(filteredWallets);
return cb();
}
});
});
};
// Setup $scope
// Grab stateParams
tx = {
toAmount: parseInt(data.stateParams.toAmount),
sendMax: data.stateParams.useSendMax == 'true' ? true : false,
toAddress: data.stateParams.toAddress,
description: data.stateParams.description,
paypro: data.stateParams.paypro,
feeLevel: configFeeLevel,
spendUnconfirmed: walletConfig.spendUnconfirmed,
// Vanity tx info (not in the real tx)
recipientType: data.stateParams.recipientType || null,
toName: data.stateParams.toName,
toEmail: data.stateParams.toEmail,
toColor: data.stateParams.toColor,
network: (new bitcore.Address(data.stateParams.toAddress)).network.name,
txp: {},
};
// Other Scope vars
$scope.isCordova = isCordova;
$scope.showAddress = false; $scope.showAddress = false;
$scope.useSendMax = data.stateParams.useSendMax == 'true' ? true : false;
$scope.recipientType = data.stateParams.recipientType || null; updateTx(tx, null, {}, function() {
$scope.toAddress = data.stateParams.toAddress;
$scope.toName = data.stateParams.toName; $scope.walletSelectorTitle = gettextCatalog.getString('Send from');
$scope.toEmail = data.stateParams.toEmail;
$scope.toColor = data.stateParams.toColor; setWalletSelector(tx.network, tx.toAmount, function(err) {
$scope.description = data.stateParams.description; if (err) {
$scope.paypro = data.stateParams.paypro; return exitWithError('Could not update wallets');
$scope.insufficientFunds = false; }
$scope.noMatchingWallet = false;
$scope.paymentExpired = { if ($scope.wallets.length > 1) {
value: false $scope.showWalletSelector();
}; } else if ($scope.wallets.length) {
$scope.remainingTimeStr = { setWallet($scope.wallets[0], tx);
value: null }
}; });
$scope.network = (new bitcore.Address($scope.toAddress)).network.name;
setFee(); });
resetValues();
setwallets();
applyButtonText();
}); });
function setFee(customFeeLevel, cb) {
feeService.getCurrentFeeValue($scope.network, customFeeLevel, function(err, currentFeePerKb) { function getSendMaxInfo(tx, wallet, cb) {
var config = configService.getSync().wallet; if (!tx.sendMax) return cb();
var configFeeLevel = (config.settings && config.settings.feeLevel) ? config.settings.feeLevel : 'normal';
feePerKb = currentFeePerKb; //ongoingProcess.set('retrievingInputs', true);
feeLevel = customFeeLevel ? customFeeLevel : configFeeLevel; walletService.getSendMaxInfo(wallet, {
$scope.feeLevel = feeService.feeOpts[feeLevel]; feePerKb: tx.feeRate,
if (cb) return cb(); excludeUnconfirmedUtxos: !tx.spendUnconfirmed,
returnInputs: true,
}, cb);
};
function getTxp(tx, wallet, dryRun, cb) {
// ToDo: use a credential's (or fc's) function for this
if (tx.description && !wallet.credentials.sharedEncryptingKey) {
var msg = gettextCatalog.getString('Could not add message to imported wallet without shared encrypting key');
$log.warn(msg);
return setSendError(msg);
}
if (tx.toAmount > Number.MAX_SAFE_INTEGER) {
var msg = gettextCatalog.getString('Amount too big');
$log.warn(msg);
return setSendError(msg);
}
var txp = {};
txp.outputs = [{
'toAddress': tx.toAddress,
'amount': tx.toAmount,
'message': tx.description
}];
if (tx.sendMaxInfo) {
txp.inputs = tx.sendMaxInfo.inputs;
txp.fee = tx.sendMaxInfo.fee;
} else {
txp.feeLevel = tx.feeLevel;
}
txp.message = tx.description;
if (tx.paypro) {
txp.payProUrl = tx.paypro.url;
}
txp.excludeUnconfirmedUtxos = !tx.spendUnconfirmed;
txp.dryRun = dryRun;
walletService.createTx(wallet, txp, function(err, ctxp) {
if (err) {
setSendError(err);
return cb(err);
}
return cb(null, ctxp);
});
};
function updateTx(tx, wallet, opts, cb) {
if (opts.clearCache) {
tx.txp = {};
}
$scope.tx = tx;
function updateAmount() {
if (!tx.toAmount) return;
// Amount
tx.amountStr = txFormatService.formatAmountStr(tx.toAmount);
tx.amountValueStr = tx.amountStr.split(' ')[0];
tx.amountUnitStr = tx.amountStr.split(' ')[1];
txFormatService.formatAlternativeStr(tx.toAmount, function(v) {
tx.alternativeAmountStr = v;
});
}
updateAmount();
refresh();
feeService.getFeeRate(tx.network, tx.feeLevel, function(err, feeRate) {
if (err) return cb(err);
tx.feeRate = feeRate;
tx.feeLevelName = feeService.feeOpts[tx.feeLevel];
// End of quick refresh, before wallet is selected.
if (!wallet)
return cb();
getSendMaxInfo(lodash.clone(tx), wallet, function(err, sendMaxInfo) {
if (err) {
var msg = gettextCatalog.getString('Error getting SendMax information');
return setSendError(msg);
}
if (sendMaxInfo) {
$log.debug('Send max info', sendMaxInfo);
if (tx.sendMax && sendMaxInfo.amount == 0) {
setNoWallet('Insufficent funds');
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee'));
return cb('no_funds');
}
tx.sendMaxInfo = sendMaxInfo;
tx.toAmount = tx.sendMaxInfo.amount;
updateAmount();
showSendMaxWarning(sendMaxInfo);
}
refresh();
// txp already generated for this wallet?
if (tx.txp[wallet.id])
return cb();
getTxp(lodash.clone(tx), wallet, opts.dryRun, function(err, txp) {
if (err) return cb(err);
txp.feeStr = txFormatService.formatAmountStr(txp.fee);
txFormatService.formatAlternativeStr(txp.fee, function(v) {
txp.alternativeFeeStr = v;
});
txp.feeRatePerStr = (txp.fee / (txp.amount + txp.fee) * 100).toFixed(2) + '%';
tx.txp[wallet.id] = txp;
$log.debug('Confirm. TX Fully Updated for wallet:' + wallet.id, tx);
return cb();
});
});
}); });
} }
function useSelectedWallet() { function useSelectedWallet() {
if (!$scope.useSendMax) displayValues();
if (!$scope.useSendMax) {
showAmount(tx.toAmount);
}
$scope.onWalletSelect($scope.wallet); $scope.onWalletSelect($scope.wallet);
} }
function applyButtonText(multisig) { function setButtonText(isMultisig, isPayPro) {
$scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' '; $scope.buttonText = gettextCatalog.getString(isCordova ? 'Slide' : 'Click') + ' ';
if ($scope.paypro) { if (isPayPro) {
$scope.buttonText += gettextCatalog.getString('to pay'); $scope.buttonText += gettextCatalog.getString('to pay');
} else if (multisig) { } else if (isMultisig) {
$scope.buttonText += gettextCatalog.getString('to accept'); $scope.buttonText += gettextCatalog.getString('to accept');
} else } else
$scope.buttonText += gettextCatalog.getString('to send'); $scope.buttonText += gettextCatalog.getString('to send');
}; };
function setwallets() {
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network
});
if (!$scope.wallets || !$scope.wallets.length) {
$scope.noMatchingWallet = true;
displayValues();
$log.warn('No ' + $scope.network + ' wallets to make the payment');
$timeout(function() {
$scope.$apply();
});
return;
}
var filteredWallets = [];
var index = 0;
var enoughFunds = false;
var walletsUpdated = 0;
lodash.each($scope.wallets, function(w) {
walletService.getStatus(w, {}, function(err, status) {
if (err || !status) {
$log.error(err);
} else {
walletsUpdated++;
w.status = status;
if (!status.availableBalanceSat) $log.debug('No balance available in: ' + w.name);
if (status.availableBalanceSat > toAmount) {
filteredWallets.push(w);
enoughFunds = true;
}
}
if (++index == $scope.wallets.length) {
if (!lodash.isEmpty(filteredWallets)) {
$scope.wallets = lodash.clone(filteredWallets);
if ($scope.useSendMax) {
if ($scope.wallets.length > 1)
$scope.showWalletSelector();
else {
$scope.wallet = $scope.wallets[0];
$scope.getSendMaxInfo();
}
} else initConfirm();
} else {
// Were we able to update any wallet?
if (walletsUpdated) {
if (!enoughFunds) $scope.insufficientFunds = true;
displayValues();
$log.warn('No wallet available to make the payment');
} else {
popupService.showAlert(gettextCatalog.getString('Could not update wallets'), bwcError.msg(err), function() {
$ionicHistory.nextViewOptions({
disableAnimate: true,
historyRoot: true
});
$ionicHistory.clearHistory();
$state.go('tabs.send');
});
}
}
$timeout(function() {
$scope.$apply();
});
}
});
});
};
$scope.toggleAddress = function() { $scope.toggleAddress = function() {
$scope.showAddress = !$scope.showAddress; $scope.showAddress = !$scope.showAddress;
}; };
var initConfirm = function() {
if ($scope.paypro) _paymentTimeControl($scope.paypro.expires);
displayValues(); function showSendMaxWarning(sendMaxInfo) {
if ($scope.wallets.length > 1) $scope.showWalletSelector();
else setWallet($scope.wallets[0]);
$timeout(function() {
$scope.$apply();
});
};
function displayValues() { function verifyExcludedUtxos() {
toAmount = parseInt(toAmount); var warningMsg = [];
$scope.amountStr = txFormatService.formatAmountStr(toAmount); if (sendMaxInfo.utxosBelowFee > 0) {
$scope.displayAmount = getDisplayAmount($scope.amountStr); warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", {
$scope.displayUnit = getDisplayUnit($scope.amountStr); amountBelowFeeStr: txFormatService.formatAmountStr(sendMaxInfo.amountBelowFee)
txFormatService.formatAlternativeStr(toAmount, function(v) { }));
$scope.alternativeAmountStr = v;
});
};
function resetValues() {
$scope.displayAmount = $scope.displayUnit = $scope.fee = $scope.feeFiat = $scope.feeRateStr = $scope.alternativeAmountStr = $scope.insufficientFunds = $scope.noMatchingWallet = null;
$scope.showAddress = false;
};
$scope.getSendMaxInfo = function() {
resetValues();
var config = configService.getSync().wallet;
ongoingProcess.set('retrievingInputs', true);
walletService.getSendMaxInfo($scope.wallet, {
feePerKb: feePerKb,
excludeUnconfirmedUtxos: !config.spendUnconfirmed,
returnInputs: true,
}, function(err, resp) {
ongoingProcess.set('retrievingInputs', false);
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
} }
if (resp.amount == 0) { if (sendMaxInfo.utxosAboveMaxSize > 0) {
$scope.insufficientFunds = true; warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", {
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee')); amountAboveMaxSizeStr: txFormatService.formatAmountStr(sendMaxInfo.amountAboveMaxSize)
return; }));
} }
return warningMsg.join('\n');
};
$scope.sendMaxInfo = { var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", {
sendMax: true, fee: txFormatService.formatAmountStr(sendMaxInfo.fee)
amount: resp.amount,
inputs: resp.inputs,
fee: resp.fee,
feePerKb: feePerKb,
};
cachedSendMax[$scope.wallet.id] = $scope.sendMaxInfo;
var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", {
fee: txFormatService.formatAmountStr(resp.fee)
});
var warningMsg = verifyExcludedUtxos();
if (!lodash.isEmpty(warningMsg))
msg += '\n' + warningMsg;
popupService.showAlert(null, msg, function() {
setSendMaxValues(resp);
createTx($scope.wallet, true, function(err, txp) {
if (err) return;
cachedTxp[$scope.wallet.id] = txp;
apply(txp);
});
});
function verifyExcludedUtxos() {
var warningMsg = [];
if (resp.utxosBelowFee > 0) {
warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", {
amountBelowFeeStr: txFormatService.formatAmountStr(resp.amountBelowFee)
}));
}
if (resp.utxosAboveMaxSize > 0) {
warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", {
amountAboveMaxSizeStr: txFormatService.formatAmountStr(resp.amountAboveMaxSize)
}));
}
return warningMsg.join('\n');
};
}); });
}; var warningMsg = verifyExcludedUtxos();
function setSendMaxValues(data) { if (!lodash.isEmpty(warningMsg))
resetValues(); msg += '\n' + warningMsg;
var config = configService.getSync().wallet;
var unitToSatoshi = config.settings.unitToSatoshi;
var satToUnit = 1 / unitToSatoshi;
var unitDecimals = config.settings.unitDecimals;
$scope.amountStr = txFormatService.formatAmountStr(data.amount, true); popupService.showAlert(null, msg, function() {});
$scope.displayAmount = getDisplayAmount($scope.amountStr);
$scope.displayUnit = getDisplayUnit($scope.amountStr);
$scope.fee = txFormatService.formatAmountStr(data.fee);
txFormatService.formatAlternativeStr(data.fee, function(v) {
$scope.feeFiat = v;
});
toAmount = parseFloat((data.amount * satToUnit).toFixed(unitDecimals));
txFormatService.formatAlternativeStr(data.amount, function(v) {
$scope.alternativeAmountStr = v;
});
$scope.feeRateStr = (data.fee / (data.amount + data.fee) * 100).toFixed(2) + '%';
$timeout(function() {
$scope.$apply();
});
};
$scope.$on('accepted', function(event) {
$scope.approve();
});
$scope.showWalletSelector = function() {
$scope.walletSelectorTitle = gettextCatalog.getString('Send from');
if (!$scope.useSendMax && ($scope.insufficientFunds || $scope.noMatchingWallet)) return;
$scope.showWallets = true;
}; };
$scope.onWalletSelect = function(wallet) { $scope.onWalletSelect = function(wallet) {
if ($scope.useSendMax) { setWallet(wallet, tx);
$scope.wallet = wallet;
if (cachedSendMax[wallet.id]) {
$log.debug('Send max cached for wallet:', wallet.id);
setSendMaxValues(cachedSendMax[wallet.id]);
return;
}
$scope.getSendMaxInfo();
} else
setWallet(wallet);
applyButtonText(wallet.credentials.m > 1);
}; };
$scope.showDescriptionPopup = function() { $scope.showDescriptionPopup = function(tx) {
var message = gettextCatalog.getString('Add description'); var message = gettextCatalog.getString('Add description');
var opts = { var opts = {
defaultText: $scope.description defaultText: tx.description
}; };
popupService.showPrompt(null, message, opts, function(res) { popupService.showPrompt(null, message, opts, function(res) {
if (typeof res != 'undefined') $scope.description = res; if (typeof res != 'undefined') tx.description = res;
$timeout(function() { $timeout(function() {
$scope.$apply(); $scope.$apply();
}); });
}); });
}; };
function getDisplayAmount(amountStr) {
return $scope.amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return $scope.amountStr.split(' ')[1];
};
function _paymentTimeControl(expirationTime) { function _paymentTimeControl(expirationTime) {
$scope.paymentExpired.value = false; $scope.paymentExpired = false;
setExpirationTime(); setExpirationTime();
countDown = $interval(function() { countDown = $interval(function() {
@ -334,12 +390,12 @@ angular.module('copayApp.controllers').controller('confirmController', function(
var totalSecs = expirationTime - now; var totalSecs = expirationTime - now;
var m = Math.floor(totalSecs / 60); var m = Math.floor(totalSecs / 60);
var s = totalSecs % 60; var s = totalSecs % 60;
$scope.remainingTimeStr.value = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2); $scope.remainingTimeStr = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
}; };
function setExpiredValues() { function setExpiredValues() {
$scope.paymentExpired.value = true; $scope.paymentExpired = true;
$scope.remainingTimeStr.value = gettextCatalog.getString('Expired'); $scope.remainingTimeStr = gettextCatalog.getString('Expired');
if (countDown) $interval.cancel(countDown); if (countDown) $interval.cancel(countDown);
$timeout(function() { $timeout(function() {
$scope.$apply(); $scope.$apply();
@ -347,31 +403,27 @@ angular.module('copayApp.controllers').controller('confirmController', function(
}; };
}; };
function setWallet(wallet, delayed) { /* sets a wallet on the UI, creates a TXPs for that wallet */
var stop;
function setWallet(wallet, tx) {
$scope.wallet = wallet; $scope.wallet = wallet;
$scope.fee = $scope.txp = null;
if (stop) {
$timeout.cancel(stop);
stop = null;
}
if (cachedTxp[wallet.id]) { setButtonText(wallet.credentials.m > 1, !!tx.paypro);
apply(cachedTxp[wallet.id]);
} else { if (tx.paypro)
stop = $timeout(function() { _paymentTimeControl(tx.paypro.expires);
createTx(wallet, true, function(err, txp) {
if (err) return; updateTx(tx, wallet, {
cachedTxp[wallet.id] = txp; dryRun: true
apply(txp); }, function(err) {
}); $timeout(function() {
}, delayed ? 2000 : 1); $ionicScrollDelegate.resize();
} $scope.$apply();
}, 10);
});
$timeout(function() {
$ionicScrollDelegate.resize();
$scope.$apply();
}, 10);
}; };
var setSendError = function(msg) { var setSendError = function(msg) {
@ -382,75 +434,6 @@ angular.module('copayApp.controllers').controller('confirmController', function(
popupService.showAlert(gettextCatalog.getString('Error at confirm'), bwcError.msg(msg)); popupService.showAlert(gettextCatalog.getString('Error at confirm'), bwcError.msg(msg));
}; };
function apply(txp) {
$scope.fee = txFormatService.formatAmountStr(txp.fee);
txFormatService.formatAlternativeStr(txp.fee, function(v) {
$scope.feeFiat = v;
});
$scope.txp = txp;
$scope.feeRateStr = (txp.fee / (txp.amount + txp.fee) * 100).toFixed(2) + '%';
$timeout(function() {
$scope.$apply();
});
};
var createTx = function(wallet, dryRun, cb) {
var config = configService.getSync().wallet;
var currentSpendUnconfirmed = config.spendUnconfirmed;
var paypro = $scope.paypro;
var toAddress = $scope.toAddress;
var description = $scope.description;
var unitToSatoshi = config.settings.unitToSatoshi;
var unitDecimals = config.settings.unitDecimals;
// ToDo: use a credential's (or fc's) function for this
if (description && !wallet.credentials.sharedEncryptingKey) {
var msg = gettextCatalog.getString('Could not add message to imported wallet without shared encrypting key');
$log.warn(msg);
return setSendError(msg);
}
if (toAmount > Number.MAX_SAFE_INTEGER) {
var msg = gettextCatalog.getString('Amount too big');
$log.warn(msg);
return setSendError(msg);
}
var txp = {};
var amount;
if ($scope.useSendMax) amount = parseFloat((toAmount * unitToSatoshi).toFixed(0));
else amount = toAmount;
txp.outputs = [{
'toAddress': toAddress,
'amount': amount,
'message': description
}];
if ($scope.sendMaxInfo) {
txp.inputs = $scope.sendMaxInfo.inputs;
txp.fee = $scope.sendMaxInfo.fee;
} else
txp.feeLevel = feeLevel;
txp.message = description;
if (paypro) {
txp.payProUrl = paypro.url;
}
txp.excludeUnconfirmedUtxos = !currentSpendUnconfirmed;
txp.dryRun = dryRun;
walletService.createTx(wallet, txp, function(err, ctxp) {
if (err) {
setSendError(err);
return cb(err);
}
return cb(null, ctxp);
});
};
$scope.openPPModal = function() { $scope.openPPModal = function() {
$ionicModal.fromTemplateUrl('views/modals/paypro.html', { $ionicModal.fromTemplateUrl('views/modals/paypro.html', {
scope: $scope scope: $scope
@ -464,14 +447,11 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$scope.payproModal.hide(); $scope.payproModal.hide();
}; };
$scope.approve = function(onSendStatusChange) { $scope.approve = function(tx, wallet, onSendStatusChange) {
var wallet = $scope.wallet; if (!tx || !wallet) return;
if (!wallet) {
return;
}
if ($scope.paypro && $scope.paymentExpired.value) { if ($scope.paymentExpired) {
popupService.showAlert(null, gettextCatalog.getString('This bitcoin payment request has expired.')); popupService.showAlert(null, gettextCatalog.getString('This bitcoin payment request has expired.'));
$scope.sendStatus = ''; $scope.sendStatus = '';
$timeout(function() { $timeout(function() {
@ -481,46 +461,54 @@ angular.module('copayApp.controllers').controller('confirmController', function(
} }
ongoingProcess.set('creatingTx', true, onSendStatusChange); ongoingProcess.set('creatingTx', true, onSendStatusChange);
createTx(wallet, false, function(err, txp) { getTxp(lodash.clone(tx), wallet, false, function(err, txp) {
ongoingProcess.set('creatingTx', false, onSendStatusChange); ongoingProcess.set('creatingTx', false, onSendStatusChange);
if (err) return; if (err) return;
var config = configService.getSync(); // confirm txs for more that 20usd, if not spending/touchid is enabled
var spendingPassEnabled = walletService.isEncrypted(wallet); function confirmTx(cb) {
var touchIdEnabled = config.touchIdFor && config.touchIdFor[wallet.id]; if (walletService.isEncrypted(wallet))
var isCordova = $scope.isCordova; return cb();
var bigAmount = parseFloat(txFormatService.formatToUSD(txp.amount)) > 20;
var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', {
amountStr: $scope.amountStr,
name: wallet.name
});
var okText = gettextCatalog.getString('Confirm');
var cancelText = gettextCatalog.getString('Cancel');
if (!spendingPassEnabled && !touchIdEnabled) { var amountUsd = parseFloat(txFormatService.formatToUSD(txp.amount));
if (isCordova) { if (amountUsd <= CONFIRM_LIMIT_USD)
if (bigAmount) { return cb();
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) { var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', {
$scope.sendStatus = ''; amountStr: tx.amountStr,
$timeout(function() { name: wallet.name
$scope.$apply(); });
}); var okText = gettextCatalog.getString('Confirm');
return; var cancelText = gettextCatalog.getString('Cancel');
} popupService.showConfirm(null, message, okText, cancelText, function(ok) {
publishAndSign(wallet, txp, onSendStatusChange); return cb(!ok);
}); });
} else publishAndSign(wallet, txp, onSendStatusChange); };
} else {
popupService.showConfirm(null, message, okText, cancelText, function(ok) { function publishAndSign() {
if (!ok) { if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
$scope.sendStatus = ''; $log.info('No signing proposal: No private key');
return;
} return walletService.onlyPublish(wallet, txp, function(err) {
publishAndSign(wallet, txp, onSendStatusChange); if (err) setSendError(err);
}); }, onSendStatusChange);
} }
} else publishAndSign(wallet, txp, onSendStatusChange);
walletService.publishAndSign(wallet, txp, function(err, txp) {
if (err) return setSendError(err);
}, onSendStatusChange);
};
confirmTx(function(nok) {
if (nok) {
$scope.sendStatus = '';
$timeout(function() {
$scope.$apply();
});
return;
}
publishAndSign();
});
}); });
}; };
@ -543,68 +531,47 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$scope.statusChangeHandler = statusChangeHandler; $scope.statusChangeHandler = statusChangeHandler;
$scope.onConfirm = function() {
$scope.approve(statusChangeHandler);
};
$scope.onSuccessConfirm = function() { $scope.onSuccessConfirm = function() {
var previousView = $ionicHistory.viewHistory().backView && $ionicHistory.viewHistory().backView.stateName;
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$ionicHistory.removeBackView();
$scope.sendStatus = ''; $scope.sendStatus = '';
$ionicHistory.nextViewOptions({ $ionicHistory.nextViewOptions({
disableAnimate: true, disableAnimate: true,
historyRoot: true historyRoot: true
}); });
$ionicHistory.clearHistory();
$state.go('tabs.send').then(function() { $state.go('tabs.send').then(function() {
$ionicHistory.clearHistory();
$state.transitionTo('tabs.home'); $state.transitionTo('tabs.home');
}); });
}; };
function publishAndSign(wallet, txp, onSendStatusChange) { $scope.chooseFeeLevel = function(tx, wallet) {
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { var scope = $rootScope.$new(true);
$log.info('No signing proposal: No private key'); scope.network = tx.network;
scope.feeLevel = tx.feeLevel;
scope.noSave = true;
return walletService.onlyPublish(wallet, txp, function(err) {
if (err) setSendError(err);
}, onSendStatusChange);
}
walletService.publishAndSign(wallet, txp, function(err, txp) {
if (err) return setSendError(err);
}, onSendStatusChange);
};
$scope.chooseFeeLevel = function() {
$scope.customFeeLevel = feeLevel;
$ionicModal.fromTemplateUrl('views/modals/chooseFeeLevel.html', { $ionicModal.fromTemplateUrl('views/modals/chooseFeeLevel.html', {
scope: $scope, scope: scope,
}).then(function(modal) { }).then(function(modal) {
$scope.chooseFeeLevelModal = modal; scope.chooseFeeLevelModal = modal;
$scope.openModal(); scope.openModal();
}); });
$scope.openModal = function() { scope.openModal = function() {
$scope.chooseFeeLevelModal.show(); scope.chooseFeeLevelModal.show();
}; };
$scope.hideModal = function(customFeeLevel) {
if (customFeeLevel) { scope.hideModal = function(customFeeLevel) {
cachedTxp = {}; $log.debug('Custom fee level choosen:' + customFeeLevel + ' was:' + tx.feeLevel);
cachedSendMax = {}; if (tx.feeLevel == customFeeLevel)
ongoingProcess.set('gettingFeeLevels', true); scope.chooseFeeLevelModal.hide();
setFee(customFeeLevel, function() {
ongoingProcess.set('gettingFeeLevels', false); tx.feeLevel = customFeeLevel;
resetValues(); updateTx(tx, wallet, {
if ($scope.wallet) useSelectedWallet(); clearCache: true,
}) dryRun: true,
} }, function() {
$scope.chooseFeeLevelModal.hide(); scope.chooseFeeLevelModal.hide();
});
}; };
}; };

View file

@ -60,7 +60,7 @@ angular.module('copayApp.controllers').controller('paperWalletController',
$scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, null, function(err, testTx) { $scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, null, function(err, testTx) {
if (err) return cb(err); if (err) return cb(err);
var rawTxLength = testTx.serialize().length; var rawTxLength = testTx.serialize().length;
feeService.getCurrentFeeValue('livenet', null, function(err, feePerKB) { feeService.getCurrentFeeRate('livenet', null, function(err, feePerKB) {
var opts = {}; var opts = {};
opts.fee = Math.round((feePerKB * rawTxLength) / 2000); opts.fee = Math.round((feePerKB * rawTxLength) / 2000);
$scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, opts, function(err, tx) { $scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, opts, function(err, tx) {

View file

@ -2,13 +2,14 @@
angular.module('copayApp.controllers').controller('preferencesFeeController', function($scope, $timeout, $ionicHistory, lodash, gettextCatalog, configService, feeService, ongoingProcess, popupService) { angular.module('copayApp.controllers').controller('preferencesFeeController', function($scope, $timeout, $ionicHistory, lodash, gettextCatalog, configService, feeService, ongoingProcess, popupService) {
$scope.save = function(newFee) { var network;
if ($scope.customFeeLevel) { $scope.save = function(newFee) {
$scope.currentFeeLevel = newFee; $scope.currentFeeLevel = newFee;
updateCurrentValues(); updateCurrentValues();
if ($scope.noSave)
return; return;
}
var opts = { var opts = {
wallet: { wallet: {
@ -20,8 +21,6 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
configService.set(opts, function(err) { configService.set(opts, function(err) {
if (err) $log.debug(err); if (err) $log.debug(err);
$scope.currentFeeLevel = newFee;
updateCurrentValues();
$timeout(function() { $timeout(function() {
$scope.$apply(); $scope.$apply();
}); });
@ -33,8 +32,10 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
}); });
$scope.init = function() { $scope.init = function() {
$scope.network = $scope.network || 'livenet';
$scope.feeOpts = feeService.feeOpts; $scope.feeOpts = feeService.feeOpts;
$scope.currentFeeLevel = $scope.customFeeLevel ? $scope.customFeeLevel : feeService.getCurrentFeeLevel(); $scope.currentFeeLevel = $scope.feeLevel || feeService.getCurrentFeeLevel();
$scope.loadingFee = true; $scope.loadingFee = true;
feeService.getFeeLevels(function(err, levels) { feeService.getFeeLevels(function(err, levels) {
$scope.loadingFee = false; $scope.loadingFee = false;
@ -51,16 +52,19 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
var updateCurrentValues = function() { var updateCurrentValues = function() {
if (lodash.isEmpty($scope.feeLevels) || lodash.isEmpty($scope.currentFeeLevel)) return; if (lodash.isEmpty($scope.feeLevels) || lodash.isEmpty($scope.currentFeeLevel)) return;
var feeLevelValue = lodash.find($scope.feeLevels['livenet'], {
var value = lodash.find($scope.feeLevels[$scope.network], {
level: $scope.currentFeeLevel level: $scope.currentFeeLevel
}); });
if (lodash.isEmpty(feeLevelValue)) {
if (lodash.isEmpty(value)) {
$scope.feePerSatByte = null; $scope.feePerSatByte = null;
$scope.avgConfirmationTime = null; $scope.avgConfirmationTime = null;
return; return;
} }
$scope.feePerSatByte = (feeLevelValue.feePerKB / 1000).toFixed();
$scope.avgConfirmationTime = feeLevelValue.nbBlocks * 10; $scope.feePerSatByte = (value.feePerKB / 1000).toFixed();
$scope.avgConfirmationTime = value.nbBlocks * 10;
}; };
$scope.chooseNewFee = function() { $scope.chooseNewFee = function() {

View file

@ -8,9 +8,7 @@ angular.module('copayApp.directives')
transclude: true, transclude: true,
scope: { scope: {
sendStatus: '=clickSendStatus', sendStatus: '=clickSendStatus',
hasWalletChosen: '=hasWalletChosen', isDisabled: '=isDisabled',
insufficientFunds: '=insufficientFunds',
noMatchingWallet: '=noMatchingWallet'
}, },
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
scope.$watch('sendStatus', function() { scope.$watch('sendStatus', function() {

View file

@ -12,7 +12,7 @@ angular.module('copayApp.directives')
elem.bind('click', function() { elem.bind('click', function() {
configService.whenAvailable(function(config) { configService.whenAvailable(function(config) {
if (config.wallet.settings.feeLevel.match(/conomy/)) { if (config.wallet.settings.feeLevel && config.wallet.settings.feeLevel.match(/conomy/)) {
$log.debug('Economy Fee setting... disabling link:' + elem.text()); $log.debug('Economy Fee setting... disabling link:' + elem.text());
popupService.showAlert('Low Fee Error', 'Please change your Bitcoin Network Fee Policy setting to Normal or higher to use this service', function() { popupService.showAlert('Low Fee Error', 'Please change your Bitcoin Network Fee Policy setting to Normal or higher to use this service', function() {
$ionicHistory.goBack(); $ionicHistory.goBack();

View file

@ -9,7 +9,7 @@ angular.module('copayApp.directives')
scope: { scope: {
sendStatus: '=slideSendStatus', sendStatus: '=slideSendStatus',
onConfirm: '&slideOnConfirm', onConfirm: '&slideOnConfirm',
wallet: '=hasWalletChosen' isDisabled: '=isDisabled'
}, },
link: function(scope, element, attrs) { link: function(scope, element, attrs) {

View file

@ -110,7 +110,7 @@ angular.module('copayApp.services')
body = gettextCatalog.getString('Amount below minimum allowed'); body = gettextCatalog.getString('Amount below minimum allowed');
break; break;
case 'INCORRECT_ADDRESS_NETWORK': case 'INCORRECT_ADDRESS_NETWORK':
body = gettextCatalog.getString('Incorrect address network'); body = gettextCatalog.getString('Incorrect network address');
break; break;
case 'COPAYER_REGISTERED': case 'COPAYER_REGISTERED':
body = gettextCatalog.getString('Key already associated with an existing wallet'); body = gettextCatalog.getString('Key already associated with an existing wallet');

View file

@ -1,8 +1,10 @@
'use strict'; 'use strict';
angular.module('copayApp.services').factory('feeService', function($log, $stateParams, bwcService, walletService, configService, gettext, lodash, txFormatService, gettextCatalog) { angular.module('copayApp.services').factory('feeService', function($log, $timeout, $stateParams, bwcService, walletService, configService, gettext, lodash, txFormatService, gettextCatalog) {
var root = {}; var root = {};
var CACHE_TIME_TS = 60; // 1 min
// Constant fee options to translate // Constant fee options to translate
root.feeOpts = { root.feeOpts = {
urgent: gettext('Urgent'), urgent: gettext('Urgent'),
@ -12,22 +14,26 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP
superEconomy: gettext('Super Economy') superEconomy: gettext('Super Economy')
}; };
var cache = {
updateTs: 0,
};
root.getCurrentFeeLevel = function() { root.getCurrentFeeLevel = function() {
return configService.getSync().wallet.settings.feeLevel || 'normal'; return configService.getSync().wallet.settings.feeLevel || 'normal';
}; };
root.getCurrentFeeValue = function(network, customFeeLevel, cb) {
network = network || 'livenet';
var feeLevel = customFeeLevel || root.getCurrentFeeLevel();
root.getFeeLevels(function(err, levels) { root.getFeeRate = function(network, feeLevel, cb) {
network = network || 'livenet';
root.getFeeLevels(function(err, levels, fromCache) {
if (err) return cb(err); if (err) return cb(err);
var feeLevelValue = lodash.find(levels[network], { var feeLevelRate = lodash.find(levels[network], {
level: feeLevel level: feeLevel
}); });
if (!feeLevelValue || !feeLevelValue.feePerKB) { if (!feeLevelRate || !feeLevelRate.feePerKB) {
return cb({ return cb({
message: gettextCatalog.getString("Could not get dynamic fee for level: {{feeLevel}}", { message: gettextCatalog.getString("Could not get dynamic fee for level: {{feeLevel}}", {
feeLevel: feeLevel feeLevel: feeLevel
@ -35,14 +41,26 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP
}); });
} }
var fee = feeLevelValue.feePerKB; var feeRate = feeLevelRate.feePerKB;
$log.debug('Dynamic fee: ' + feeLevel + ' ' + fee + ' SAT');
return cb(null, fee); if (!fromCache) $log.debug('Dynamic fee: ' + feeLevel + '/' + network +' ' + (feeLevelRate.feePerKB / 1000).toFixed() + ' SAT/B');
return cb(null, feeRate);
}); });
}; };
root.getCurrentFeeRate = function(network, cb) {
return root.getFeeRate(network, root.getCurrentFeeLevel(), cb);
};
root.getFeeLevels = function(cb) { root.getFeeLevels = function(cb) {
if (cache.updateTs > Date.now() - CACHE_TIME_TS * 1000 ) {
$timeout( function() {
return cb(null, cache.data, true);
}, 1);
}
var walletClient = bwcService.getClient(); var walletClient = bwcService.getClient();
var unitName = configService.getSync().wallet.settings.unitName; var unitName = configService.getSync().wallet.settings.unitName;
@ -51,10 +69,14 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP
if (errLivenet || errTestnet) { if (errLivenet || errTestnet) {
return cb(gettextCatalog.getString('Could not get dynamic fee')); return cb(gettextCatalog.getString('Could not get dynamic fee'));
} }
return cb(null, {
cache.updateTs =Date.now();
cache.data = {
'livenet': levelsLivenet, 'livenet': levelsLivenet,
'testnet': levelsTestnet 'testnet': levelsTestnet
}); };
return cb(null, cache.data);
}); });
}); });
}; };

View file

@ -10,7 +10,7 @@ angular.module('copayApp.services').service('sendMaxService', function(feeServic
* *
*/ */
this.getInfo = function(wallet, cb) { this.getInfo = function(wallet, cb) {
feeService.getCurrentFeeValue(wallet.credentials.network, null, function(err, feePerKb) { feeService.getCurrentFeeRate(wallet.credentials.network, null, function(err, feePerKb) {
if (err) return cb(err); if (err) return cb(err);
var config = configService.getSync().wallet; var config = configService.getSync().wallet;

View file

@ -53,23 +53,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!wallet"
ng-click="buyConfirm()" ng-click="buyConfirm()"
ng-if="!isCordova" ng-if="!isCordova"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!wallet">
insufficient-funds="false"
no-matching-wallet="false">
Confirm purchase Confirm purchase
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!wallet"
ng-if="isCordova" ng-if="isCordova"
slide-on-confirm="buyConfirm()" slide-on-confirm="buyConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!wallet">
insufficient-funds="false"
no-matching-wallet="false">
Slide to buy Slide to buy
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success

View file

@ -17,7 +17,7 @@
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{amountUnitStr}}</div> <div class="amount">{{amountUnitStr}}</div>
<div class="alternative" ng-if="buyPrice"> <div class="alternative" ng-if="buyPrice">
<span ng-show="isFiat">{{buyRequestInfo.amount.amount}} {{buyRequestInfo.amount.currency}}</span> <span ng-show="isFiat">{{buyRequestInfo.amount.amount}} {{buyRequestInfo.amount.currency}}</span>
@ ${{buyPrice.amount}} per BTC @ ${{buyPrice.amount}} per BTC
</div> </div>
</div> </div>
@ -59,7 +59,7 @@
{{fee.type}} fee {{fee.type}} fee
</span> </span>
<span class="item-note"> <span class="item-note">
{{fee.amount.amount}} {{fee.amount.currency}} {{fee.amount.amount}} {{fee.amount.currency}}
</span> </span>
</div> </div>
<div class="item"> <div class="item">
@ -74,23 +74,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet"
ng-click="buyConfirm()" ng-click="buyConfirm()"
ng-if="!isCordova && buyRequestInfo" ng-if="!isCordova && buyRequestInfo"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet">
insufficient-funds="!selectedPaymentMethodId.value"
no-matching-wallet="!buyRequestInfo">
Confirm purchase Confirm purchase
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet"
ng-if="isCordova && buyRequestInfo" ng-if="isCordova && buyRequestInfo"
slide-on-confirm="buyConfirm()" slide-on-confirm="buyConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet">
insufficient-funds="!selectedPaymentMethodId.value"
no-matching-wallet="!buyRequestInfo">
Slide to buy Slide to buy
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success

View file

@ -17,8 +17,8 @@
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{amountUnitStr}}</div> <div class="amount">{{amountUnitStr}}</div>
<div class="alternative"> <div class="alternative">
<span ng-show="!isFiat">{{buyInfo.subtotal}} {{buyInfo.currency}}</span> <span ng-show="!isFiat">{{buyInfo.subtotal}} {{buyInfo.currency}}</span>
<span ng-show="isFiat">{{buyInfo.qty}} BTC</span> <span ng-show="isFiat">{{buyInfo.qty}} BTC</span>
@ ${{buyInfo.price}} per BTC @ ${{buyInfo.price}} per BTC
</div> </div>
</div> </div>
@ -64,23 +64,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!buyInfo || !wallet"
ng-click="buyConfirm()" ng-click="buyConfirm()"
ng-if="!isCordova && buyInfo" ng-if="!isCordova && buyInfo"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!buyInfo || !wallet">
insufficient-funds="false"
no-matching-wallet="!buyInfo">
Confirm purchase Confirm purchase
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!buyInfo || !wallet"
ng-if="isCordova && buyInfo" ng-if="isCordova && buyInfo"
slide-on-confirm="buyConfirm()" slide-on-confirm="buyConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!buyInfo || !wallet">
insufficient-funds="false"
no-matching-wallet="!buyInfo">
Slide to buy Slide to buy
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success

View file

@ -7,24 +7,24 @@
</ion-nav-back-button> </ion-nav-back-button>
</ion-nav-bar> </ion-nav-bar>
<ion-content ng-class="{'add-bottom-for-cta': !insufficientFunds && !noMatchingWallet}"> <ion-content class="add-bottom-for-cta">
<div class="list"> <div class="list">
<div class="item head"> <div class="item head">
<div class="sending-label"> <div class="sending-label">
<img src="img/icon-tx-sent-outline.svg"> <img src="img/icon-tx-sent-outline.svg">
<span translate ng-if="!useSendMax">Sending</span> <span translate ng-if="!tx.sendMax">Sending</span>
<span translate ng-if="useSendMax">Sending maximum amount</span> <span translate ng-if="tx.sendMax">Sending maximum amount</span>
</div> </div>
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{displayAmount || '...'}} <span class="unit">{{displayUnit}}</span></div> <div class="amount">{{tx.amountValueStr || '...'}} <span class="unit">{{tx.amountUnitStr}}</span></div>
<div class="alternative">{{alternativeAmountStr || '...'}}</div> <div class="alternative">{{tx.alternativeAmountStr || '...'}}</div>
</div> </div>
</div> </div>
<div class="info"> <div class="info">
<div class="item single-line" ng-if="paypro"> <div class="item single-line" ng-if="tx.paypro">
<span class="label" translate>Payment Expires:</span> <span class="label" translate>Payment Expires:</span>
<span class="item-note" ng-if="!paymentExpired.value">{{remainingTimeStr.value}}</span> <span class="item-note" ng-if="!paymentExpired">{{remainingTimeStr}}</span>
<span class="item-note" ng-if="paymentExpired.value" ng-style="{'color': 'red'}" translate>Expired</span> <span class="item-note" ng-if="paymentExpired" ng-style="{'color': 'red'}" translate>Expired</span>
</div> </div>
<div class="item"> <div class="item">
@ -32,36 +32,36 @@
<span class="payment-proposal-to" ng-if="!recipientType"> <span class="payment-proposal-to" ng-if="!recipientType">
<img src="img/icon-bitcoin-small.svg"> <img src="img/icon-bitcoin-small.svg">
<div copy-to-clipboard="toAddress" ng-if="!paypro" class="ellipsis"> <div copy-to-clipboard="tx.toAddress" ng-if="!tx.paypro" class="ellipsis">
<contact ng-if="!toName" address="{{toAddress}}"></contact> <contact ng-if="!tx.toName" address="{{tx.toAddress}}"></contact>
<span class="m15l size-14" ng-if="toName">{{toName}}</span> <span class="m15l size-14" ng-if="tx.toName">{{tx.toName}}</span>
</div> </div>
<div ng-if="paypro" ng-click="openPPModal(paypro)" class="m15l size-14 w100p pointer"> <div ng-if="tx.paypro" ng-click="openPPModal(tx.paypro)" class="m15l size-14 w100p pointer">
<i ng-show="paypro.verified && paypro.caTrusted" class="ion-locked" style="color:green"></i> <i ng-show="tx.paypro.verified && tx.paypro.caTrusted" class="ion-locked" style="color:green"></i>
<i ng-show="!paypro.caTrusted" class="ion-unlocked" style="color:red"></i> <i ng-show="!tx.paypro.caTrusted" class="ion-unlocked" style="color:red"></i>
<span class="ellipsis" ng-show="!toName">{{paypro.domain || paypro.toAddress}}</span> <span class="ellipsis" ng-show="!tx.toName">{{tx.paypro.domain || tx.paypro.toAddress}}</span>
<span ng-show="toName">{{toName}}</span> <span ng-show="tx.toName">{{tx.toName}}</span>
</div> </div>
<!-- <contact ng-if="!tx.hasMultiplesOutputs" class="ellipsis" address="{{toAddress}}"></contact> <!-- <contact ng-if="!tx.hasMultiplesOutputs" class="ellipsis" address="{{tx.toAddress}}"></contact>
<span ng-if="tx.hasMultiplesOutputs" translate>Multiple recipients</span> --> <span ng-if="tx.hasMultiplesOutputs" translate>Multiple recipients</span> -->
</span> </span>
<div class="wallet" ng-if="recipientType == 'wallet'"> <div class="wallet" ng-if="recipientType == 'wallet'">
<i class="icon big-icon-svg"> <i class="icon big-icon-svg">
<img src="img/icon-wallet.svg" ng-class="{'wallet-background-color-default': !toColor}" ng-style="{'background-color': toColor}" class="bg"/> <img src="img/icon-wallet.svg" ng-class="{'wallet-background-color-default': !toColor}" ng-style="{'background-color': toColor}" class="bg"/>
</i> </i>
<div copy-to-clipboard="toAddress" class="ellipsis"> <div copy-to-clipboard="tx.toAddress" class="ellipsis">
<contact ng-if="!toName" address="{{toAddress}}"></contact> <contact ng-if="!tx.toName" address="{{tx.toAddress}}"></contact>
<span ng-if="toName" class="wallet-name">{{toName}}</span> <span ng-if="tx.toName" class="wallet-name">{{tx.toName}}</span>
</div> </div>
</div> </div>
<div ng-if="recipientType == 'contact' && !isChromeApp" class="gravatar-contact toggle" ng-click="toggleAddress()"> <div ng-if="recipientType == 'contact' && !isChromeApp" class="gravatar-contact toggle" ng-click="toggleAddress()">
<gravatar class="send-gravatar" name="{{toName}}" height="30" width="30" email="{{toEmail}}"></gravatar> <gravatar class="send-gravatar" name="{{tx.toName}}" height="30" width="30" email="{{toEmail}}"></gravatar>
<span ng-if="toName && !showAddress">{{toName}}</span> <span ng-if="tx.toName && !showAddress">{{tx.toName}}</span>
<span ng-if="toName && showAddress">{{toAddress}}</span> <span ng-if="tx.toName && showAddress">{{tx.toAddress}}</span>
</div> </div>
</div> </div>
<a class="item item-icon-right" ng-hide="!useSendMax && (insufficientFunds || noMatchingWallet)" ng-click="showWalletSelector()"> <a class="item item-icon-right" ng-hide="!wallets" ng-click="showWalletSelector()">
<span class="label" translate>From</span> <span class="label" translate>From</span>
<div class="wallet" ng-if="wallet"> <div class="wallet" ng-if="wallet">
<i class="icon big-icon-svg"> <i class="icon big-icon-svg">
@ -77,46 +77,39 @@
</div> </div>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
<div class="item item-icon-right" ng-if="!insufficientFunds && !noMatchingWallet" ng-click="chooseFeeLevel()"> <div class="item item-icon-right" ng-if="wallet" ng-click="chooseFeeLevel(tx, wallet)">
<span class="label">{{'Fee:' | translate}} {{feeLevel | translate}}</span> <span class="label">{{'Fee:' | translate}} {{tx.feeLevelName | translate}}</span>
<span class="m10l">{{fee || '...'}}</span> <span class="m10l">{{tx.txp[wallet.id].feeStr || '...'}}</span>
<span class="item-note m10l"> <span class="item-note m10l">
<span>{{feeFiat || '...'}}&nbsp;<span class="fee-rate" ng-if="feeRateStr" translate>- {{feeRateStr}} of the transaction</span></span> <span>{{tx.txp[wallet.id].alternativeFeeStr || '...'}}&nbsp;<span class="fee-rate" ng-if="tx.txp[wallet.id].feeRatePerStr" translate>- {{tx.txp[wallet.id].feeRatePerStr}} of the transaction</span></span>
</span> </span>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</div> </div>
<a class="item item-icon-right" ng-if="!insufficientFunds && !noMatchingWallet" ng-click="showDescriptionPopup()"> <a class="item item-icon-right" ng-if="wallet" ng-click="showDescriptionPopup(tx)">
<span class="label" translate>Add Memo</span> <span class="label" translate>Add Memo</span>
<span class="item-note m10l"> <span class="item-note m10l">
{{description}} {{tx.description}}
</span> </span>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
<div class="text-center" ng-show="noMatchingWallet"> <div class="text-center" ng-show="noWalletMessage">
<span class="badge badge-energized" translate>No wallets available</span> <span class="badge badge-energized">{{noWalletMessage}}</span>
</div>
<div class="text-center" ng-show="insufficientFunds">
<span class="badge badge-energized" translate>Insufficient funds</span>
</div> </div>
</div> </div>
</div> </div>
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-click="approve(statusChangeHandler)" ng-click="approve(tx, wallet, statusChangeHandler)"
ng-if="!isCordova" ng-if="!isCordova"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!wallet">
insufficient-funds="insufficientFunds"
no-matching-wallet="noMatchingWallet">
{{buttonText}} {{buttonText}}
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-if="isCordova && (wallet && !insufficientFunds && !noMatchingWallet)" ng-if="isCordova && wallet"
slide-on-confirm="onConfirm()" slide-on-confirm="approve(tx, wallet, statusChangeHandler)"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!wallet">
insufficient-funds="insufficientFunds"
no-matching-wallet="noMatchingWallet">
{{buttonText}} {{buttonText}}
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success
@ -132,7 +125,7 @@
wallet-selector-title="walletSelectorTitle" wallet-selector-title="walletSelectorTitle"
wallet-selector-wallets="wallets" wallet-selector-wallets="wallets"
wallet-selector-selected-wallet="wallet" wallet-selector-selected-wallet="wallet"
wallet-selector-show="showWallets" wallet-selector-show="walletSelector"
wallet-selector-on-select="onWalletSelect"> wallet-selector-on-select="onWalletSelect">
</wallet-selector> </wallet-selector>

View file

@ -1,4 +1,4 @@
<button ng-disabled="!hasWalletChosen || insufficientFunds || noMatchingWallet" class="click-to-accept__button button button-standard button-primary" ng-class="{disable: sendStatus}"> <button ng-disabled="isDisabled" class="click-to-accept__button button button-standard button-primary" ng-class="{disable: sendStatus}">
<span ng-if="!sendStatus"> <span ng-if="!sendStatus">
<ng-transclude></ng-transclude> <ng-transclude></ng-transclude>
</span> </span>

View file

@ -1,4 +1,4 @@
<ion-modal-view id="settings-fee" class="settings" ng-controller="preferencesFeeController" ng-init="init()"> <ion-modal-view id="settings-fee" class="settings" ng-controller="preferencesFeeController" >
<ion-header-bar align-title="center" class="bar-royal"> <ion-header-bar align-title="center" class="bar-royal">
<button class="button button-clear" ng-click="hideModal()"> <button class="button button-clear" ng-click="hideModal()">
Close Close
@ -7,7 +7,7 @@
{{'Bitcoin Network Fee Policy'|translate}} {{'Bitcoin Network Fee Policy'|translate}}
</div> </div>
</ion-header-bar> </ion-header-bar>
<ion-content> <ion-content ng-init="init(network)">
<div class="settings-explanation"> <div class="settings-explanation">
<div class="estimates"> <div class="estimates">
<div> <div>
@ -20,6 +20,7 @@
<span class="fee-rate" ng-if="feePerSatByte">{{feePerSatByte}} satoshis/byte</span> <span class="fee-rate" ng-if="feePerSatByte">{{feePerSatByte}} satoshis/byte</span>
<span ng-if="loadingFee">...</span> <span ng-if="loadingFee">...</span>
</div> </div>
<div ng-if="network!='livenet'">[{{network}}]</span>
</div> </div>
</div> </div>
<div class="fee-policies"> <div class="fee-policies">
@ -28,7 +29,10 @@
</ion-radio> </ion-radio>
</div> </div>
<div class="m20t"> <div class="m20t">
<button class="button button-standard button-primary" ng-click="chooseNewFee()" translate>Save</button> <button class="button button-standard button-primary" ng-click="chooseNewFee()" >
<span translate ng-if="!noSave">Save</span>
<span translate ng-if="noSave">OK</span>
</button>
</div> </div>
</ion-content> </ion-content>
</ion-modal-view> </ion-modal-view>

View file

@ -9,51 +9,51 @@
<div class="list"> <div class="list">
<div class="item head"> <div class="item head">
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{displayAmount || '...'}} <span class="unit">{{displayUnit}}</span></div> <div class="amount">{{tx.amountValueStr || '...'}} <span class="unit">{{tx.amountUnitStr}}</span></div>
<div class="alternative">{{alternativeAmountStr || '...'}}</div> <div class="alternative">{{tx.alternativeAmountStr || '...'}}</div>
</div> </div>
</div> </div>
<div class="info"> <div class="info">
<div class="item single-line" ng-if="paypro.domain"> <div class="item single-line" ng-if="tx.paypro.domain">
<span class="label">{{'Pay To'|translate}}</span> <span class="label">{{'Pay To'|translate}}</span>
<span class="item-note"> <span class="item-note">
{{paypro.domain}} {{tx.paypro.domain}}
</span> </span>
</div> </div>
<div class="item single-line" ng-if="paypro.toAddress"> <div class="item single-line" ng-if="tx.paypro.toAddress">
<span class="label">{{'Address'|translate}}</span> <span class="label">{{'Address'|translate}}</span>
<span class="item-note m10l ellipsis"> <span class="item-note m10l ellipsis">
{{paypro.toAddress}} {{tx.paypro.toAddress}}
</span> </span>
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{'Certified by'|translate}}</span> <span class="label">{{'Certified by'|translate}}</span>
<span class="item-note w100p"> <span class="item-note w100p">
<span ng-show="paypro.caTrusted"> <span ng-show="tx.paypro.caTrusted">
<i class="ion-locked" style="color:green"></i> <i class="ion-locked" style="color:green"></i>
{{paypro.caName}} {{'(Trusted)' | translate}}</span> {{tx.paypro.caName}} {{'(Trusted)' | translate}}</span>
</span> </span>
<span ng-show="!paypro.caTrusted"> <span ng-show="!tx.paypro.caTrusted">
<span ng-show="paypro.selfSigned"> <span ng-show="tx.paypro.selfSigned">
<i class="ion-unlocked" style="color:red"></i> <span translate>Self-signed Certificate</span> <i class="ion-unlocked" style="color:red"></i> <span translate>Self-signed Certificate</span>
</span> </span>
<span ng-show="!paypro.selfSigned"> <span ng-show="!tx.paypro.selfSigned">
<i class="ion-locked" style="color:yellow"></i>{{paypro.caName}}<br> <i class="ion-locked" style="color:yellow"></i>{{tx.paypro.caName}}<br>
<span translate>WARNING: UNTRUSTED CERTIFICATE</span> <span translate>WARNING: UNTRUSTED CERTIFICATE</span>
</span> </span>
</span> </span>
</span> </span>
</div> </div>
<div class="item" ng-if="paypro.memo"> <div class="item" ng-if="tx.paypro.memo">
<span class="label">{{'Memo'|translate}}</span> <span class="label">{{'Memo'|translate}}</span>
<span class="item-note w100p"> <span class="item-note w100p">
{{paypro.memo}} {{tx.paypro.memo}}
</span> </span>
</div> </div>
<div class="item single-line" ng-if="paypro.expires"> <div class="item single-line" ng-if="tx.paypro.expires">
<span class="label">{{'Expires'|translate}}</span> <span class="label">{{'Expires'|translate}}</span>
<span class="item-note"> <span class="item-note">
{{paypro.expires * 1000 | amTimeAgo }} {{tx.paypro.expires * 1000 | amTimeAgo }}
</span> </span>
</div> </div>
</div> </div>

View file

@ -17,11 +17,11 @@
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{amountUnitStr}}</div> <div class="amount">{{amountUnitStr}}</div>
<div class="alternative" ng-if="sellPrice"> <div class="alternative" ng-if="sellPrice">
<span ng-show="isFiat">{{sellRequestInfo.amount.amount}} {{sellRequestInfo.amount.currency}}</span> <span ng-show="isFiat">{{sellRequestInfo.amount.amount}} {{sellRequestInfo.amount.currency}}</span>
@ ${{sellPrice.amount}} per BTC @ ${{sellPrice.amount}} per BTC
</div> </div>
</div> </div>
</div> </div>
<div class="info"> <div class="info">
@ -62,16 +62,16 @@
will be sent to your Coinbase account, and sold when Coinbase accepts the transaction (usually one will be sent to your Coinbase account, and sold when Coinbase accepts the transaction (usually one
hour). hour).
</div> </div>
<div class="label" ng-if="sellRequestInfo">Estimated sale value: <div class="label" ng-if="sellRequestInfo">Estimated sale value:
<strong> <strong>
{{sellRequestInfo.total.amount | currency : '' : 2}} {{sellRequestInfo.total.amount | currency : '' : 2}}
{{sellRequestInfo.total.currency}} {{sellRequestInfo.total.currency}}
</strong> </strong>
</div> </div>
<div class="label" ng-if="sellRequestInfo">Still sell if price fall until: <div class="label" ng-if="sellRequestInfo">Still sell if price fall until:
<strong> <strong>
{{(sellRequestInfo.total.amount - {{(sellRequestInfo.total.amount -
(selectedPriceSensitivity.data.value / 100) * sellRequestInfo.total.amount) | currency : '' : 2}} (selectedPriceSensitivity.data.value / 100) * sellRequestInfo.total.amount) | currency : '' : 2}}
{{sellRequestInfo.total.currency}} {{sellRequestInfo.total.currency}}
</strong> </strong>
</div> </div>
@ -107,23 +107,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet"
ng-click="sellConfirm()" ng-click="sellConfirm()"
ng-if="!isCordova && sellRequestInfo" ng-if="!isCordova && sellRequestInfo"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet">
insufficient-funds="!selectedPaymentMethodId.value"
no-matching-wallet="!sellRequestInfo">
Confirm sale Confirm sale
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet"
ng-if="isCordova && sellRequestInfo" ng-if="isCordova && sellRequestInfo"
slide-on-confirm="sellConfirm()" slide-on-confirm="sellConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet">
insufficient-funds="!selectedPaymentMethodId.value"
no-matching-wallet="!sellRequestInfo">
Slide to sell Slide to sell
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success

View file

@ -17,12 +17,12 @@
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{amountUnitStr}}</div> <div class="amount">{{amountUnitStr}}</div>
<div class="alternative"> <div class="alternative">
<span ng-show="!isFiat">{{sellInfo.subtotal}} {{sellInfo.currency}}</span> <span ng-show="!isFiat">{{sellInfo.subtotal}} {{sellInfo.currency}}</span>
<span ng-show="isFiat">{{sellInfo.qty}} BTC</span> <span ng-show="isFiat">{{sellInfo.qty}} BTC</span>
@ ${{sellInfo.price}} per BTC @ ${{sellInfo.price}} per BTC
</div> </div>
</div> </div>
</div> </div>
<div class="info"> <div class="info">
@ -64,23 +64,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!sellInfo || !wallet"
ng-click="sellConfirm()" ng-click="sellConfirm()"
ng-if="!isCordova && sellInfo" ng-if="!isCordova && sellInfo"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!sellInfo || !wallet">
insufficient-funds="false"
no-matching-wallet="!sellInfo">
Confirm sale Confirm sale
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!sellInfo || !wallet"
ng-if="isCordova && sellInfo" ng-if="isCordova && sellInfo"
slide-on-confirm="sellConfirm()" slide-on-confirm="sellConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!sellInfo || !wallet">
insufficient-funds="false"
no-matching-wallet="!sellInfo">
Slide to sell Slide to sell
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success

View file

@ -84,23 +84,17 @@
</ion-content> </ion-content>
<click-to-accept <click-to-accept
ng-disabled="!cardInfo || !wallet"
ng-click="topUpConfirm()" ng-click="topUpConfirm()"
ng-if="!isCordova && cardInfo" ng-if="!isCordova && cardInfo"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!cardInfo || !wallet">
insufficient-funds="insufficientFunds"
no-matching-wallet="!cardInfo">
Add funds Add funds
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-disabled="!cardInfo || !wallet"
ng-if="isCordova && cardInfo" ng-if="isCordova && cardInfo"
slide-on-confirm="topUpConfirm()" slide-on-confirm="topUpConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="wallet" is-disabled="!cardInfo || !wallet">
insufficient-funds="insufficientFunds"
no-matching-wallet="!cardInfo">
Slide to confirm Slide to confirm
</slide-to-accept> </slide-to-accept>
<slide-to-accept-success <slide-to-accept-success