This commit is contained in:
magmahindenburg 2017-07-06 11:27:29 +09:00
commit 98c93e3c6f
133 changed files with 5157 additions and 3511 deletions

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('addressesController', function($scope, $log, $stateParams, $state, $timeout, $ionicHistory, $ionicScrollDelegate, configService, popupService, gettextCatalog, ongoingProcess, lodash, profileService, walletService, bwcError, platformInfo, appConfigService) {
angular.module('copayApp.controllers').controller('addressesController', function($scope, $log, $stateParams, $state, $timeout, $ionicHistory, $ionicScrollDelegate, configService, popupService, gettextCatalog, ongoingProcess, lodash, profileService, walletService, bwcError, platformInfo, appConfigService, txFormatService, feeService) {
var UNUSED_ADDRESS_LIMIT = 5;
var BALANCE_ADDRESS_LIMIT = 5;
var config = configService.getSync().wallet.settings;
@ -55,7 +55,7 @@ angular.module('copayApp.controllers').controller('addressesController', functio
$scope.latestWithBalance = lodash.slice(withBalance, 0, BALANCE_ADDRESS_LIMIT);
lodash.each(withBalance, function(a) {
a.balanceStr = (a.amount * satToUnit).toFixed(unitDecimals) + ' ' + unitName;
a.balanceStr = txFormatService.formatAmount(a.amount);
});
$scope.viewAll = {
@ -72,6 +72,31 @@ angular.module('copayApp.controllers').controller('addressesController', functio
});
});
});
feeService.getFeeLevels(function(err, levels){
walletService.getLowUtxos($scope.wallet, levels, function(err, resp) {
if (err) return;
if (resp.allUtxos && resp.allUtxos.length) {
var allSum = lodash.sum(resp.allUtxos || 0, 'satoshis');
var per = (resp.minFee / allSum) * 100;
$scope.lowWarning = resp.warning;
$scope.lowUtxosNb = resp.lowUtxos.length;
$scope.allUtxosNb = resp.allUtxos.length;
$scope.lowUtxosSum = txFormatService.formatAmountStr(lodash.sum(resp.lowUtxos || 0, 'satoshis'));
$scope.allUtxosSum = txFormatService.formatAmountStr(allSum);
$scope.minFee = txFormatService.formatAmountStr(resp.minFee || 0);
$scope.minFeePer = per.toFixed(2) + '%';
}
});
});
};
function processPaths(list) {
@ -116,7 +141,14 @@ angular.module('copayApp.controllers').controller('addressesController', functio
};
$scope.viewAllAddresses = function() {
$state.go('tabs.settings.allAddresses', {
var fromView = $ionicHistory.currentStateName();
var path;
if (fromView.indexOf('settings') !== -1) {
path = 'tabs.settings.allAddresses';
} else {
path = 'tabs.wallet.allAddresses';
}
$state.go(path, {
walletId: $scope.wallet.id
});
};

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('advancedSettingsController', function($scope, $log, configService) {
angular.module('copayApp.controllers').controller('advancedSettingsController', function($scope, $log, configService, platformInfo) {
var updateConfig = function() {
var config = configService.getSync();
@ -50,6 +50,7 @@ angular.module('copayApp.controllers').controller('advancedSettingsController',
};
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
updateConfig();
});

View file

@ -44,66 +44,77 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$scope.isFiat = data.stateParams.currency != 'bits' && data.stateParams.currency != 'BTC' ? true : false;
var parsedAmount = txFormatService.parseAmount(
data.stateParams.amount,
data.stateParams.amount,
data.stateParams.currency);
amount = parsedAmount.amount;
currency = parsedAmount.currency;
// Buy always in BTC
amount = (parsedAmount.amountSat / 100000000).toFixed(8);
currency = 'BTC';
$scope.amountUnitStr = parsedAmount.amountUnitStr;
$scope.network = coinbaseService.getNetwork();
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network
});
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack('No wallets available');
return;
}
$scope.wallet = $scope.wallets[0]; // Default first wallet
ongoingProcess.set('connectingCoinbase', true);
coinbaseService.init(function(err, res) {
ongoingProcess.set('calculatingFee', true);
coinbaseService.checkEnoughFundsForFee(amount, function(err) {
ongoingProcess.set('calculatingFee', false);
if (err) {
ongoingProcess.set('connectingCoinbase', false);
showErrorAndBack(err);
return;
}
var accessToken = res.accessToken;
coinbaseService.buyPrice(accessToken, coinbaseService.getAvailableCurrency(), function(err, b) {
$scope.buyPrice = b.data || null;
$scope.network = coinbaseService.getNetwork();
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network
});
$scope.paymentMethods = [];
$scope.selectedPaymentMethodId = { value : null };
coinbaseService.getPaymentMethods(accessToken, function(err, p) {
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack('No wallets available');
return;
}
$scope.wallet = $scope.wallets[0]; // Default first wallet
ongoingProcess.set('connectingCoinbase', true);
coinbaseService.init(function(err, res) {
if (err) {
ongoingProcess.set('connectingCoinbase', false);
showErrorAndBack(err);
return;
}
var accessToken = res.accessToken;
var hasPrimary;
var pm;
for(var i = 0; i < p.data.length; i++) {
pm = p.data[i];
if (pm.allow_buy) {
$scope.paymentMethods.push(pm);
coinbaseService.buyPrice(accessToken, coinbaseService.getAvailableCurrency(), function(err, b) {
$scope.buyPrice = b.data || null;
});
$scope.paymentMethods = [];
$scope.selectedPaymentMethodId = { value : null };
coinbaseService.getPaymentMethods(accessToken, function(err, p) {
if (err) {
ongoingProcess.set('connectingCoinbase', false);
showErrorAndBack(err);
return;
}
if (pm.allow_buy && pm.primary_buy) {
hasPrimary = true;
$scope.selectedPaymentMethodId.value = pm.id;
var hasPrimary;
var pm;
for(var i = 0; i < p.data.length; i++) {
pm = p.data[i];
if (pm.allow_buy) {
$scope.paymentMethods.push(pm);
}
if (pm.allow_buy && pm.primary_buy) {
hasPrimary = true;
$scope.selectedPaymentMethodId.value = pm.id;
}
}
}
if (lodash.isEmpty($scope.paymentMethods)) {
ongoingProcess.set('connectingCoinbase', false);
showErrorAndBack('No payment method available to buy');
return;
}
if (!hasPrimary) $scope.selectedPaymentMethodId.value = $scope.paymentMethods[0].id;
$scope.buyRequest();
if (lodash.isEmpty($scope.paymentMethods)) {
ongoingProcess.set('connectingCoinbase', false);
showErrorAndBack('No payment method available to buy');
return;
}
if (!hasPrimary) $scope.selectedPaymentMethodId.value = $scope.paymentMethods[0].id;
$scope.buyRequest();
});
});
});
});
@ -139,12 +150,12 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
};
$scope.buyConfirm = function() {
var message = 'Buy bitcoin for ' + amount + ' ' + currency;
var message = 'Buy bitcoin for ' + $scope.amountUnitStr;
var okText = 'Confirm';
var cancelText = 'Cancel';
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) return;
ongoingProcess.set('buyingBitcoin', true, statusChangeHandler);
coinbaseService.init(function(err, res) {
if (err) {

View file

@ -1,14 +1,38 @@
'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) {
var cachedTxp = {};
var feeLevel;
var feePerKb;
var toAmount;
var isChromeApp = platformInfo.isChromeApp;
var countDown = null;
var cachedSendMax = {};
$scope.isCordova = platformInfo.isCordova;
var CONFIRM_LIMIT_USD = 20;
var FEE_TOO_HIGH_LIMIT_PER = 15;
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;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
function refresh() {
$timeout(function() {
$scope.$apply();
}, 1);
}
$scope.showWalletSelector = function() {
$scope.walletSelector = true;
refresh();
};
$scope.$on("$ionicView.beforeLeave", function(event, data) {
$ionicConfig.views.swipeBackEnabled(true);
@ -18,305 +42,346 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$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) {
toAmount = data.stateParams.toAmount;
cachedSendMax = {};
function setWalletSelector(network, minAmount, cb) {
// 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.isWindowsPhoneApp = isWindowsPhoneApp;
$scope.showAddress = false;
$scope.useSendMax = data.stateParams.useSendMax == 'true' ? true : false;
$scope.recipientType = data.stateParams.recipientType || null;
$scope.toAddress = data.stateParams.toAddress;
$scope.toName = data.stateParams.toName;
$scope.toEmail = data.stateParams.toEmail;
$scope.toColor = data.stateParams.toColor;
$scope.description = data.stateParams.description;
$scope.paypro = data.stateParams.paypro;
$scope.insufficientFunds = false;
$scope.noMatchingWallet = false;
$scope.paymentExpired = {
value: false
};
$scope.remainingTimeStr = {
value: null
};
$scope.network = (new bitcore.Address($scope.toAddress)).network.name;
setFee();
resetValues();
setwallets();
applyButtonText();
updateTx(tx, null, {}, function() {
$scope.walletSelectorTitle = gettextCatalog.getString('Send from');
setWalletSelector(tx.network, tx.toAmount, function(err) {
if (err) {
return exitWithError('Could not update wallets');
}
if ($scope.wallets.length > 1) {
$scope.showWalletSelector();
} else if ($scope.wallets.length) {
setWallet($scope.wallets[0], tx);
}
});
});
});
function setFee(customFeeLevel, cb) {
feeService.getCurrentFeeValue($scope.network, customFeeLevel, function(err, currentFeePerKb) {
var config = configService.getSync().wallet;
var configFeeLevel = (config.settings && config.settings.feeLevel) ? config.settings.feeLevel : 'normal';
feePerKb = currentFeePerKb;
feeLevel = customFeeLevel ? customFeeLevel : configFeeLevel;
$scope.feeLevel = feeService.feeOpts[feeLevel];
if (cb) return cb();
function getSendMaxInfo(tx, wallet, cb) {
if (!tx.sendMax) return cb();
//ongoingProcess.set('retrievingInputs', true);
walletService.getSendMaxInfo(wallet, {
feePerKb: tx.feeRate,
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();
// End of quick refresh, before wallet is selected.
if (!wallet)return cb();
feeService.getFeeRate(tx.network, tx.feeLevel, function(err, feeRate) {
if (err) return cb(err);
tx.feeRate = feeRate;
tx.feeLevelName = feeService.feeOpts[tx.feeLevel];
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);
}
// txp already generated for this wallet?
if (tx.txp[wallet.id]) {
refresh();
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;
});
var per = (txp.fee / (txp.amount + txp.fee) * 100);
txp.feeRatePerStr = per.toFixed(2) + '%';
txp.feeToHigh = per > FEE_TOO_HIGH_LIMIT_PER;
tx.txp[wallet.id] = txp;
$log.debug('Confirm. TX Fully Updated for wallet:' + wallet.id, tx);
refresh();
return cb();
});
});
});
}
function useSelectedWallet() {
if (!$scope.useSendMax) displayValues();
if (!$scope.useSendMax) {
showAmount(tx.toAmount);
}
$scope.onWalletSelect($scope.wallet);
}
function applyButtonText(multisig) {
$scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' ';
function setButtonText(isMultisig, isPayPro) {
$scope.buttonText = gettextCatalog.getString(isCordova && !isWindowsPhoneApp ? 'Slide' : 'Click') + ' ';
if ($scope.paypro) {
if (isPayPro) {
$scope.buttonText += gettextCatalog.getString('to pay');
} else if (multisig) {
} else if (isMultisig) {
$scope.buttonText += gettextCatalog.getString('to accept');
} else
$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.showAddress = !$scope.showAddress;
};
var initConfirm = function() {
if ($scope.paypro) _paymentTimeControl($scope.paypro.expires);
displayValues();
if ($scope.wallets.length > 1) $scope.showWalletSelector();
else setWallet($scope.wallets[0]);
$timeout(function() {
$scope.$apply();
});
};
function showSendMaxWarning(sendMaxInfo) {
function displayValues() {
toAmount = parseInt(toAmount);
$scope.amountStr = txFormatService.formatAmountStr(toAmount);
$scope.displayAmount = getDisplayAmount($scope.amountStr);
$scope.displayUnit = getDisplayUnit($scope.amountStr);
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;
function verifyExcludedUtxos() {
var warningMsg = [];
if (sendMaxInfo.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(sendMaxInfo.amountBelowFee)
}));
}
if (resp.amount == 0) {
$scope.insufficientFunds = true;
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee'));
return;
if (sendMaxInfo.utxosAboveMaxSize > 0) {
warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", {
amountAboveMaxSizeStr: txFormatService.formatAmountStr(sendMaxInfo.amountAboveMaxSize)
}));
}
return warningMsg.join('\n');
};
$scope.sendMaxInfo = {
sendMax: true,
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 msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", {
fee: txFormatService.formatAmountStr(sendMaxInfo.fee)
});
};
var warningMsg = verifyExcludedUtxos();
function setSendMaxValues(data) {
resetValues();
var config = configService.getSync().wallet;
var unitToSatoshi = config.settings.unitToSatoshi;
var satToUnit = 1 / unitToSatoshi;
var unitDecimals = config.settings.unitDecimals;
if (!lodash.isEmpty(warningMsg))
msg += '\n' + warningMsg;
$scope.amountStr = txFormatService.formatAmountStr(data.amount, true);
$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;
popupService.showAlert(null, msg, function() {});
};
$scope.onWalletSelect = function(wallet) {
if ($scope.useSendMax) {
$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);
setWallet(wallet, tx);
};
$scope.showDescriptionPopup = function() {
$scope.showDescriptionPopup = function(tx) {
var message = gettextCatalog.getString('Add description');
var opts = {
defaultText: $scope.description
defaultText: tx.description
};
popupService.showPrompt(null, message, opts, function(res) {
if (typeof res != 'undefined') $scope.description = res;
if (typeof res != 'undefined') tx.description = res;
$timeout(function() {
$scope.$apply();
});
});
};
function getDisplayAmount(amountStr) {
return $scope.amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return $scope.amountStr.split(' ')[1];
};
function _paymentTimeControl(expirationTime) {
$scope.paymentExpired.value = false;
$scope.paymentExpired = false;
setExpirationTime();
countDown = $interval(function() {
@ -334,12 +399,12 @@ angular.module('copayApp.controllers').controller('confirmController', function(
var totalSecs = expirationTime - now;
var m = Math.floor(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() {
$scope.paymentExpired.value = true;
$scope.remainingTimeStr.value = gettextCatalog.getString('Expired');
$scope.paymentExpired = true;
$scope.remainingTimeStr = gettextCatalog.getString('Expired');
if (countDown) $interval.cancel(countDown);
$timeout(function() {
$scope.$apply();
@ -347,31 +412,27 @@ angular.module('copayApp.controllers').controller('confirmController', function(
};
};
function setWallet(wallet, delayed) {
var stop;
/* sets a wallet on the UI, creates a TXPs for that wallet */
function setWallet(wallet, tx) {
$scope.wallet = wallet;
$scope.fee = $scope.txp = null;
if (stop) {
$timeout.cancel(stop);
stop = null;
}
if (cachedTxp[wallet.id]) {
apply(cachedTxp[wallet.id]);
} else {
stop = $timeout(function() {
createTx(wallet, true, function(err, txp) {
if (err) return;
cachedTxp[wallet.id] = txp;
apply(txp);
});
}, delayed ? 2000 : 1);
}
setButtonText(wallet.credentials.m > 1, !!tx.paypro);
if (tx.paypro)
_paymentTimeControl(tx.paypro.expires);
updateTx(tx, wallet, {
dryRun: true
}, function(err) {
$timeout(function() {
$ionicScrollDelegate.resize();
$scope.$apply();
}, 10);
});
$timeout(function() {
$ionicScrollDelegate.resize();
$scope.$apply();
}, 10);
};
var setSendError = function(msg) {
@ -382,75 +443,6 @@ angular.module('copayApp.controllers').controller('confirmController', function(
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() {
$ionicModal.fromTemplateUrl('views/modals/paypro.html', {
scope: $scope
@ -464,14 +456,11 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$scope.payproModal.hide();
};
$scope.approve = function(onSendStatusChange) {
$scope.approve = function(tx, wallet, onSendStatusChange) {
var wallet = $scope.wallet;
if (!wallet) {
return;
}
if (!tx || !wallet) return;
if ($scope.paypro && $scope.paymentExpired.value) {
if ($scope.paymentExpired) {
popupService.showAlert(null, gettextCatalog.getString('This bitcoin payment request has expired.'));
$scope.sendStatus = '';
$timeout(function() {
@ -481,46 +470,54 @@ angular.module('copayApp.controllers').controller('confirmController', function(
}
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);
if (err) return;
var config = configService.getSync();
var spendingPassEnabled = walletService.isEncrypted(wallet);
var touchIdEnabled = config.touchIdFor && config.touchIdFor[wallet.id];
var isCordova = $scope.isCordova;
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');
// confirm txs for more that 20usd, if not spending/touchid is enabled
function confirmTx(cb) {
if (walletService.isEncrypted(wallet))
return cb();
if (!spendingPassEnabled && !touchIdEnabled) {
if (isCordova) {
if (bigAmount) {
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) {
$scope.sendStatus = '';
$timeout(function() {
$scope.$apply();
});
return;
}
publishAndSign(wallet, txp, onSendStatusChange);
});
} else publishAndSign(wallet, txp, onSendStatusChange);
} else {
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) {
$scope.sendStatus = '';
return;
}
publishAndSign(wallet, txp, onSendStatusChange);
});
var amountUsd = parseFloat(txFormatService.formatToUSD(txp.amount));
if (amountUsd <= CONFIRM_LIMIT_USD)
return cb();
var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', {
amountStr: tx.amountStr,
name: wallet.name
});
var okText = gettextCatalog.getString('Confirm');
var cancelText = gettextCatalog.getString('Cancel');
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
return cb(!ok);
});
};
function publishAndSign() {
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
$log.info('No signing proposal: No private key');
return walletService.onlyPublish(wallet, txp, function(err) {
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 +540,47 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$scope.statusChangeHandler = statusChangeHandler;
$scope.onConfirm = function() {
$scope.approve(statusChangeHandler);
};
$scope.onSuccessConfirm = function() {
var previousView = $ionicHistory.viewHistory().backView && $ionicHistory.viewHistory().backView.stateName;
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$ionicHistory.removeBackView();
$scope.sendStatus = '';
$ionicHistory.nextViewOptions({
disableAnimate: true,
historyRoot: true
});
$ionicHistory.clearHistory();
$state.go('tabs.send').then(function() {
$ionicHistory.clearHistory();
$state.transitionTo('tabs.home');
});
};
function publishAndSign(wallet, txp, onSendStatusChange) {
$scope.chooseFeeLevel = function(tx, wallet) {
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
$log.info('No signing proposal: No private key');
var scope = $rootScope.$new(true);
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', {
scope: $scope,
scope: scope,
}).then(function(modal) {
$scope.chooseFeeLevelModal = modal;
$scope.openModal();
scope.chooseFeeLevelModal = modal;
scope.openModal();
});
$scope.openModal = function() {
$scope.chooseFeeLevelModal.show();
scope.openModal = function() {
scope.chooseFeeLevelModal.show();
};
$scope.hideModal = function(customFeeLevel) {
if (customFeeLevel) {
cachedTxp = {};
cachedSendMax = {};
ongoingProcess.set('gettingFeeLevels', true);
setFee(customFeeLevel, function() {
ongoingProcess.set('gettingFeeLevels', false);
resetValues();
if ($scope.wallet) useSelectedWallet();
})
}
$scope.chooseFeeLevelModal.hide();
scope.hideModal = function(customFeeLevel) {
scope.chooseFeeLevelModal.hide();
$log.debug('Custom fee level choosen:' + customFeeLevel + ' was:' + tx.feeLevel);
if (tx.feeLevel == customFeeLevel)
return;
tx.feeLevel = customFeeLevel;
updateTx(tx, wallet, {
clearCache: true,
dryRun: true,
}, function() {
});
};
};

View file

@ -5,7 +5,7 @@ angular.module('copayApp.controllers').controller('rateAppController', function(
$scope.appName = appConfigService.nameCase;
var isAndroid = platformInfo.isAndroid;
var isIOS = platformInfo.isIOS;
var isWP = platformInfo.isWP;
var config = configService.getSync();
$scope.skip = function() {
@ -42,8 +42,6 @@ angular.module('copayApp.controllers').controller('rateAppController', function(
url = $scope.appName == 'Copay' ? defaults.rateApp.copay.android : defaults.rateApp.bitpay.android;
if (isIOS)
url = $scope.appName == 'Copay' ? defaults.rateApp.copay.ios : defaults.rateApp.bitpay.ios;
// if (isWP)
// url = $scope.appName == 'Copay' ? defaults.rateApp.copay.windows : defaults.rateApp.bitpay.windows;
externalLinkService.open(url);
$state.go('tabs.rate.complete', {

View file

@ -50,6 +50,7 @@ angular.module('copayApp.controllers').controller('joinController',
$scope.onQrCodeScannedJoin = function(data) {
$scope.formData.secret = data;
$scope.$apply();
};
if ($stateParams.url) {
@ -136,7 +137,7 @@ angular.module('copayApp.controllers').controller('joinController',
}
if ($scope.formData.seedSource.id == walletService.externalSource.ledger.id || $scope.formData.seedSource.id == walletService.externalSource.trezor.id || $scope.formData.seedSource.id == walletService.externalSource.intelTEE.id) {
var account = $scope.account;
var account = $scope.formData.account;
if (!account || account < 1) {
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid account number'));
return;

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('txpDetailsController', function($scope, $rootScope, $timeout, $interval, $log, ongoingProcess, platformInfo, $ionicScrollDelegate, txFormatService, bwcError, gettextCatalog, lodash, walletService, popupService, $ionicHistory) {
angular.module('copayApp.controllers').controller('txpDetailsController', function($scope, $rootScope, $timeout, $interval, $log, ongoingProcess, platformInfo, $ionicScrollDelegate, txFormatService, bwcError, gettextCatalog, lodash, walletService, popupService, $ionicHistory, feeService) {
var isGlidera = $scope.isGlidera;
var GLIDERA_LOCK_TIME = 6 * 60 * 60;
var now = Math.floor(Date.now() / 1000);
@ -9,37 +9,41 @@ angular.module('copayApp.controllers').controller('txpDetailsController', functi
$scope.init = function() {
$scope.loading = null;
$scope.isCordova = platformInfo.isCordova;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.copayers = $scope.wallet.status.wallet.copayers;
$scope.copayerId = $scope.wallet.credentials.copayerId;
$scope.isShared = $scope.wallet.credentials.n > 1;
$scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal();
$scope.color = $scope.wallet.color;
$scope.data = {};
$scope.displayAmount = getDisplayAmount($scope.tx.amountStr);
$scope.displayUnit = getDisplayUnit($scope.tx.amountStr);
displayFeeValues();
initActionList();
checkPaypro();
applyButtonText();
};
function displayFeeValues() {
txFormatService.formatAlternativeStr($scope.tx.fee, function(v) {
$scope.tx.feeFiatStr = v;
});
$scope.tx.feeRateStr = ($scope.tx.fee / ($scope.tx.amount + $scope.tx.fee) * 100).toFixed(2) + '%';
$scope.tx.feeLevelStr = feeService.feeOpts[$scope.tx.feeLevel];
};
function applyButtonText() {
$scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' ';
$scope.buttonText = $scope.isCordova && !$scope.isWindowsPhoneApp ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' ';
var lastSigner = lodash.filter($scope.tx.actions, {
type: 'accept'
}).length == $scope.tx.requiredSignatures - 1;
if (lastSigner)
if (lastSigner) {
$scope.buttonText += gettextCatalog.getString('to send');
else
$scope.successText = gettextCatalog.getString('Payment Sent');
} else {
$scope.buttonText += gettextCatalog.getString('to accept');
};
function getDisplayAmount(amountStr) {
return amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return amountStr.split(' ')[1];
$scope.successText = gettextCatalog.getString('Payment Accepted');
}
};
function initActionList() {

View file

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

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.controllers').controller('preferencesController',
function($scope, $rootScope, $timeout, $log, $ionicHistory, configService, profileService, fingerprintService, walletService) {
function($scope, $rootScope, $timeout, $log, $ionicHistory, configService, profileService, fingerprintService, walletService, platformInfo) {
var wallet;
var walletId;
@ -76,7 +76,7 @@ angular.module('copayApp.controllers').controller('preferencesController',
wallet = profileService.getWallet(data.stateParams.walletId);
walletId = wallet.credentials.walletId;
$scope.wallet = wallet;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.externalSource = null;
if (!wallet)

View file

@ -2,13 +2,14 @@
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.currentFeeLevel = newFee;
updateCurrentValues();
$scope.save = function(newFee) {
$scope.currentFeeLevel = newFee;
updateCurrentValues();
if ($scope.noSave)
return;
}
var opts = {
wallet: {
@ -20,8 +21,6 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
configService.set(opts, function(err) {
if (err) $log.debug(err);
$scope.currentFeeLevel = newFee;
updateCurrentValues();
$timeout(function() {
$scope.$apply();
});
@ -33,8 +32,10 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
});
$scope.init = function() {
$scope.network = $scope.network || 'livenet';
$scope.feeOpts = feeService.feeOpts;
$scope.currentFeeLevel = $scope.customFeeLevel ? $scope.customFeeLevel : feeService.getCurrentFeeLevel();
$scope.currentFeeLevel = $scope.feeLevel || feeService.getCurrentFeeLevel();
$scope.loadingFee = true;
feeService.getFeeLevels(function(err, levels) {
$scope.loadingFee = false;
@ -45,22 +46,27 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu
}
$scope.feeLevels = levels;
updateCurrentValues();
$scope.$apply();
$timeout(function() {
$scope.$apply();
});
});
};
var updateCurrentValues = function() {
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
});
if (lodash.isEmpty(feeLevelValue)) {
if (lodash.isEmpty(value)) {
$scope.feePerSatByte = null;
$scope.avgConfirmationTime = null;
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() {

View file

@ -12,6 +12,7 @@ angular.module('copayApp.controllers').controller('tabHomeController',
$scope.homeTip = $stateParams.fromOnboarding;
$scope.isCordova = platformInfo.isCordova;
$scope.isAndroid = platformInfo.isAndroid;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.isNW = platformInfo.isNW;
$scope.showRateCard = {};
@ -42,6 +43,11 @@ angular.module('copayApp.controllers').controller('tabHomeController',
}
storageService.getFeedbackInfo(function(error, info) {
if ($scope.isWindowsPhoneApp) {
$scope.showRateCard.value = false;
return;
}
if (!info) {
initFeedBackInfo();
} else {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tabSendController', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, bwcError, gettextCatalog) {
angular.module('copayApp.controllers').controller('tabSendController', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, bwcError, gettextCatalog, scannerService) {
var originalList;
var CONTACTS_SHOW_LIMIT;
@ -120,7 +120,20 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
};
$scope.openScanner = function() {
$state.go('tabs.scan');
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
$state.go('tabs.scan');
return;
}
scannerService.useOldScanner(function(err, contents) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
incomingData.redir(contents);
});
};
$scope.showMore = function() {

View file

@ -51,6 +51,7 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$scope.isCordova = platformInfo.isCordova;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.isDevel = platformInfo.isDevel;
$scope.appName = appConfigService.nameCase;
configService.whenAvailable(function(config) {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, incomingData, lodash, popupService, gettextCatalog) {
angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingData, lodash, popupService, gettextCatalog, scannerService) {
$scope.onScan = function(data) {
if (!incomingData.redir(data)) {
@ -22,8 +22,23 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
}, 1);
};
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$rootScope.hideTabs = '';
});
$scope.chooseScanner = function() {
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
$state.go('tabs.scan');
return;
}
scannerService.useOldScanner(function(err, contents) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
incomingData.redir(contents);
});
};
});

View file

@ -1,24 +1,18 @@
'use strict';
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) {
var amount;
var currency;
var cardId;
var sendMax;
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) {
$scope.isCordova = platformInfo.isCordova;
$scope.$on("$ionicView.beforeLeave", function(event, data) {
$ionicConfig.views.swipeBackEnabled(true);
});
$scope.$on("$ionicView.enter", function(event, data) {
$ionicConfig.views.swipeBackEnabled(false);
});
var cardId;
var useSendMax;
var amount;
var currency;
var createdTx;
var message;
var configWallet = configService.getSync().wallet;
var showErrorAndBack = function(title, msg) {
title = title || 'Error';
title = title || gettextCatalog.getString('Error');
$scope.sendStatus = '';
$log.error(msg);
msg = msg.errors ? msg.errors[0].message : msg;
@ -27,17 +21,24 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
});
};
var showError = function(title, msg) {
title = title || 'Error';
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) {
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
var err = 'No signing proposal: No private key';
var err = gettextCatalog.getString('No signing proposal: No private key');
$log.info(err);
return cb(err);
}
@ -50,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 == 'topup' && !isOn) {
if (processName == 'topup' && !isOn) {
$scope.sendStatus = 'success';
$timeout(function() {
$scope.$digest();
@ -60,137 +61,237 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
}
};
$scope.$on("$ionicView.beforeEnter", function(event, data) {
cardId = data.stateParams.id;
sendMax = data.stateParams.useSendMax;
var setTotalAmount = function(amountSat, invoiceFeeSat, networkFeeSat) {
satToFiat(amountSat, function(a) {
$scope.amount = Number(a);
if (!cardId) {
showErrorAndBack(null, 'No card selected');
return;
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) {
return cb({
title: gettextCatalog.getString('Could not create the invoice'),
message: err
});
}
bitpayCardService.getInvoice(invoiceId, function(err, inv) {
if (err) {
return cb({
title: gettextCatalog.getString('Could not get the invoice'),
message: err
});
}
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 parsedAmount = txFormatService.parseAmount(
data.stateParams.amount,
data.stateParams.currency);
amount = parsedAmount.amount;
currency = parsedAmount.currency;
$scope.amountUnitStr = parsedAmount.amountUnitStr;
var outputs = [];
var toAddress = invoice.bitcoinAddress;
var amountSat = parseInt(invoice.btcDue * 100000000); // BTC to Satoshi
$scope.network = bitpayService.getEnvironment().network;
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network,
hasFunds: true,
minAmount: parsedAmount.amountSat
outputs.push({
'toAddress': toAddress,
'amount': amountSat,
'message': message
});
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack(null, 'Insufficient funds');
return;
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);
}
$scope.onWalletSelect($scope.wallets[0]); // Default first wallet
};
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);
});
$scope.$on("$ionicView.enter", function(event, data) {
$ionicConfig.views.swipeBackEnabled(false);
});
$scope.$on("$ionicView.beforeEnter", function(event, data) {
cardId = data.stateParams.id;
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
});
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack(null, gettextCatalog.getString('No wallets available'));
return;
}
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() {
var config = configService.getSync();
var configWallet = config.wallet;
var walletSettings = configWallet.settings;
if (!createdTx) {
showError(null, gettextCatalog.getString('Transaction has not been created'));
return;
}
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 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;
}
var dataSrc = {
amount: amount,
currency: currency
};
ongoingProcess.set('topup', true, statusChangeHandler);
bitpayCardService.topUp(cardId, dataSrc, function(err, invoiceId) {
publishAndSign($scope.wallet, createdTx, function() {}, function(err, txSent) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showErrorAndBack('Could not create the invoice', err);
ongoingProcess.set('topup', false);
$scope.sendStatus = '';
showError(gettextCatalog.getString('Could not send transaction'), err);
return;
}
bitpayCardService.getInvoice(invoiceId, function(err, invoice) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Could not get the invoice', err);
return;
}
var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null;
if (!payProUrl) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Error in Payment Protocol', 'Invalid URL');
return;
}
payproService.getPayProDetails(payProUrl, function(err, payProDetails) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Error fetching invoice', err);
return;
}
var outputs = [];
var toAddress = payProDetails.toAddress;
var amountSat = payProDetails.amount;
var comment = 'Top up ' + amount + ' ' + currency + ' to Debit Card (' + $scope.cardInfo.lastFourDigits + ')';
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
});
});
});
ongoingProcess.set('topup', false, statusChangeHandler);
});
});
};
$scope.showWalletSelector = function() {
@ -200,29 +301,19 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
$scope.onWalletSelect = function(wallet) {
$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 config = configService.getSync().wallet.settings;
var unitName = config.unitName;
var amountUnit = txFormatService.satToUnit(values.amount);
var parsedAmount = txFormatService.parseAmount(
amountUnit,
unitName);
amount = parsedAmount.amount;
currency = parsedAmount.currency;
$scope.amountUnitStr = parsedAmount.amountUnitStr;
$timeout(function() {
$scope.$digest();
}, 100);
});
}
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() {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('txDetailsController', function($rootScope, $log, $ionicHistory, $scope, $timeout, walletService, lodash, gettextCatalog, profileService, externalLinkService, popupService, ongoingProcess, txFormatService, txConfirmNotification) {
angular.module('copayApp.controllers').controller('txDetailsController', function($rootScope, $log, $ionicHistory, $scope, $timeout, walletService, lodash, gettextCatalog, profileService, externalLinkService, popupService, ongoingProcess, txFormatService, txConfirmNotification, feeService) {
var txId;
var listeners = [];
@ -11,20 +11,22 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.wallet = profileService.getWallet(data.stateParams.walletId);
$scope.color = $scope.wallet.color;
$scope.copayerId = $scope.wallet.credentials.copayerId;
$scope.isShared = $scope.wallet.credentials.n > 1;
$scope.isShared = $scope.wallet.credentials.n > 1;
txConfirmNotification.checkIfEnabled(txId, function(res) {
$scope.txNotification = { value: res };
$scope.txNotification = {
value: res
};
});
});
$scope.$on("$ionicView.afterEnter", function(event) {
updateTx();
listeners = [
$rootScope.$on('bwsEvent', function(e, walletId, type, n) {
if (type == 'NewBlock' && n && n.data && n.data.network == 'livenet') {
updateTxDebounced({hideLoading: true});
updateTxDebounced({
hideLoading: true
});
}
})
];
@ -36,14 +38,6 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
});
});
function getDisplayAmount(amountStr) {
return amountStr.split(' ')[0];
}
function getDisplayUnit(amountStr) {
return amountStr.split(' ')[1];
}
function updateMemo() {
walletService.getTxNote($scope.wallet, $scope.btx.txid, function(err, note) {
if (err) {
@ -108,7 +102,8 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.btx = txFormatService.processTx(tx);
txFormatService.formatAlternativeStr(tx.fees, function(v) {
$scope.feeFiatStr = v;
$scope.btx.feeFiatStr = v;
$scope.btx.feeRateStr = ($scope.btx.fees / ($scope.btx.amount + $scope.btx.fees) * 100).toFixed(2) + '%';
});
if ($scope.btx.action != 'invalid') {
@ -117,20 +112,30 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
if ($scope.btx.action == 'moved') $scope.title = gettextCatalog.getString('Moved Funds');
}
$scope.displayAmount = getDisplayAmount($scope.btx.amountStr);
$scope.displayUnit = getDisplayUnit($scope.btx.amountStr);
updateMemo();
initActionList();
getFiatRate();
$timeout(function() {
$scope.$apply();
$scope.$digest();
});
feeService.getFeeLevels(function(err, levels) {
if (err) return;
walletService.getLowAmount($scope.wallet, levels, function(err, amount) {
if (err) return;
$scope.btx.lowAmount = tx.amount < amount;
$timeout(function() {
$scope.$apply();
});
});
});
});
};
var updateTxDebounced = lodash.debounce(updateTx, 5000);
$scope.showCommentPopup = function() {
var opts = {};
if ($scope.btx.message) {
@ -197,7 +202,9 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.txConfirmNotificationChange = function() {
if ($scope.txNotification.value) {
txConfirmNotification.subscribe($scope.wallet, { txid: txId });
txConfirmNotification.subscribe($scope.wallet, {
txid: txId
});
} else {
txConfirmNotification.unsubscribe($scope.wallet, txId);
}

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService) {
angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService, feeService) {
var HISTORY_SHOW_LIMIT = 10;
var currentTxHistoryPage = 0;
@ -47,6 +47,21 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.txps = lodash.sortBy(txps, 'createdOn').reverse();
};
var analyzeUtxosDone;
var analyzeUtxos = function() {
if (analyzeUtxosDone) return;
feeService.getFeeLevels(function(err, levels){
if (err) return;
walletService.getLowUtxos($scope.wallet, levels, function(err, resp){
if (err || !resp) return;
analyzeUtxosDone = true;
$scope.lowUtxosWarning = resp.warning;
});
});
};
var updateStatus = function(force) {
$scope.updatingStatus = true;
$scope.updateStatusError = null;
@ -72,6 +87,8 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.$apply();
});
analyzeUtxos();
});
};
@ -154,9 +171,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
});
};
$timeout(function() {
feeService.getFeeLevels(function(err, levels){
walletService.getTxHistory($scope.wallet, {
progressFn: progressFn,
feeLevels: levels,
}, function(err, txHistory) {
$scope.updatingTxHistory = false;
if (err) {

View file

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

View file

@ -0,0 +1,67 @@
'use strict';
angular.module('copayApp.directives')
.directive('timer', function() {
return {
restrict: 'EAC',
replace: false,
scope: {
countdown: "=",
interval: "=",
active: "=",
onZeroCallback: "="
},
template:"{{formatted}}",
controller: function ($scope, $attrs, $timeout, lodash) {
$scope.format = $attrs.outputFormat;
var queueTick = function () {
$scope.timer = $timeout(function () {
if ($scope.countdown > 0) {
$scope.countdown -= 1;
if ($scope.countdown > 0) {
queueTick();
} else {
$scope.countdown = 0;
$scope.active = false;
if (!lodash.isUndefined($scope.onZeroCallback)) {
$scope.onZeroCallback();
}
}
}
}, $scope.interval);
};
if ($scope.active) {
queueTick();
}
$scope.$watch('active', function (newValue, oldValue) {
if (newValue !== oldValue) {
if (newValue === true) {
if ($scope.countdown > 0) {
queueTick();
} else {
$scope.active = false;
}
} else {
$timeout.cancel($scope.timer);
}
}
});
$scope.$watch('countdown', function () {
updateFormatted();
});
var updateFormatted = function () {
$scope.formatted = moment($scope.countdown * $scope.interval).format($scope.format);
};
updateFormatted();
$scope.$on('$destroy', function () {
$timeout.cancel($scope.timer);
});
}
};
});

View file

@ -12,7 +12,7 @@ angular.module('copayApp.directives')
elem.bind('click', function() {
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());
popupService.showAlert('Low Fee Error', 'Please change your Bitcoin Network Fee Policy setting to Normal or higher to use this service', function() {
$ionicHistory.goBack();

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.directives')
.directive('qrScanner', function($state, $rootScope, $log, $ionicHistory) {
.directive('qrScanner', function($state, $rootScope, $log, $ionicHistory, platformInfo, scannerService, popupService) {
return {
restrict: 'E',
@ -9,26 +9,49 @@ angular.module('copayApp.directives')
onScan: "&"
},
replace: true,
template: '<a on-tap="openScanner()" nav-transition="none"><i class="icon ion-qr-scanner"></i></a>',
template: '<a on-tap="chooseScanner()" nav-transition="none"><i class="icon ion-qr-scanner"></i></a>',
link: function(scope, el, attrs) {
scope.chooseScanner = function() {
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
scope.openScanner();
return;
}
scannerService.useOldScanner(function(err, contents) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
scope.onScan({
data: contents
});
});
};
scope.openScanner = function() {
$log.debug('Opening scanner by directive...');
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('scanner', { passthroughMode: 1 });
$state.go('scanner', {
passthroughMode: 1
});
};
var afterEnter = $rootScope.$on('$ionicView.afterEnter', function() {
if($rootScope.scanResult) {
scope.onScan({ data: $rootScope.scanResult });
if ($rootScope.scanResult) {
scope.onScan({
data: $rootScope.scanResult
});
$rootScope.scanResult = null;
}
});
// Destroy event
scope.$on('$destroy', function(){
scope.$on('$destroy', function() {
afterEnter();
});
}

View file

@ -0,0 +1,15 @@
'use strict';
angular.module('copayApp.directives')
.directive('showTabs', function($rootScope, $timeout) {
return {
restrict: 'A',
link: function($scope, $el) {
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$timeout(function() {
$rootScope.hideTabs = '';
$rootScope.$apply();
});
});
}
};
});

View file

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

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.directives')
.directive('slideToAcceptSuccess', function($timeout) {
.directive('slideToAcceptSuccess', function($timeout, platformInfo) {
return {
restrict: 'E',
templateUrl: 'views/includes/slideToAcceptSuccess.html',
@ -12,10 +12,13 @@ angular.module('copayApp.directives')
hideOnConfirm: '=slideSuccessHideOnConfirm'
},
link: function(scope, element, attrs) {
scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var elm = element[0];
elm.style.display = 'none';
scope.$watch('isShown', function() {
if(scope.isShown) {
if (scope.isShown) {
elm.style.display = 'flex';
$timeout(function() {
scope.fillScreen = true;
@ -24,7 +27,7 @@ angular.module('copayApp.directives')
});
scope.onConfirmButtonClick = function() {
scope.onConfirm();
if(scope.hideOnConfirm) {
if (scope.hideOnConfirm) {
scope.fillScreen = false;
elm.style.display = 'none';
}

View file

@ -112,11 +112,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
.state('starting', {
url: '/starting',
template: '<ion-view id="starting"><ion-content>{{starting}}</ion-content></ion-view>',
controller: function($scope, $log, gettextCatalog) {
$log.info('Starting...');
$scope.starting = gettextCatalog.getString('Starting...');
}
template: '<ion-view id="starting"><ion-content><div class="block-spinner row"><ion-spinner class="spinner-stable" icon="crescent"></ion-spinner></div></ion-content></ion-view>'
})
/*
@ -136,10 +132,6 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
})
}
})
.state('uripayment', {
url: '/uri-payment/:url',
templateUrl: 'views/paymentUri.html'
})
/*
*
@ -202,6 +194,25 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
}
})
.state('tabs.wallet.addresses', {
url: '/addresses/:walletId/:toAddress',
views: {
'tab-home@tabs': {
controller: 'addressesController',
templateUrl: 'views/addresses.html'
}
}
})
.state('tabs.wallet.allAddresses', {
url: '/allAddresses/:walletId',
views: {
'tab-home@tabs': {
controller: 'addressesController',
templateUrl: 'views/allAddresses.html'
}
}
})
/*
*
* Tabs
@ -1204,10 +1215,12 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
$ionicHistory.goBack();
} else
if ($rootScope.backButtonPressedOnceToExit) {
ionic.Platform.exitApp();
navigator.app.exitApp();
} else {
$rootScope.backButtonPressedOnceToExit = true;
window.plugins.toast.showShortBottom(gettextCatalog.getString('Press again to exit'));
$rootScope.$apply(function() {
ionicToast.show(gettextCatalog.getString('Press again to exit'), 'bottom', false, 1000);
});
$timeout(function() {
$rootScope.backButtonPressedOnceToExit = false;
}, 3000);

View file

@ -14,18 +14,18 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
* email: email address associated with bitpay account
* otp: two-factor one-time use password
* }
*
*
* pairingReason - text string to be embedded into popup message. If `null` then the reason
* message is not shown to the UI.
* "To {{reason}} you must pair this app with your BitPay account ({{email}})."
*
*
* cb - callback after completion
* callback(err, paired, apiContext)
*
* err - something unexpected happened which prevented the pairing
*
*
* paired - boolean indicating whether the pairing was compledted by the user
*
*
* apiContext - the context needed for making future api calls
* {
* token: api token for use in future calls
@ -33,6 +33,7 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
* appIdentity: the identity of this app
* }
*/
root.pair = function(pairData, pairingReason, cb) {
checkOtp(pairData, function(otp) {
pairData.otp = otp;
@ -66,14 +67,19 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
fetchBasicInfo(apiContext, function(err, basicInfo) {
if (err) return cb(err);
var title = gettextCatalog.getString('Add BitPay Account?');
var msgDetail = 'Add this BitPay account ({{email}})?';
var msg;
if (pairingReason) {
msgDetail = 'To {{reason}} you must first add your BitPay account - {{email}}';
}
var msg = gettextCatalog.getString(msgDetail, {
reason: pairingReason,
email: pairData.email
});
msg = gettextCatalog.getString('To {{reason}} you must first add your BitPay account - {{email}}', {
reason: pairingReason,
email: pairData.email
});
} else {
msg = gettextCatalog.getString('Add this BitPay account ({{email}})?', {
email: pairData.email
});
}
var ok = gettextCatalog.getString('Add Account');
var cancel = gettextCatalog.getString('Go back');
popupService.showConfirm(title, msg, ok, cancel, function(res) {
@ -182,5 +188,5 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
};
return root;
});

View file

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

View file

@ -1,11 +1,11 @@
'use strict';
angular.module('copayApp.services').factory('coinbaseService', function($http, $log, $window, $filter, platformInfo, lodash, storageService, configService, appConfigService, txFormatService, buyAndSellService, $rootScope) {
angular.module('copayApp.services').factory('coinbaseService', function($http, $log, $window, $filter, platformInfo, lodash, storageService, configService, appConfigService, txFormatService, buyAndSellService, $rootScope, feeService) {
var root = {};
var credentials = {};
var isCordova = platformInfo.isCordova;
var isNW = platformInfo.isNW;
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
root.priceSensitivity = [{
value: 0.5,
@ -107,6 +107,19 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
};
};
root.checkEnoughFundsForFee = function(amount, cb) {
_getNetAmount(amount, function(err, reducedAmount) {
if (err) return cb(err);
// Check if transaction has enough funds to transfer bitcoin from Coinbase to Copay
if (reducedAmount < 0) {
return cb('Not enough funds for fee');
}
return cb();
});
};
root.getSignupUrl = function() {
return credentials.HOST + '/signup';
}
@ -153,6 +166,17 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
});
};
var _getNetAmount = function(amount, cb) {
// Fee Normal for a single transaction (450 bytes)
var txNormalFeeKB = 450 / 1000;
feeService.getFeeRate(null, 'normal', function(err, feePerKB) {
if (err) return cb(err);
var feeBTC = (feePerKB * txNormalFeeKB / 100000000).toFixed(8);
return cb(null, amount - feeBTC, feeBTC);
});
};
var _refreshToken = function(refreshToken, cb) {
var req = {
method: 'POST',
@ -303,14 +327,14 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
};
root.getBuyOrder = function(token, accountId, buyId, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/accounts/' + accountId + '/buys/' + buyId, token)).then(function(data) {
$log.info('Coinbase Buy Info: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Coinbase Buy Info: ERROR ' + data.statusText);
return cb(data.data);
});
if (!token) return cb('Invalid Token');
$http(_get('/accounts/' + accountId + '/buys/' + buyId, token)).then(function(data) {
$log.info('Coinbase Buy Info: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Coinbase Buy Info: ERROR ' + data.statusText);
return cb(data.data);
});
};
root.getTransaction = function(token, accountId, transactionId, cb) {
@ -657,13 +681,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) {
if (!tx) return;
var desc = appConfigService.nameCase + ' Wallet';
var data = {
to: tx.toAddr,
amount: tx.amount.amount,
currency: tx.amount.currency,
description: desc
};
root.sendTo(accessToken, accountId, data, function(err, res) {
_getNetAmount(tx.amount.amount, function(err, amountBTC, feeBTC) {
if (err) {
_savePendingTransaction(tx, {
status: 'error',
@ -672,8 +690,18 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
} else {
if (res.data && !res.data.id) {
return;
}
var data = {
to: tx.toAddr,
amount: amountBTC,
currency: tx.amount.currency,
description: desc,
fee: feeBTC
};
root.sendTo(accessToken, accountId, data, function(err, res) {
if (err) {
_savePendingTransaction(tx, {
status: 'error',
error: err
@ -681,19 +709,29 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
return;
}
root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) {
_savePendingTransaction(tx, {
remove: true
}, function(err) {
_savePendingTransaction(sendTx.data, {}, function(err) {
} else {
if (res.data && !res.data.id) {
_savePendingTransaction(tx, {
status: 'error',
error: err
}, function(err) {
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
return;
}
root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) {
_savePendingTransaction(tx, {
remove: true
}, function(err) {
_savePendingTransaction(sendTx.data, {}, function(err) {
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
});
});
});
}
}
});
});
};
@ -723,7 +761,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
var register = function() {
root.isActive(function(err, isActive){
root.isActive(function(err, isActive) {
if (err) return;
buyAndSellService.register({
@ -742,7 +780,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
$rootScope.$on('bwsEvent', function(e, walletId, type, n) {
if (type == 'NewBlock' && n && n.data && n.data.network == 'livenet') {
root.isActive(function(err,isActive){
root.isActive(function(err, isActive) {
// Update Coinbase
if (isActive)
root.updatePendingTransactions();

View file

@ -1,8 +1,10 @@
'use strict';
angular.module('copayApp.services').factory('configService', function(storageService, lodash, $log, $timeout, $rootScope) {
angular.module('copayApp.services').factory('configService', function(storageService, lodash, $log, $timeout, $rootScope, platformInfo) {
var root = {};
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var defaultConfig = {
// wallet limits
limits: {
@ -73,7 +75,7 @@ angular.module('copayApp.services').factory('configService', function(storageSer
},
hideNextSteps: {
enabled: false,
enabled: isWindowsPhoneApp ? true : false,
},
rates: {

View file

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

View file

@ -4,7 +4,7 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
var root = {};
var credentials = {};
var isCordova = platformInfo.isCordova;
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var setCredentials = function() {
if (!$window.externalServices || !$window.externalServices.glidera) {

View file

@ -3,7 +3,7 @@
angular.module('copayApp.services').factory('ongoingProcess', function($log, $timeout, $filter, lodash, $ionicLoading, gettext, platformInfo) {
var root = {};
var isCordova = platformInfo.isCordova;
var isWP = platformInfo.isWP;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var ongoingProcess = {};
@ -17,8 +17,8 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
'creatingTx': gettext('Creating transaction'),
'creatingWallet': gettext('Creating Wallet...'),
'deletingWallet': gettext('Deleting Wallet...'),
'extractingWalletInfo': gettext('Extracting Wallet Information...'),
'fetchingPayPro': gettext('Fetching Payment Information'),
'extractingWalletInfo': gettext('Extracting Wallet information...'),
'fetchingPayPro': gettext('Fetching payment information'),
'generatingCSV': gettext('Generating .csv file...'),
'gettingFeeLevels': gettext('Getting fee levels...'),
'importingWallet': gettext('Importing Wallet...'),
@ -45,12 +45,12 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
'cancelingGiftCard': 'Canceling Gift Card...',
'creatingGiftCard': 'Creating Gift Card...',
'buyingGiftCard': 'Buying Gift Card...',
'topup': 'Top up in progress...'
'topup': gettext('Top up in progress...')
};
root.clear = function() {
ongoingProcess = {};
if (isCordova && !isWP) {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();
@ -80,19 +80,19 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
if (customHandler) {
customHandler(processName, showName, isOn);
} else if (root.onGoingProcessName) {
if (isCordova && !isWP) {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.show(null, showName, root.clear);
} else {
var tmpl;
if (isWP) tmpl = '<div>' + showName + '</div>';
if (isWindowsPhoneApp) tmpl = '<div>' + showName + '</div>';
else tmpl = '<div class="item-icon-left">' + showName + '<ion-spinner class="spinner-stable" icon="lines"></ion-spinner></div>';
$ionicLoading.show({
template: tmpl
});
}
} else {
if (isCordova && !isWP) {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();

View file

@ -3,16 +3,17 @@
angular.module('copayApp.services').service('popupService', function($log, $ionicPopup, platformInfo, gettextCatalog) {
var isCordova = platformInfo.isCordova;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
/*************** Ionic ****************/
var _ionicAlert = function(title, message, cb, buttonName) {
var _ionicAlert = function(title, message, cb, okText) {
if (!cb) cb = function() {};
$ionicPopup.alert({
title: title,
subTitle: message,
okType: 'button-clear button-positive',
okText: buttonName || gettextCatalog.getString('OK'),
okText: okText || gettextCatalog.getString('OK'),
}).then(cb);
};
@ -45,9 +46,11 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
/*************** Cordova ****************/
var _cordovaAlert = function(title, message, cb, buttonName) {
var _cordovaAlert = function(title, message, cb, okText) {
if (!cb) cb = function() {};
navigator.notification.alert(message, cb, title, buttonName);
title = title ? title : '';
okText = okText || gettextCatalog.getString('OK');
navigator.notification.alert(message, cb, title, okText);
};
var _cordovaConfirm = function(title, message, okText, cancelText, cb) {
@ -57,6 +60,7 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
}
okText = okText || gettextCatalog.getString('OK');
cancelText = cancelText || gettextCatalog.getString('Cancel');
title = title ? title : '';
navigator.notification.confirm(message, onConfirm, title, [cancelText, okText]);
};
@ -65,7 +69,10 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
if (results.buttonIndex == 1) return cb(results.input1);
else return cb();
}
navigator.notification.prompt(message, onPrompt, title, null, opts.defaultText);
var okText = gettextCatalog.getString('OK');
var cancelText = gettextCatalog.getString('Cancel');
title = title ? title : '';
navigator.notification.prompt(message, onPrompt, title, [cancelText, okText], opts.defaultText);
};
/**
@ -76,14 +83,14 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
* @param {Callback} Function (optional)
*/
this.showAlert = function(title, msg, cb, buttonName) {
this.showAlert = function(title, msg, cb, okText) {
var message = (msg && msg.message) ? msg.message : msg;
$log.warn(title ? (title + ': ' + message) : message);
if (isCordova)
_cordovaAlert(title, message, cb, buttonName);
_cordovaAlert(title, message, cb, okText);
else
_ionicAlert(title, message, cb, buttonName);
_ionicAlert(title, message, cb, okText);
};
/**
@ -121,7 +128,7 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
opts = opts ||  {};
if (isCordova && !opts.forceHTMLPrompt)
if (isCordova && !isWindowsPhoneApp && !opts.forceHTMLPrompt)
_cordovaPrompt(title, message, opts, cb);
else
_ionicPrompt(title, message, opts, cb);

View file

@ -5,12 +5,12 @@ angular.module('copayApp.services')
var isChromeApp = platformInfo.isChromeApp;
var isCordova = platformInfo.isCordova;
var isWP = platformInfo.isWP;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var isIOS = platformInfo.isIOS;
var root = {};
var errors = bwcService.getErrors();
var usePushNotifications = isCordova && !isWP;
var usePushNotifications = isCordova && !isWindowsPhoneApp;
var UPDATE_PERIOD = 15;
@ -208,9 +208,9 @@ angular.module('copayApp.services')
};
var shouldSkipValidation = function(walletId) {
return root.profile.isChecked(platformInfo.ua, walletId) || isIOS || isWP;
}
// Used when reading wallets from the profile
return root.profile.isChecked(platformInfo.ua, walletId) || isIOS || isWindowsPhoneApp;
}
// Used when reading wallets from the profile
root.bindWallet = function(credentials, cb) {
if (!credentials.walletId || !credentials.m)
return cb('bindWallet should receive credentials JSON');

View file

@ -17,27 +17,27 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
var canChangeCamera = false;
var canOpenSettings = false;
function _checkCapabilities(status){
function _checkCapabilities(status) {
$log.debug('scannerService is reviewing platform capabilities...');
// Permission can be assumed on the desktop builds
hasPermission = (isDesktop || status.authorized)? true: false;
isDenied = status.denied? true : false;
isRestricted = status.restricted? true : false;
canEnableLight = status.canEnableLight? true : false;
canChangeCamera = status.canChangeCamera? true : false;
canOpenSettings = status.canOpenSettings? true : false;
hasPermission = (isDesktop || status.authorized) ? true : false;
isDenied = status.denied ? true : false;
isRestricted = status.restricted ? true : false;
canEnableLight = status.canEnableLight ? true : false;
canChangeCamera = status.canChangeCamera ? true : false;
canOpenSettings = status.canOpenSettings ? true : false;
_logCapabilities();
}
function _logCapabilities(){
function _orIsNot(bool){
return bool? '' : 'not ';
function _logCapabilities() {
function _orIsNot(bool) {
return bool ? '' : 'not ';
}
$log.debug('A camera is ' + _orIsNot(isAvailable) + 'available to this app.');
var access = 'not authorized';
if(hasPermission) access = 'authorized';
if(isDenied) access = 'denied';
if(isRestricted) access = 'restricted';
if (hasPermission) access = 'authorized';
if (isDenied) access = 'denied';
if (isRestricted) access = 'restricted';
$log.debug('Camera access is ' + access + '.');
$log.debug('Support for opening device settings is ' + _orIsNot(canOpenSettings) + 'available on this platform.');
$log.debug('A light is ' + _orIsNot(canEnableLight) + 'available on this platform.');
@ -47,7 +47,7 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
/**
* Immediately return known capabilities of the current platform.
*/
this.getCapabilities = function(){
this.getCapabilities = function() {
return {
isAvailable: isAvailable,
hasPermission: hasPermission,
@ -68,18 +68,18 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
* The `status` of QRScanner is returned to the callback.
*/
this.gentleInitialize = function(callback) {
if(initializeStarted && !isDesktop){
QRScanner.getStatus(function(status){
if (initializeStarted && !isDesktop) {
QRScanner.getStatus(function(status) {
_completeInitialization(status, callback);
});
return;
}
initializeStarted = true;
$log.debug('Trying to pre-initialize QRScanner.');
if(!isDesktop){
QRScanner.getStatus(function(status){
if (!isDesktop) {
QRScanner.getStatus(function(status) {
_checkCapabilities(status);
if(status.authorized){
if (status.authorized) {
$log.debug('Camera permission already granted.');
initialize(callback);
} else {
@ -92,14 +92,14 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
}
};
function initialize(callback){
function initialize(callback) {
$log.debug('Initializing scanner...');
QRScanner.prepare(function(err, status){
if(err){
QRScanner.prepare(function(err, status) {
if (err) {
isAvailable = false;
$log.error(err);
// does not return `status` if there is an error
QRScanner.getStatus(function(status){
QRScanner.getStatus(function(status) {
_completeInitialization(status, callback);
});
} else {
@ -112,18 +112,19 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
// This could be much cleaner with a Promise API
// (needs a polyfill for some platforms)
var initializeCompleted = false;
function _completeInitialization(status, callback){
function _completeInitialization(status, callback) {
_checkCapabilities(status);
initializeCompleted = true;
$rootScope.$emit('scannerServiceInitialized');
if(typeof callback === "function"){
if (typeof callback === "function") {
callback(status);
}
}
this.isInitialized = function(){
this.isInitialized = function() {
return initializeCompleted;
};
this.initializeStarted = function(){
this.initializeStarted = function() {
return initializeStarted;
};
@ -140,21 +141,21 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
*/
this.activate = function(callback) {
$log.debug('Activating scanner...');
QRScanner.show(function(status){
initializeCompleted = true;
_checkCapabilities(status);
if(typeof callback === "function"){
callback(status);
}
});
if(nextHide !== null){
$timeout.cancel(nextHide);
nextHide = null;
}
if(nextDestroy !== null){
$timeout.cancel(nextDestroy);
nextDestroy = null;
QRScanner.show(function(status) {
initializeCompleted = true;
_checkCapabilities(status);
if (typeof callback === "function") {
callback(status);
}
});
if (nextHide !== null) {
$timeout.cancel(nextHide);
nextHide = null;
}
if (nextDestroy !== null) {
$timeout.cancel(nextDestroy);
nextDestroy = null;
}
};
/**
@ -193,18 +194,18 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
// Natively hide the QRScanner's preview
// On mobile platforms, this can reduce GPU/power usage
// On desktop, this fully turns off the camera (and any associated privacy lights)
function _hide(){
function _hide() {
$log.debug('Scanner not in use for ' + hideAfterSeconds + ' seconds, hiding...');
QRScanner.hide();
}
// Reduce QRScanner power/processing consumption by the maximum amount
function _destroy(){
function _destroy() {
$log.debug('Scanner not in use for ' + destroyAfterSeconds + ' seconds, destroying...');
QRScanner.destroy();
}
this.reinitialize = function(callback){
this.reinitialize = function(callback) {
initializeCompleted = false;
QRScanner.destroy();
initialize(callback);
@ -217,17 +218,18 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
*/
this.toggleLight = function(callback) {
$log.debug('Toggling light...');
if(lightEnabled){
if (lightEnabled) {
QRScanner.disableLight(_handleResponse);
} else {
QRScanner.enableLight(_handleResponse);
}
function _handleResponse(err, status){
if(err){
function _handleResponse(err, status) {
if (err) {
$log.error(err);
} else {
lightEnabled = status.lightEnabled;
var state = lightEnabled? 'enabled' : 'disabled';
var state = lightEnabled ? 'enabled' : 'disabled';
$log.debug('Light ' + state + '.');
}
callback(lightEnabled);
@ -241,16 +243,17 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
* is complete.
*/
this.toggleCamera = function(callback) {
var nextCamera = backCamera? 1 : 0;
function cameraToString(index){
return index === 1? 'front' : 'back'; // front = 1, back = 0
var nextCamera = backCamera ? 1 : 0;
function cameraToString(index) {
return index === 1 ? 'front' : 'back'; // front = 1, back = 0
}
$log.debug('Toggling to the ' + cameraToString(nextCamera) + ' camera...');
QRScanner.useCamera(nextCamera, function(err, status){
if(err){
QRScanner.useCamera(nextCamera, function(err, status) {
if (err) {
$log.error(err);
}
backCamera = status.currentCamera === 1? false : true;
backCamera = status.currentCamera === 1 ? false : true;
$log.debug('Camera toggled. Now using the ' + cameraToString(backCamera) + ' camera.');
callback(status);
});
@ -260,4 +263,15 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
$log.debug('Attempting to open device settings...');
QRScanner.openSettings();
};
this.useOldScanner = function(callback) {
cordova.plugins.barcodeScanner.scan(
function(result) {
callback(null, result.text);
},
function(error) {
callback(error);
}
);
}
});

View file

@ -10,7 +10,7 @@ angular.module('copayApp.services').service('sendMaxService', function(feeServic
*
*/
this.getInfo = function(wallet, cb) {
feeService.getCurrentFeeValue(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;

View file

@ -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() {
@ -93,6 +113,11 @@ angular.module('copayApp.services').factory('txFormatService', function($filter,
tx.alternativeAmountStr = root.formatAlternativeStr(tx.amount);
tx.feeStr = root.formatAmountStr(tx.fee || tx.fees);
if (tx.amountStr) {
tx.amountValueStr = tx.amountStr.split(' ')[0];
tx.amountUnitStr = tx.amountStr.split(' ')[1];
}
return tx;
};
@ -164,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);
@ -176,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

View file

@ -1,7 +1,12 @@
'use strict';
angular.module('copayApp.services').factory('walletService', function($log, $timeout, lodash, trezor, ledger, intelTEE, storageService, configService, rateService, uxLanguage, $filter, gettextCatalog, bwcError, $ionicPopup, fingerprintService, ongoingProcess, gettext, $rootScope, txFormatService, $ionicModal, $state, bwcService, bitcore, popupService) {
// `wallet` is a decorated version of client.
// Ratio low amount warning (fee/amount) in incoming TX
var LOW_AMOUNT_RATIO = 0.15;
// Ratio of "many utxos" warning in total balance (fee/amount)
var TOTAL_LOW_WARNING_RATIO = .3;
var root = {};
@ -401,6 +406,11 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
var progressFn = opts.progressFn || function() {};
var foundLimitTx = false;
if (opts.feeLevels) {
opts.lowAmount = root.getLowAmount(wallet, opts.feeLevels);
}
var fixTxsUnit = function(txs) {
if (!txs || !txs[0] || !txs[0].amountStr) return;
@ -413,7 +423,6 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
$log.debug('Fixing Tx Cache Unit to:' + name)
lodash.each(txs, function(tx) {
tx.amountStr = txFormatService.formatAmount(tx.amount) + name;
tx.feeStr = txFormatService.formatAmount(tx.fees) + name;
});
@ -511,6 +520,16 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
}
function updateLowAmount(txs) {
if (!opts.lowAmount) return;
lodash.each(txs, function(tx) {
tx.lowAmount = tx.amount < opts.lowAmount;
});
};
updateLowAmount(txs);
updateNotes(function() {
// <HACK>
@ -567,9 +586,9 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
root.getTx = function(wallet, txid, cb) {
function finish(list){
function finish(list) {
var tx = lodash.find(list, {
txid: txid
txid: txid
});
if (!tx) return cb('Could not get transaction');
@ -602,7 +621,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
};
root.getTxHistory = function(wallet, opts, cb) {
opts = opts || {};
@ -873,6 +892,81 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
};
// These 2 functions were taken from
// https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js#L243
function getEstimatedSizeForSingleInput(wallet) {
switch (wallet.credentials.addressType) {
case 'P2PKH':
return 147;
default:
case 'P2SH':
return wallet.m * 72 + wallet.n * 36 + 44;
}
};
root.getEstimatedTxSize = function(wallet, nbOutputs) {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.02;
var overhead = 4 + 4 + 9 + 9;
var inputSize = getEstimatedSizeForSingleInput(wallet);
var outputSize = 34;
var nbInputs = 1; //Assume 1 input
var nbOutputs = nbOutputs || 2; // Assume 2 outputs
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
return parseInt((size * (1 + safetyMargin)).toFixed(0));
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getMinFee = function(wallet, feeLevels, nbOutputs) {
var lowLevelRate = (lodash.find(feeLevels[wallet.network], {
level: 'normal',
}).feePerKB / 1000).toFixed(0);
var size = root.getEstimatedTxSize(wallet, nbOutputs);
return size * lowLevelRate;
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getLowAmount = function(wallet, feeLevels, nbOutputs) {
var minFee = root.getMinFee(wallet,feeLevels, nbOutputs);
return parseInt( minFee / LOW_AMOUNT_RATIO);
};
root.getLowUtxos = function(wallet, levels, cb) {
wallet.getUtxos({}, function(err, resp) {
if (err || !resp || !resp.length) return cb();
var minFee = root.getMinFee(wallet, levels, resp.length);
var balance = lodash.sum(resp, 'satoshis');
// for 2 outputs
var lowAmount = root.getLowAmount(wallet, levels);
var lowUtxos = lodash.filter(resp, function(x) {
return x.satoshis < lowAmount;
});
var totalLow = lodash.sum(lowUtxos, 'satoshis');
return cb(err, {
allUtxos: resp || [],
lowUtxos: lowUtxos || [],
warning: minFee / balance > TOTAL_LOW_WARNING_RATIO,
minFee: minFee,
});
});
};
root.getAddress = function(wallet, forceNew, cb) {
storageService.getLastAddress(wallet.id, function(err, addr) {
if (err) return cb(err);

View file

@ -11,7 +11,7 @@
font-size: 14px;
font-weight: 300;
color: #727272;
padding: 2px 1rem;
padding: 1rem;
background: #f8f8f9;
i {
position: absolute;
@ -52,8 +52,8 @@
width: 35px;
}
span {
text-transform: capitalize;
.big-icon-svg {
padding: 0 12px 0 0;
}
}
.amount-label{
@ -101,6 +101,9 @@
color: $v-bitcoin-orange;
}
}
.total {
font-weight: bold;
}
}
.tx-icon {
margin-right: 25px;

View file

@ -5,6 +5,10 @@
float: none;
.fee-rate {
display: inline-block;
.warn {
color: red;
}
}
}
.icon-amazon {

View file

@ -24,7 +24,6 @@ click-to-accept {
height: 100%;
width: 100%;
z-index: 4;
text-transform: capitalize;
-webkit-transform: translateY(2rem);
transform: translateY(2rem);
opacity: 0;

View file

@ -95,7 +95,6 @@ slide-to-accept {
width: 100%;
font-size: 17px;
letter-spacing: 0.02rem;
text-transform: capitalize;
-webkit-transform: translateY(2rem);
transform: translateY(2rem);
opacity: 0;

View file

@ -12,6 +12,12 @@ slide-to-accept-success {
.slide-success {
$duration: 400ms;
&__windows-background {
background: $v-success-bg-color;
height: 100%;
width: 100%;
position: fixed;
}
&__background {
$start-radius: 5;
$scale-factor: 20;

View file

@ -5,7 +5,12 @@
$item-vertical-padding: 10px;
$item-border-color: #EFEFEF;
$item-label-color: #6C6C6E;
.item-note {
float: none;
.fee-rate {
display: inline-block;
}
}
.list {
background: #f5f5f5;
}
@ -27,10 +32,6 @@
height: 35px;
width: 35px;
}
span {
text-transform: capitalize;
}
}
.amount-label{
line-height: 30px;
@ -93,7 +94,7 @@
&.low-fees {
display: flex;
font-size: 14px;
color: #aaa;
color: #777;
align-items: center;
i {
padding-right: 20px;
@ -107,7 +108,7 @@
margin-top: 5px;
font-size: 14px;
color: $item-label-color;
}
}
.item-divider {
padding-top: 1.2rem;
color: $item-label-color;

View file

@ -11,7 +11,7 @@
stroke: black;
fill: black;
}
.add-bottom-for-cta {
bottom: 92px;
}
@ -31,10 +31,6 @@
width: 35px;
}
span {
text-transform: capitalize;
}
.big-icon-svg {
margin-right: 0.6rem;
}
@ -80,7 +76,7 @@
color: $item-label-color;
margin-bottom: 8px;
}
.capitalized {
text-transform: capitalize;
}

View file

@ -28,10 +28,6 @@
height: 35px;
width: 35px;
}
span {
text-transform: capitalize;
}
}
.amount-label{
line-height: 30px;

View file

@ -28,10 +28,6 @@
height: 35px;
width: 35px;
}
span {
text-transform: capitalize;
}
}
.amount-label{
line-height: 30px;

View file

@ -18,4 +18,7 @@
vertical-align: middle;
text-align: center;
}
.block-spinner {
display: block;
}
}

View file

@ -85,7 +85,7 @@
font-size: 14px;
font-weight: 300;
color: #727272;
padding: 2px 1rem;
padding: 1rem;
background: #f8f8f9;
}
}
@ -248,6 +248,7 @@
text-decoration: none;
z-index: 9999;
position: relative;
padding: 2px 5px;
}
a.item {