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

@ -93,7 +93,14 @@ module.exports = function(grunt) {
'src/js/controllers/**/*.js' 'src/js/controllers/**/*.js'
], ],
tasks: ['concat:js'] tasks: ['concat:js']
} },
gettext: {
files: [
'i18n/po/*.po',
'i18n/po/*.pot'
],
tasks: ['nggettext_compile','concat']
},
}, },
sass: { sass: {
dist: { dist: {

View file

@ -43,11 +43,11 @@ cd copay
Ensure you have [Node](https://nodejs.org/) installed, then install and start Copay: Ensure you have [Node](https://nodejs.org/) installed, then install and start Copay:
```sh ```sh
npm install npm run apply:copay
npm start npm start
``` ```
Visit [`localhost:3000`](http://localhost:3000/) to view the app. Visit [`localhost:8100`](http://localhost:8100/) to view the app.
A watch task is also available to rebuild components of the app as changes are made. This task can be run in a separate process while the server started by `npm start` is running to quickly test changes. A watch task is also available to rebuild components of the app as changes are made. This task can be run in a separate process while the server started by `npm start` is running to quickly test changes.

View file

@ -17,13 +17,13 @@
"url": "https://bitpay.com", "url": "https://bitpay.com",
"appDescription": "Secure Bitcoin Wallet", "appDescription": "Secure Bitcoin Wallet",
"winAppName": "BitPayWallet", "winAppName": "BitPayWallet",
"wpPublisherId": "{}", "WindowsStoreIdentityName": "18C7659D.BitPaySecureBitcoinWallet",
"wpProductId": "{}", "WindowsStoreDisplayName": "BitPay - Secure Bitcoin Wallet",
"windowsAppId": "2d1002d7-ee34-4f60-bd29-0c871ba0c195", "windowsAppId": "2d1002d7-ee34-4f60-bd29-0c871ba0c195",
"pushSenderId": "1036948132229", "pushSenderId": "1036948132229",
"description": "Secure Bitcoin Wallet", "description": "Secure Bitcoin Wallet",
"version": "3.5.0", "version": "3.6.3",
"androidVersion": "350000", "androidVersion": "363000",
"_extraCSS": null, "_extraCSS": null,
"_enabledExtensions": { "_enabledExtensions": {
"coinbase": true, "coinbase": true,

View file

@ -17,6 +17,9 @@
<preference name="DisallowOverscroll" value="true"/> <preference name="DisallowOverscroll" value="true"/>
<preference name="HideKeyboardFormAccessoryBar" value="true"/> <preference name="HideKeyboardFormAccessoryBar" value="true"/>
<!-- #355 --> <!-- #355 -->
<preference name="WindowsStoreIdentityName" value="*WINDOWSSTOREIDENTITYNAME*"/>
<preference name="WindowsStoreDisplayName" value="*WINDOWSSTOREDISPLAYNAME*"/>
<!-- <preference name="KeyboardDisplayRequiresUserAction" value="false" /> --> <!-- <preference name="KeyboardDisplayRequiresUserAction" value="false" /> -->
<preference name="StatusBarBackgroundColor" value="#494949" /> <preference name="StatusBarBackgroundColor" value="#494949" />
<preference name="BackupWebStorage" value="none"/> <preference name="BackupWebStorage" value="none"/>
@ -43,7 +46,7 @@
<plugin name="cordova-plugin-statusbar" spec="~2.2.0" /> <plugin name="cordova-plugin-statusbar" spec="~2.2.0" />
<plugin name="cordova-plugin-inappbrowser" spec="~1.5.0" /> <plugin name="cordova-plugin-inappbrowser" spec="~1.5.0" />
<plugin name="cordova-plugin-x-toast" spec="~2.5.2" /> <plugin name="cordova-plugin-x-toast" spec="~2.5.2" />
<plugin name="com.verso.cordova.clipboard" spec="https://github.com/VersoSolutions/CordovaClipboard" /> <plugin name="com.verso.cordova.clipboard" spec="https://github.com/Visigo/CordovaClipboard" />
<plugin name="cordova-plugin-x-socialsharing" spec="https://github.com/EddyVerbruggen/SocialSharing-PhoneGap-Plugin.git" /> <plugin name="cordova-plugin-x-socialsharing" spec="https://github.com/EddyVerbruggen/SocialSharing-PhoneGap-Plugin.git" />
<plugin name="cordova-plugin-spinner-dialog" spec="~1.3.1" /> <plugin name="cordova-plugin-spinner-dialog" spec="~1.3.1" />
<plugin name="cordova-plugin-dialogs" spec="~1.3.0" /> <plugin name="cordova-plugin-dialogs" spec="~1.3.0" />
@ -68,10 +71,13 @@
<plugin name="cordova-plugin-queries-schemes" spec="~0.1.5" /> <plugin name="cordova-plugin-queries-schemes" spec="~0.1.5" />
<plugin name="cordova-plugin-fcm" spec="https://github.com/cmgustavo/cordova-plugin-fcm.git" /> <plugin name="cordova-plugin-fcm" spec="https://github.com/cmgustavo/cordova-plugin-fcm.git" />
<!-- Delete cordova-plugin-qrscanner plugin and enable phonegap-plugin-barcodescanner to build cordova windows-->
<!-- <plugin name="phonegap-plugin-barcodescanner" spec="https://github.com/phonegap/phonegap-plugin-barcodescanner.git" /> -->
<!-- Supported Platforms --> <!-- Supported Platforms -->
<engine name="ios" spec="~4.2.1" /> <engine name="ios" spec="~4.2.1" />
<engine name="android" spec="~6.2.3" /> <engine name="android" spec="~6.2.3" />
<engine name="windows" spec="~4.4.3" /> <engine name="windows" spec="~5.0.0" />
<!-- Platform Specific Settings --> <!-- Platform Specific Settings -->
<platform name="ios"> <platform name="ios">

View file

@ -17,13 +17,13 @@
"url": "https://copay.io", "url": "https://copay.io",
"appDescription": "Copay Bitcoin Wallet", "appDescription": "Copay Bitcoin Wallet",
"winAppName": "CopayWallet", "winAppName": "CopayWallet",
"wpPublisherId": "{31cdd08b-457c-413d-b440-f6665eec847d}", "WindowsStoreIdentityName": "18C7659D.Copay-SecureBitcoinWallet",
"wpProductId": "{5381aa50-9069-11e4-84cc-293caf9cbdc8}", "WindowsStoreDisplayName": "Copay - Secure Bitcoin Wallet",
"windowsAppId": "804636ee-b017-4cad-8719-e58ac97ffa5c", "windowsAppId": "804636ee-b017-4cad-8719-e58ac97ffa5c",
"pushSenderId": "1036948132229", "pushSenderId": "1036948132229",
"description": "A Secure Bitcoin Wallet", "description": "A Secure Bitcoin Wallet",
"version": "3.5.0", "version": "3.6.3",
"androidVersion": "350000", "androidVersion": "363000",
"_extraCSS": null, "_extraCSS": null,
"_enabledExtensions": { "_enabledExtensions": {
"coinbase": true, "coinbase": true,

View file

@ -81,7 +81,7 @@
}, },
"scripts": { "scripts": {
"postinstall": "bower install", "postinstall": "bower install",
"start": "npm run build:www && ionic serve --nolivereload --nogulp -s", "start": "npm run build:www && ionic serve --nolivereload --nogulp -s --address 0.0.0.0",
"start:ios": "npm run build:www && npm run build:ios && npm run open:ios", "start:ios": "npm run build:www && npm run build:ios && npm run open:ios",
"start:android": "npm run build:www && npm run build:android && npm run run:android", "start:android": "npm run build:www && npm run build:android && npm run run:android",
"start:windows": "npm run build:www && npm run build:windows", "start:windows": "npm run build:www && npm run build:windows",
@ -91,10 +91,10 @@
"build:www-release": "grunt prod", "build:www-release": "grunt prod",
"build:ios": "cordova prepare ios && cordova build ios --debug", "build:ios": "cordova prepare ios && cordova build ios --debug",
"build:android": "cordova prepare android && cordova build android --debug", "build:android": "cordova prepare android && cordova build android --debug",
"build:windows": "cordova prepare windows && cordova build windows -- --arch=\"x86\"", "build:windows": "cordova prepare windows && cordova build windows -- --arch=\"ARM\"",
"build:ios-release": "cordova prepare ios && cordova build ios --release", "build:ios-release": "cordova prepare ios && cordova build ios --release",
"build:android-release": "cordova prepare android && cordova build android --release", "build:android-release": "cordova prepare android && cordova build android --release",
"build:windows-release": "cordova prepare windows && cordova build windows --release --arch=\"x86\"", "build:windows-release": "cordova prepare windows && cordova build windows --release --arch=\"ARM\"",
"build:desktop": "grunt desktop", "build:desktop": "grunt desktop",
"build:osx": "grunt osx", "build:osx": "grunt osx",
"open:ios": "open platforms/ios/*.xcodeproj", "open:ios": "open platforms/ios/*.xcodeproj",

11
build.json Normal file
View file

@ -0,0 +1,11 @@
{
"windows": {
"debug": {
"packageCertificateKeyFile": "platforms\\windows\\CordovaApp_TemporaryKey.pfx"
},
"release": {
"packageThumbprint": "ABCABCABCABC123123123123",
"publisherId": "CN=F89609D1-EB3E-45FD-A58A-C2E3895FCE7B"
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
'use strict'; '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 UNUSED_ADDRESS_LIMIT = 5;
var BALANCE_ADDRESS_LIMIT = 5; var BALANCE_ADDRESS_LIMIT = 5;
var config = configService.getSync().wallet.settings; 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); $scope.latestWithBalance = lodash.slice(withBalance, 0, BALANCE_ADDRESS_LIMIT);
lodash.each(withBalance, function(a) { lodash.each(withBalance, function(a) {
a.balanceStr = (a.amount * satToUnit).toFixed(unitDecimals) + ' ' + unitName; a.balanceStr = txFormatService.formatAmount(a.amount);
}); });
$scope.viewAll = { $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) { function processPaths(list) {
@ -116,7 +141,14 @@ angular.module('copayApp.controllers').controller('addressesController', functio
}; };
$scope.viewAllAddresses = function() { $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 walletId: $scope.wallet.id
}); });
}; };

View file

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

View file

@ -47,10 +47,20 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
data.stateParams.amount, data.stateParams.amount,
data.stateParams.currency); data.stateParams.currency);
amount = parsedAmount.amount; // Buy always in BTC
currency = parsedAmount.currency; amount = (parsedAmount.amountSat / 100000000).toFixed(8);
currency = 'BTC';
$scope.amountUnitStr = parsedAmount.amountUnitStr; $scope.amountUnitStr = parsedAmount.amountUnitStr;
ongoingProcess.set('calculatingFee', true);
coinbaseService.checkEnoughFundsForFee(amount, function(err) {
ongoingProcess.set('calculatingFee', false);
if (err) {
showErrorAndBack(err);
return;
}
$scope.network = coinbaseService.getNetwork(); $scope.network = coinbaseService.getNetwork();
$scope.wallets = profileService.getWallets({ $scope.wallets = profileService.getWallets({
onlyComplete: true, onlyComplete: true,
@ -107,6 +117,7 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
}); });
}); });
}); });
});
$scope.buyRequest = function() { $scope.buyRequest = function() {
ongoingProcess.set('connectingCoinbase', true); ongoingProcess.set('connectingCoinbase', true);
@ -139,7 +150,7 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
}; };
$scope.buyConfirm = function() { $scope.buyConfirm = function() {
var message = 'Buy bitcoin for ' + amount + ' ' + currency; var message = 'Buy bitcoin for ' + $scope.amountUnitStr;
var okText = 'Confirm'; var okText = 'Confirm';
var cancelText = 'Cancel'; var cancelText = 'Cancel';
popupService.showConfirm(null, message, okText, cancelText, function(ok) { popupService.showConfirm(null, message, okText, cancelText, function(ok) {

View file

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

View file

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

View file

@ -50,6 +50,7 @@ angular.module('copayApp.controllers').controller('joinController',
$scope.onQrCodeScannedJoin = function(data) { $scope.onQrCodeScannedJoin = function(data) {
$scope.formData.secret = data; $scope.formData.secret = data;
$scope.$apply();
}; };
if ($stateParams.url) { 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) { 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) { if (!account || account < 1) {
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid account number')); popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid account number'));
return; return;

View file

@ -1,6 +1,6 @@
'use strict'; '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 isGlidera = $scope.isGlidera;
var GLIDERA_LOCK_TIME = 6 * 60 * 60; var GLIDERA_LOCK_TIME = 6 * 60 * 60;
var now = Math.floor(Date.now() / 1000); var now = Math.floor(Date.now() / 1000);
@ -9,37 +9,41 @@ angular.module('copayApp.controllers').controller('txpDetailsController', functi
$scope.init = function() { $scope.init = function() {
$scope.loading = null; $scope.loading = null;
$scope.isCordova = platformInfo.isCordova; $scope.isCordova = platformInfo.isCordova;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.copayers = $scope.wallet.status.wallet.copayers;
$scope.copayerId = $scope.wallet.credentials.copayerId; $scope.copayerId = $scope.wallet.credentials.copayerId;
$scope.isShared = $scope.wallet.credentials.n > 1; $scope.isShared = $scope.wallet.credentials.n > 1;
$scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal(); $scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal();
$scope.color = $scope.wallet.color; $scope.color = $scope.wallet.color;
$scope.data = {}; $scope.data = {};
$scope.displayAmount = getDisplayAmount($scope.tx.amountStr); displayFeeValues();
$scope.displayUnit = getDisplayUnit($scope.tx.amountStr);
initActionList(); initActionList();
checkPaypro(); checkPaypro();
applyButtonText(); 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() { 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, { var lastSigner = lodash.filter($scope.tx.actions, {
type: 'accept' type: 'accept'
}).length == $scope.tx.requiredSignatures - 1; }).length == $scope.tx.requiredSignatures - 1;
if (lastSigner) if (lastSigner) {
$scope.buttonText += gettextCatalog.getString('to send'); $scope.buttonText += gettextCatalog.getString('to send');
else $scope.successText = gettextCatalog.getString('Payment Sent');
} else {
$scope.buttonText += gettextCatalog.getString('to accept'); $scope.buttonText += gettextCatalog.getString('to accept');
}; $scope.successText = gettextCatalog.getString('Payment Accepted');
}
function getDisplayAmount(amountStr) {
return amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return amountStr.split(' ')[1];
}; };
function initActionList() { function initActionList() {

View file

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

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('preferencesController', 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 wallet;
var walletId; var walletId;
@ -76,7 +76,7 @@ angular.module('copayApp.controllers').controller('preferencesController',
wallet = profileService.getWallet(data.stateParams.walletId); wallet = profileService.getWallet(data.stateParams.walletId);
walletId = wallet.credentials.walletId; walletId = wallet.credentials.walletId;
$scope.wallet = wallet; $scope.wallet = wallet;
$scope.isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
$scope.externalSource = null; $scope.externalSource = null;
if (!wallet) if (!wallet)

View file

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

View file

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

View file

@ -1,6 +1,6 @@
'use strict'; '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 originalList;
var CONTACTS_SHOW_LIMIT; var CONTACTS_SHOW_LIMIT;
@ -120,7 +120,20 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
}; };
$scope.openScanner = function() { $scope.openScanner = function() {
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
$state.go('tabs.scan'); $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() { $scope.showMore = function() {

View file

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

View file

@ -1,6 +1,6 @@
'use strict'; '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) { $scope.onScan = function(data) {
if (!incomingData.redir(data)) { if (!incomingData.redir(data)) {
@ -22,8 +22,23 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
}, 1); }, 1);
}; };
$scope.$on("$ionicView.beforeEnter", function(event, data) { $scope.chooseScanner = function() {
$rootScope.hideTabs = '';
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'; '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) { angular.module('copayApp.controllers').controller('topUpController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicConfig, lodash, popupService, profileService, ongoingProcess, walletService, configService, platformInfo, bitpayService, bitpayCardService, payproService, bwcError, txFormatService, sendMaxService, gettextCatalog) {
var amount;
var currency;
var cardId;
var sendMax;
$scope.isCordova = platformInfo.isCordova; $scope.isCordova = platformInfo.isCordova;
var cardId;
$scope.$on("$ionicView.beforeLeave", function(event, data) { var useSendMax;
$ionicConfig.views.swipeBackEnabled(true); var amount;
}); var currency;
var createdTx;
$scope.$on("$ionicView.enter", function(event, data) { var message;
$ionicConfig.views.swipeBackEnabled(false); var configWallet = configService.getSync().wallet;
});
var showErrorAndBack = function(title, msg) { var showErrorAndBack = function(title, msg) {
title = title || 'Error'; title = title || gettextCatalog.getString('Error');
$scope.sendStatus = ''; $scope.sendStatus = '';
$log.error(msg); $log.error(msg);
msg = msg.errors ? msg.errors[0].message : 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) { var showError = function(title, msg, cb) {
title = title || 'Error'; cb = cb || function() {};
title = title || gettextCatalog.getString('Error');
$scope.sendStatus = ''; $scope.sendStatus = '';
$log.error(msg); $log.error(msg);
msg = msg.errors ? msg.errors[0].message : 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) { var publishAndSign = function (wallet, txp, onSendStatusChange, cb) {
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { 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); $log.info(err);
return cb(err); return cb(err);
} }
@ -60,135 +61,235 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
} }
}; };
$scope.$on("$ionicView.beforeEnter", function(event, data) { var setTotalAmount = function(amountSat, invoiceFeeSat, networkFeeSat) {
cardId = data.stateParams.id; satToFiat(amountSat, function(a) {
sendMax = data.stateParams.useSendMax; $scope.amount = Number(a);
if (!cardId) { satToFiat(invoiceFeeSat, function(i) {
showErrorAndBack(null, 'No card selected'); $scope.invoiceFee = Number(i);
return;
}
var parsedAmount = txFormatService.parseAmount( satToFiat(networkFeeSat, function(n) {
data.stateParams.amount, $scope.networkFee = Number(n);
data.stateParams.currency); $scope.totalAmount = $scope.amount + $scope.invoiceFee + $scope.networkFee;
$timeout(function() {
amount = parsedAmount.amount; $scope.$digest();
currency = parsedAmount.currency; });
$scope.amountUnitStr = parsedAmount.amountUnitStr;
$scope.network = bitpayService.getEnvironment().network;
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network,
hasFunds: true,
minAmount: parsedAmount.amountSat
});
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack(null, 'Insufficient funds');
return;
}
$scope.onWalletSelect($scope.wallets[0]); // Default first wallet
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;
}); });
}); });
}); });
$scope.topUpConfirm = function() {
var config = configService.getSync();
var configWallet = config.wallet;
var walletSettings = configWallet.settings;
var message = 'Add ' + amount + ' ' + currency + ' to debit card';
var okText = 'Confirm';
var cancelText = 'Cancel';
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) return;
var dataSrc = {
amount: amount,
currency: currency
}; };
ongoingProcess.set('topup', true, statusChangeHandler);
bitpayCardService.topUp(cardId, dataSrc, function(err, invoiceId) { var createInvoice = function(data, cb) {
bitpayCardService.topUp(cardId, data, function(err, invoiceId) {
if (err) { if (err) {
ongoingProcess.set('topup', false, statusChangeHandler); return cb({
showErrorAndBack('Could not create the invoice', err); title: gettextCatalog.getString('Could not create the invoice'),
return; message: err
});
} }
bitpayCardService.getInvoice(invoiceId, function(err, invoice) { bitpayCardService.getInvoice(invoiceId, function(err, inv) {
if (err) { if (err) {
ongoingProcess.set('topup', false, statusChangeHandler); return cb({
showError('Could not get the invoice', err); title: gettextCatalog.getString('Could not get the invoice'),
return; message: err
});
} }
return cb(null, inv);
});
});
};
var createTx = function(wallet, invoice, message, cb) {
var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null; var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null;
if (!payProUrl) { if (!payProUrl) {
ongoingProcess.set('topup', false, statusChangeHandler); return cb({
showError('Error in Payment Protocol', 'Invalid URL'); title: gettextCatalog.getString('Error in Payment Protocol'),
return; message: gettextCatalog.getString('Invalid URL')
} });
payproService.getPayProDetails(payProUrl, function(err, payProDetails) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Error fetching invoice', err);
return;
} }
var outputs = []; var outputs = [];
var toAddress = payProDetails.toAddress; var toAddress = invoice.bitcoinAddress;
var amountSat = payProDetails.amount; var amountSat = parseInt(invoice.btcDue * 100000000); // BTC to Satoshi
var comment = 'Top up ' + amount + ' ' + currency + ' to Debit Card (' + $scope.cardInfo.lastFourDigits + ')';
outputs.push({ outputs.push({
'toAddress': toAddress, 'toAddress': toAddress,
'amount': amountSat, 'amount': amountSat,
'message': comment 'message': message
}); });
var txp = { var txp = {
toAddress: toAddress, toAddress: toAddress,
amount: amountSat, amount: amountSat,
outputs: outputs, outputs: outputs,
message: comment, message: message,
payProUrl: payProUrl, payProUrl: payProUrl,
excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true,
feeLevel: walletSettings.feeLevel || 'normal' feeLevel: configWallet.settings.feeLevel || 'normal'
}; };
walletService.createTx($scope.wallet, txp, function(err, ctxp) { walletService.createTx(wallet, txp, function(err, ctxp) {
if (err) { if (err) {
ongoingProcess.set('topup', false, statusChangeHandler); return cb({
showError('Could not create transaction', bwcError.msg(err)); title: gettextCatalog.getString('Could not create transaction'),
message: bwcError.msg(err)
});
}
return cb(null, ctxp);
});
};
var calculateAmount = function(wallet, cb) {
// Global variables defined beforeEnter
var a = amount;
var c = currency;
if (useSendMax) {
sendMaxService.getInfo(wallet, function(err, maxValues) {
if (err) {
return cb({
title: null,
message: err
})
}
if (maxValues.amount == 0) {
return cb({message: gettextCatalog.getString('Insufficient funds for fee')});
}
var maxAmountBtc = Number((maxValues.amount / 100000000).toFixed(8));
createInvoice({amount: maxAmountBtc, currency: 'BTC'}, function(err, inv) {
if (err) return cb(err);
var invoiceFeeSat = parseInt((inv.buyerPaidBtcMinerFee * 100000000).toFixed());
var newAmountSat = maxValues.amount - invoiceFeeSat;
if (newAmountSat <= 0) {
return cb({message: gettextCatalog.getString('Insufficient funds for fee')});
}
return cb(null, newAmountSat, 'sat');
});
});
} else {
return cb(null, a, c);
}
};
var initializeTopUp = function(wallet, parsedAmount) {
$scope.amountUnitStr = parsedAmount.amountUnitStr;
var dataSrc = {
amount: parsedAmount.amount,
currency: parsedAmount.currency
};
ongoingProcess.set('loadingTxInfo', true);
createInvoice(dataSrc, function(err, invoice) {
if (err) {
ongoingProcess.set('loadingTxInfo', false);
showErrorAndBack(err.title, err.message);
return; return;
} }
publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) {
ongoingProcess.set('topup', false, statusChangeHandler); 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) { if (err) {
showError('Could not send transaction', err); showErrorAndBack(err.title, err.message);
return; 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;
}
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
}); });
}); });
}, true); // Disable loader
}); $scope.topUpConfirm = function() {
if (!createdTx) {
showError(null, gettextCatalog.getString('Transaction has not been created'));
return;
}
var title = gettextCatalog.getString('Confirm');
var okText = gettextCatalog.getString('OK');
var cancelText = gettextCatalog.getString('Cancel');
popupService.showConfirm(title, message, okText, cancelText, function(ok) {
if (!ok) {
$scope.sendStatus = '';
return;
}
ongoingProcess.set('topup', true, statusChangeHandler);
publishAndSign($scope.wallet, createdTx, function() {}, function(err, txSent) {
if (err) {
ongoingProcess.set('topup', false);
$scope.sendStatus = '';
showError(gettextCatalog.getString('Could not send transaction'), err);
return;
}
ongoingProcess.set('topup', false, statusChangeHandler);
}); });
}); });
}; };
@ -200,29 +301,19 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
$scope.onWalletSelect = function(wallet) { $scope.onWalletSelect = function(wallet) {
$scope.wallet = wallet; $scope.wallet = wallet;
if (sendMax) {
ongoingProcess.set('retrievingInputs', true); ongoingProcess.set('retrievingInputs', true);
sendMaxService.getInfo($scope.wallet, function(err, values) { calculateAmount(wallet, function(err, a, c) {
ongoingProcess.set('retrievingInputs', false); ongoingProcess.set('retrievingInputs', false);
if (err) { if (err) {
showErrorAndBack(null, err); createdTx = message = $scope.totalAmountStr = $scope.amountUnitStr = $scope.wallet = null;
showError(err.title, err.message, function() {
$scope.showWalletSelector();
});
return; return;
} }
var config = configService.getSync().wallet.settings; var parsedAmount = txFormatService.parseAmount(a, c);
var unitName = config.unitName; initializeTopUp(wallet, parsedAmount);
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);
}); });
}
}; };
$scope.goBackHome = function() { $scope.goBackHome = function() {

View file

@ -1,6 +1,6 @@
'use strict'; '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 txId;
var listeners = []; var listeners = [];
@ -14,17 +14,19 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.isShared = $scope.wallet.credentials.n > 1; $scope.isShared = $scope.wallet.credentials.n > 1;
txConfirmNotification.checkIfEnabled(txId, function(res) { txConfirmNotification.checkIfEnabled(txId, function(res) {
$scope.txNotification = { value: res }; $scope.txNotification = {
}); value: res
};
}); });
$scope.$on("$ionicView.afterEnter", function(event) {
updateTx(); updateTx();
listeners = [ listeners = [
$rootScope.$on('bwsEvent', function(e, walletId, type, n) { $rootScope.$on('bwsEvent', function(e, walletId, type, n) {
if (type == 'NewBlock' && n && n.data && n.data.network == 'livenet') { 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() { function updateMemo() {
walletService.getTxNote($scope.wallet, $scope.btx.txid, function(err, note) { walletService.getTxNote($scope.wallet, $scope.btx.txid, function(err, note) {
if (err) { if (err) {
@ -108,7 +102,8 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.btx = txFormatService.processTx(tx); $scope.btx = txFormatService.processTx(tx);
txFormatService.formatAlternativeStr(tx.fees, function(v) { 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') { if ($scope.btx.action != 'invalid') {
@ -117,15 +112,25 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
if ($scope.btx.action == 'moved') $scope.title = gettextCatalog.getString('Moved Funds'); if ($scope.btx.action == 'moved') $scope.title = gettextCatalog.getString('Moved Funds');
} }
$scope.displayAmount = getDisplayAmount($scope.btx.amountStr);
$scope.displayUnit = getDisplayUnit($scope.btx.amountStr);
updateMemo(); updateMemo();
initActionList(); initActionList();
getFiatRate(); getFiatRate();
$timeout(function() {
$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() { $timeout(function() {
$scope.$apply(); $scope.$apply();
}); });
});
});
}); });
}; };
@ -197,7 +202,9 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.txConfirmNotificationChange = function() { $scope.txConfirmNotificationChange = function() {
if ($scope.txNotification.value) { if ($scope.txNotification.value) {
txConfirmNotification.subscribe($scope.wallet, { txid: txId }); txConfirmNotification.subscribe($scope.wallet, {
txid: txId
});
} else { } else {
txConfirmNotification.unsubscribe($scope.wallet, txId); txConfirmNotification.unsubscribe($scope.wallet, txId);
} }

View file

@ -1,6 +1,6 @@
'use strict'; '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 HISTORY_SHOW_LIMIT = 10;
var currentTxHistoryPage = 0; var currentTxHistoryPage = 0;
@ -47,6 +47,21 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.txps = lodash.sortBy(txps, 'createdOn').reverse(); $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) { var updateStatus = function(force) {
$scope.updatingStatus = true; $scope.updatingStatus = true;
$scope.updateStatusError = null; $scope.updateStatusError = null;
@ -72,6 +87,8 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.$apply(); $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, { walletService.getTxHistory($scope.wallet, {
progressFn: progressFn, progressFn: progressFn,
feeLevels: levels,
}, function(err, txHistory) { }, function(err, txHistory) {
$scope.updatingTxHistory = false; $scope.updatingTxHistory = false;
if (err) { if (err) {

View file

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

View file

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

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
angular.module('copayApp.directives') angular.module('copayApp.directives')
.directive('qrScanner', function($state, $rootScope, $log, $ionicHistory) { .directive('qrScanner', function($state, $rootScope, $log, $ionicHistory, platformInfo, scannerService, popupService) {
return { return {
restrict: 'E', restrict: 'E',
@ -9,20 +9,43 @@ angular.module('copayApp.directives')
onScan: "&" onScan: "&"
}, },
replace: true, 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) { 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() { scope.openScanner = function() {
$log.debug('Opening scanner by directive...'); $log.debug('Opening scanner by directive...');
$ionicHistory.nextViewOptions({ $ionicHistory.nextViewOptions({
disableAnimate: true disableAnimate: true
}); });
$state.go('scanner', { passthroughMode: 1 }); $state.go('scanner', {
passthroughMode: 1
});
}; };
var afterEnter = $rootScope.$on('$ionicView.afterEnter', function() { var afterEnter = $rootScope.$on('$ionicView.afterEnter', function() {
if ($rootScope.scanResult) { if ($rootScope.scanResult) {
scope.onScan({ data: $rootScope.scanResult }); scope.onScan({
data: $rootScope.scanResult
});
$rootScope.scanResult = null; $rootScope.scanResult = null;
} }
}); });

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: { scope: {
sendStatus: '=slideSendStatus', sendStatus: '=slideSendStatus',
onConfirm: '&slideOnConfirm', onConfirm: '&slideOnConfirm',
wallet: '=hasWalletChosen' isDisabled: '=isDisabled'
}, },
link: function(scope, element, attrs) { link: function(scope, element, attrs) {

View file

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

View file

@ -112,11 +112,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
.state('starting', { .state('starting', {
url: '/starting', url: '/starting',
template: '<ion-view id="starting"><ion-content>{{starting}}</ion-content></ion-view>', 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>'
controller: function($scope, $log, gettextCatalog) {
$log.info('Starting...');
$scope.starting = gettextCatalog.getString('Starting...');
}
}) })
/* /*
@ -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 * Tabs
@ -1204,10 +1215,12 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
$ionicHistory.goBack(); $ionicHistory.goBack();
} else } else
if ($rootScope.backButtonPressedOnceToExit) { if ($rootScope.backButtonPressedOnceToExit) {
ionic.Platform.exitApp(); navigator.app.exitApp();
} else { } else {
$rootScope.backButtonPressedOnceToExit = true; $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() { $timeout(function() {
$rootScope.backButtonPressedOnceToExit = false; $rootScope.backButtonPressedOnceToExit = false;
}, 3000); }, 3000);

View file

@ -33,6 +33,7 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
* appIdentity: the identity of this app * appIdentity: the identity of this app
* } * }
*/ */
root.pair = function(pairData, pairingReason, cb) { root.pair = function(pairData, pairingReason, cb) {
checkOtp(pairData, function(otp) { checkOtp(pairData, function(otp) {
pairData.otp = otp; pairData.otp = otp;
@ -66,14 +67,19 @@ angular.module('copayApp.services').factory('bitpayAccountService', function($lo
fetchBasicInfo(apiContext, function(err, basicInfo) { fetchBasicInfo(apiContext, function(err, basicInfo) {
if (err) return cb(err); if (err) return cb(err);
var title = gettextCatalog.getString('Add BitPay Account?'); var title = gettextCatalog.getString('Add BitPay Account?');
var msgDetail = 'Add this BitPay account ({{email}})?'; var msg;
if (pairingReason) { if (pairingReason) {
msgDetail = 'To {{reason}} you must first add your BitPay account - {{email}}'; msg = gettextCatalog.getString('To {{reason}} you must first add your BitPay account - {{email}}', {
}
var msg = gettextCatalog.getString(msgDetail, {
reason: pairingReason, reason: pairingReason,
email: pairData.email email: pairData.email
}); });
} else {
msg = gettextCatalog.getString('Add this BitPay account ({{email}})?', {
email: pairData.email
});
}
var ok = gettextCatalog.getString('Add Account'); var ok = gettextCatalog.getString('Add Account');
var cancel = gettextCatalog.getString('Go back'); var cancel = gettextCatalog.getString('Go back');
popupService.showConfirm(title, msg, ok, cancel, function(res) { popupService.showConfirm(title, msg, ok, cancel, function(res) {

View file

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

View file

@ -1,11 +1,11 @@
'use strict'; '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 root = {};
var credentials = {}; var credentials = {};
var isCordova = platformInfo.isCordova; var isCordova = platformInfo.isCordova;
var isNW = platformInfo.isNW; var isNW = platformInfo.isNW;
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova; var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
root.priceSensitivity = [{ root.priceSensitivity = [{
value: 0.5, 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() { root.getSignupUrl = function() {
return credentials.HOST + '/signup'; 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 _refreshToken = function(refreshToken, cb) {
var req = { var req = {
method: 'POST', method: 'POST',
@ -657,11 +681,24 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) { var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) {
if (!tx) return; if (!tx) return;
var desc = appConfigService.nameCase + ' Wallet'; var desc = appConfigService.nameCase + ' Wallet';
_getNetAmount(tx.amount.amount, function(err, amountBTC, feeBTC) {
if (err) {
_savePendingTransaction(tx, {
status: 'error',
error: err
}, function(err) {
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
return;
}
var data = { var data = {
to: tx.toAddr, to: tx.toAddr,
amount: tx.amount.amount, amount: amountBTC,
currency: tx.amount.currency, currency: tx.amount.currency,
description: desc description: desc,
fee: feeBTC
}; };
root.sendTo(accessToken, accountId, data, function(err, res) { root.sendTo(accessToken, accountId, data, function(err, res) {
if (err) { if (err) {
@ -695,6 +732,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
}); });
} }
}); });
});
}; };
var _updateCoinbasePendingTransactions = function(obj /*, …*/ ) { var _updateCoinbasePendingTransactions = function(obj /*, …*/ ) {

View file

@ -1,8 +1,10 @@
'use strict'; '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 root = {};
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
var defaultConfig = { var defaultConfig = {
// wallet limits // wallet limits
limits: { limits: {
@ -73,7 +75,7 @@ angular.module('copayApp.services').factory('configService', function(storageSer
}, },
hideNextSteps: { hideNextSteps: {
enabled: false, enabled: isWindowsPhoneApp ? true : false,
}, },
rates: { rates: {

View file

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

View file

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

View file

@ -3,16 +3,17 @@
angular.module('copayApp.services').service('popupService', function($log, $ionicPopup, platformInfo, gettextCatalog) { angular.module('copayApp.services').service('popupService', function($log, $ionicPopup, platformInfo, gettextCatalog) {
var isCordova = platformInfo.isCordova; var isCordova = platformInfo.isCordova;
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
/*************** Ionic ****************/ /*************** Ionic ****************/
var _ionicAlert = function(title, message, cb, buttonName) { var _ionicAlert = function(title, message, cb, okText) {
if (!cb) cb = function() {}; if (!cb) cb = function() {};
$ionicPopup.alert({ $ionicPopup.alert({
title: title, title: title,
subTitle: message, subTitle: message,
okType: 'button-clear button-positive', okType: 'button-clear button-positive',
okText: buttonName || gettextCatalog.getString('OK'), okText: okText || gettextCatalog.getString('OK'),
}).then(cb); }).then(cb);
}; };
@ -45,9 +46,11 @@ angular.module('copayApp.services').service('popupService', function($log, $ioni
/*************** Cordova ****************/ /*************** Cordova ****************/
var _cordovaAlert = function(title, message, cb, buttonName) { var _cordovaAlert = function(title, message, cb, okText) {
if (!cb) cb = function() {}; 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) { 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'); okText = okText || gettextCatalog.getString('OK');
cancelText = cancelText || gettextCatalog.getString('Cancel'); cancelText = cancelText || gettextCatalog.getString('Cancel');
title = title ? title : '';
navigator.notification.confirm(message, onConfirm, title, [cancelText, okText]); 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); if (results.buttonIndex == 1) return cb(results.input1);
else return cb(); 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) * @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; var message = (msg && msg.message) ? msg.message : msg;
$log.warn(title ? (title + ': ' + message) : message); $log.warn(title ? (title + ': ' + message) : message);
if (isCordova) if (isCordova)
_cordovaAlert(title, message, cb, buttonName); _cordovaAlert(title, message, cb, okText);
else 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 ||  {}; opts = opts ||  {};
if (isCordova && !opts.forceHTMLPrompt) if (isCordova && !isWindowsPhoneApp && !opts.forceHTMLPrompt)
_cordovaPrompt(title, message, opts, cb); _cordovaPrompt(title, message, opts, cb);
else else
_ionicPrompt(title, message, opts, cb); _ionicPrompt(title, message, opts, cb);

View file

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

View file

@ -112,6 +112,7 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
// This could be much cleaner with a Promise API // This could be much cleaner with a Promise API
// (needs a polyfill for some platforms) // (needs a polyfill for some platforms)
var initializeCompleted = false; var initializeCompleted = false;
function _completeInitialization(status, callback) { function _completeInitialization(status, callback) {
_checkCapabilities(status); _checkCapabilities(status);
initializeCompleted = true; initializeCompleted = true;
@ -222,6 +223,7 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
} else { } else {
QRScanner.enableLight(_handleResponse); QRScanner.enableLight(_handleResponse);
} }
function _handleResponse(err, status) { function _handleResponse(err, status) {
if (err) { if (err) {
$log.error(err); $log.error(err);
@ -242,6 +244,7 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
*/ */
this.toggleCamera = function(callback) { this.toggleCamera = function(callback) {
var nextCamera = backCamera ? 1 : 0; var nextCamera = backCamera ? 1 : 0;
function cameraToString(index) { function cameraToString(index) {
return index === 1 ? 'front' : 'back'; // front = 1, back = 0 return index === 1 ? 'front' : 'back'; // front = 1, back = 0
} }
@ -260,4 +263,15 @@ angular.module('copayApp.services').service('scannerService', function($log, $ti
$log.debug('Attempting to open device settings...'); $log.debug('Attempting to open device settings...');
QRScanner.openSettings(); 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) { 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); if (err) return cb(err);
var config = configService.getSync().wallet; 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; 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) { root.formatToUSD = function(satoshis, cb) {
if (isNaN(satoshis)) return; if (isNaN(satoshis)) return;
var val = function() { var val = function() {
@ -93,6 +113,11 @@ angular.module('copayApp.services').factory('txFormatService', function($filter,
tx.alternativeAmountStr = root.formatAlternativeStr(tx.amount); tx.alternativeAmountStr = root.formatAlternativeStr(tx.amount);
tx.feeStr = root.formatAmountStr(tx.fee || tx.fees); 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; return tx;
}; };
@ -164,9 +189,15 @@ angular.module('copayApp.services').factory('txFormatService', function($filter,
var alternativeIsoCode = config.alternativeIsoCode; var alternativeIsoCode = config.alternativeIsoCode;
// If fiat currency // If fiat currency
if (currency != 'bits' && currency != 'BTC') { if (currency != 'bits' && currency != 'BTC' && currency != 'sat') {
amountUnitStr = $filter('formatFiatAmount')(amount) + ' ' + currency; amountUnitStr = $filter('formatFiatAmount')(amount) + ' ' + currency;
amountSat = rateService.fromFiat(amount, currency).toFixed(0); 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 { } else {
amountSat = parseInt((amount * unitToSatoshi).toFixed(0)); amountSat = parseInt((amount * unitToSatoshi).toFixed(0));
amountUnitStr = root.formatAmountStr(amountSat); amountUnitStr = root.formatAmountStr(amountSat);

View file

@ -1,7 +1,12 @@
'use strict'; '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) { 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 = {}; var root = {};
@ -401,6 +406,11 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
var progressFn = opts.progressFn || function() {}; var progressFn = opts.progressFn || function() {};
var foundLimitTx = false; var foundLimitTx = false;
if (opts.feeLevels) {
opts.lowAmount = root.getLowAmount(wallet, opts.feeLevels);
}
var fixTxsUnit = function(txs) { var fixTxsUnit = function(txs) {
if (!txs || !txs[0] || !txs[0].amountStr) return; 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) $log.debug('Fixing Tx Cache Unit to:' + name)
lodash.each(txs, function(tx) { lodash.each(txs, function(tx) {
tx.amountStr = txFormatService.formatAmount(tx.amount) + name; tx.amountStr = txFormatService.formatAmount(tx.amount) + name;
tx.feeStr = txFormatService.formatAmount(tx.fees) + 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() { updateNotes(function() {
// <HACK> // <HACK>
@ -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) { root.getAddress = function(wallet, forceNew, cb) {
storageService.getLastAddress(wallet.id, function(err, addr) { storageService.getLastAddress(wallet.id, function(err, addr) {
if (err) return cb(err); if (err) return cb(err);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,10 +31,6 @@
width: 35px; width: 35px;
} }
span {
text-transform: capitalize;
}
.big-icon-svg { .big-icon-svg {
margin-right: 0.6rem; margin-right: 0.6rem;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<ion-view> <ion-view show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title> <ion-nav-title>
{{'Recent Transactions'|translate}} {{'Recent Transactions'|translate}}

View file

@ -1,4 +1,4 @@
<ion-view id="view-add"> <ion-view id="view-add" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Add wallet' | translate}}</ion-nav-title> <ion-nav-title>{{'Add wallet' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="addresses" class="addr"> <ion-view id="addresses" class="addr" hide-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Wallet Addresses' | translate}}</ion-nav-title> <ion-nav-title>{{'Wallet Addresses' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>
@ -70,6 +70,34 @@
<div class="addr-balance">{{w.balanceStr}}</div> <div class="addr-balance">{{w.balanceStr}}</div>
</div> </div>
</div> </div>
<div ng-if="allUtxosNb">
<div class="item item-divider" translate>
Wallet Inputs
</div>
<div class="item" >
<span translate> Total wallet inputs </span>
<div class="addr-path">
{{allUtxosNb}} [{{allUtxosSum}}]
</div>
</div>
<div class="item" >
<span translate> Low amount inputs </span>
<div class="addr-path">
{{lowUtxosNb}} [{{ lowUtxosSum }}]
</div>
</div>
<div class="item" >
<span translate> Approximate Bitcoin network fee to transfer wallet's balance (with normal priority) </span>
<div class="addr-path">
{{minFeePer}} [{{minFee}}]
</div>
</div>
</div>
</div> </div>
</div> </div>
</ion-content> </ion-content>

View file

@ -1,4 +1,4 @@
<ion-view id="advanced-settings" class="settings"> <ion-view id="advanced-settings" class="settings" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Advanced Settings' | translate}}</ion-nav-title> <ion-nav-title>{{'Advanced Settings' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>
@ -16,7 +16,7 @@
<div class="item item-divider"></div> <div class="item item-divider"></div>
<ion-toggle class="has-comment" ng-show="!isWP" ng-model="recentTransactionsEnabled.value" toggle-class="toggle-balanced" ng-change="recentTransactionsChange()"> <ion-toggle class="has-comment" ng-model="recentTransactionsEnabled.value" toggle-class="toggle-balanced" ng-change="recentTransactionsChange()">
<span class="toggle-label" translate>Recent Transaction Card</span> <span class="toggle-label" translate>Recent Transaction Card</span>
</ion-toggle> </ion-toggle>
<div class="comment" translate> <div class="comment" translate>
@ -25,7 +25,7 @@
<div class="item item-divider"></div> <div class="item item-divider"></div>
<ion-toggle ng-model="hideNextSteps.value" toggle-class="toggle-balanced" ng-change="nextStepsChange()"> <ion-toggle ng-model="hideNextSteps.value" ng-if="!isWindowsPhoneApp" toggle-class="toggle-balanced" ng-change="nextStepsChange()">
<span class="toggle-label" translate>Hide Next Steps Card</span> <span class="toggle-label" translate>Hide Next Steps Card</span>
</ion-toggle> </ion-toggle>
</div> </div>

View file

@ -1,4 +1,4 @@
<ion-view hide-tabs id="addresses" class="addr"> <ion-view id="addresses" class="addr" hide-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'All Addresses' | translate}}</ion-nav-title> <ion-nav-title>{{'All Addresses' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view> <ion-view show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view> <ion-view show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="bitpayCard"> <ion-view id="bitpayCard" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>
@ -48,7 +48,7 @@
<i class="icon ion-ios-arrow-thin-up get-started__arrow"></i> <i class="icon ion-ios-arrow-thin-up get-started__arrow"></i>
<h1 translate>Get started</h1> <h1 translate>Get started</h1>
<div class="get-started__text" translate> <div class="get-started__text" translate>
Your BitPay Card is ready. Add funds to your card to start using your card at stores and ATMs worldwide. Your BitPay Card is ready. Add funds to your card to start using it at stores and ATMs worldwide.
</div> </div>
</div> </div>
<div class="list" ng-show="!bitpayCard.getStarted"> <div class="list" ng-show="!bitpayCard.getStarted">
@ -64,8 +64,8 @@
</label> </label>
<div ng-if="bitpayCard.bitpayCardTransactionHistoryConfirming[0]"> <div ng-if="bitpayCard.bitpayCardTransactionHistoryConfirming[0]">
<label class="item status-label" ng-click="bitpayCard.openExternalLink('https://help.bitpay.com/bitpay-card/why-do-you-require-one-blockchain-confirmation-for-bitpay-card-loads')"> <label class="item status-label" ng-click="bitpayCard.openExternalLink('https://help.bitpay.com/bitpay-card/why-do-you-require-one-blockchain-confirmation-for-bitpay-card-loads')">
<div translate> <div>
Confirming <span translate>Confirming</span>
<i class="icon"> <i class="icon">
<img src="img/icon-help-support.svg" class="bg"/> <img src="img/icon-help-support.svg" class="bg"/>
</i> </i>
@ -77,8 +77,8 @@
</div> </div>
<div ng-if="bitpayCard.bitpayCardTransactionHistoryPreAuth[0]"> <div ng-if="bitpayCard.bitpayCardTransactionHistoryPreAuth[0]">
<label class="item status-label" ng-click="bitpayCard.openExternalLink('https://help.bitpay.com/bitpay-card/why-was-i-overcharged-on-my-bitpay-card-account-why-is-there-a-hold-on-my-account')"> <label class="item status-label" ng-click="bitpayCard.openExternalLink('https://help.bitpay.com/bitpay-card/why-was-i-overcharged-on-my-bitpay-card-account-why-is-there-a-hold-on-my-account')">
<div translate> <div>
Pre-Auth Holds <span translate>Pre-Auth Holds</span>
<i class="icon"> <i class="icon">
<img src="img/icon-help-support.svg" class="bg"/> <img src="img/icon-help-support.svg" class="bg"/>
</i> </i>
@ -89,7 +89,7 @@
</div> </div>
</div> </div>
<div ng-if="bitpayCard.bitpayCardTransactionHistoryCompleted[0]"> <div ng-if="bitpayCard.bitpayCardTransactionHistoryCompleted[0]">
<label class="item status-label"> <label class="item status-label" ng-if="bitpayCard.bitpayCardTransactionHistoryPreAuth[0] || bitpayCard.bitpayCardTransactionHistoryConfirming[0]">
<div translate> <div translate>
Completed Completed
</div> </div>

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<ion-view id="buy-and-sell"> <ion-view id="buy-and-sell" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="coinbase"> <ion-view id="coinbase" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

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

View file

@ -1,5 +1,4 @@
<ion-view id="copayers-invitation" hide-tabs>
<ion-view id="copayers-invitation">
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="export"> <ion-view id="export" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Export wallet' | translate}}</ion-nav-title> <ion-nav-title>{{'Export wallet' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="glidera"> <ion-view id="glidera" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view> <ion-view show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view id="import" class="settings"> <ion-view id="import" class="settings" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Import Wallet' | translate}}</ion-nav-title> <ion-nav-title>{{'Import Wallet' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>

View file

@ -1,5 +1,3 @@
<div class="list card">
<div class="item item-icon-right item-heading"> <div class="item item-icon-right item-heading">
<span translate>Cards</span> <span translate>Cards</span>
<a ui-sref="tabs.bitpayCardIntro"><i class="icon ion-ios-plus-empty list-add-button"></i></a> <a ui-sref="tabs.bitpayCardIntro"><i class="icon ion-ios-plus-empty list-add-button"></i></a>
@ -16,6 +14,3 @@
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
</div> </div>
</div>

View file

@ -1,5 +1,4 @@
<div ng-controller="buyAndSellCardController">
<div class="list card" ng-controller="buyAndSellCardController">
<div class="item item-sub item-icon-right item-heading"> <div class="item item-sub item-icon-right item-heading">
<span translate>Buy bitcoin</span> <span translate>Buy bitcoin</span>
<a ui-sref="tabs.buyandsell"><i class="icon ion-ios-plus-empty list-add-button"></i></a> <a ui-sref="tabs.buyandsell"><i class="icon ion-ios-plus-empty list-add-button"></i></a>

View file

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

View file

@ -1,4 +1,4 @@
<div class="list card" ng-controller="homeIntegrationsController"> <div ng-controller="homeIntegrationsController">
<div class="item item-icon-right item-heading" ng-click="toggle()" > <div class="item item-icon-right item-heading" ng-click="toggle()" >
<span translate>Services</span> <span translate>Services</span>
<i class="icon bp-arrow-up" ng-show="!hide"></i> <i class="icon bp-arrow-up" ng-show="!hide"></i>
@ -16,4 +16,3 @@
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="list card" ng-controller="nextStepsController"> <div ng-controller="nextStepsController">
<div class="item item-icon-right item-heading" ng-click="toggle()" > <div class="item item-icon-right item-heading" ng-click="toggle()" >
<span translate>Next steps</span> <span translate>Next steps</span>
<i class="icon bp-arrow-up" ng-show="!hide"></i> <i class="icon bp-arrow-up" ng-show="!hide"></i>
@ -16,4 +16,3 @@
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
<div <div
class="slide-success__background" class="slide-success__background"
ng-class="{'fill-screen': fillScreen}"> ng-class="{'fill-screen': fillScreen, 'slide-success__windows-background': isWindowsPhoneApp}">
</div> </div>
<div ng-disabled="wallet" class="slide-success__content"> <div ng-disabled="wallet" class="slide-success__content">

View file

@ -26,6 +26,11 @@
<i class="icon"><img src="img/icon-warning.png" width="20px"></i> <i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span class="comment" translate>Low fees</span> <span class="comment" translate>Low fees</span>
</div> </div>
<div class="low-fees" ng-if="btx.lowAmount">
<i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span class="comment" translate>Amount too low to spend</span>
</div>
</div> </div>
<div ng-show="btx.action == 'sent'" class="ellipsis"> <div ng-show="btx.action == 'sent'" class="ellipsis">

View file

@ -1,4 +1,4 @@
<ion-view id="join" class="settings"> <ion-view id="join" class="settings" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-back-button> <ion-nav-back-button>
</ion-nav-back-button> </ion-nav-back-button>

View file

@ -1,4 +1,4 @@
<ion-view class="settings"> <ion-view class="settings" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Startup Lock' | translate}}</ion-nav-title> <ion-nav-title>{{'Startup Lock' | translate}}</ion-nav-title>
<ion-nav-back-button> <ion-nav-back-button>

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<ion-modal-view id="pin" ng-controller="pinController"> <ion-modal-view id="pin" ng-controller="pinController">
<ion-header-bar align-title="center" class="bar-royal"> <ion-header-bar align-title="center" class="bar-royal">
<button ng-if="action != 'check'" class="button button-back button-clear" ng-click="hideModal()"> <button ng-if="action != 'check'" class="button button-back button-clear" ng-click="hideModal()" translate>
Close Close
</button> </button>
</ion-header-bar> </ion-header-bar>

View file

@ -19,7 +19,7 @@
<span translate>Sending</span> <span translate>Sending</span>
</div> </div>
<div class="amount-label"> <div class="amount-label">
<div class="amount">{{displayAmount}} <span class="unit">{{displayUnit}}</span></div> <div class="amount">{{tx.amountValueStr}} <span class="unit">{{tx.amountUnitStr}}</span></div>
<div class="alternative" ng-show="tx.alternativeAmountStr">{{tx.alternativeAmountStr}}</div> <div class="alternative" ng-show="tx.alternativeAmountStr">{{tx.alternativeAmountStr}}</div>
</div> </div>
@ -97,10 +97,11 @@
{{tx.message}} {{tx.message}}
</span> </span>
</div> </div>
<div class="item single-line"> <div class="item">
<span class="label" translate>Fee</span> <span class="label">{{'Fee:' | translate}} {{tx.feeLevelStr | translate}}</span>
<span class="item-note"> <span class="m10l">{{tx.feeStr || '...'}}</span>
{{tx.feeStr}} <span class="item-note m10l">
<span>{{tx.feeFiatStr || '...'}}&nbsp;<span class="fee-rate" ng-if="tx.feeRateStr" translate>- {{tx.feeRateStr}} of the transaction</span></span>
</span> </span>
</div> </div>
@ -169,13 +170,13 @@
<click-to-accept <click-to-accept
ng-click="onConfirm(statusChangeHandler)" ng-click="onConfirm(statusChangeHandler)"
ng-if="tx.pendingForUs && canSign && !paymentExpired && !isCordova" ng-if="tx.pendingForUs && canSign && !paymentExpired && (!isCordova || isWindowsPhoneApp)"
click-send-status="sendStatus" click-send-status="sendStatus"
has-wallet-chosen="true"> has-wallet-chosen="true">
{{buttonText}} {{buttonText}}
</click-to-accept> </click-to-accept>
<slide-to-accept <slide-to-accept
ng-if="tx.pendingForUs && canSign && !paymentExpired && isCordova" ng-if="tx.pendingForUs && canSign && !paymentExpired && isCordova && !isWindowsPhoneApp"
slide-on-confirm="onConfirm()" slide-on-confirm="onConfirm()"
slide-send-status="sendStatus" slide-send-status="sendStatus"
has-wallet-chosen="true"> has-wallet-chosen="true">
@ -184,6 +185,6 @@
<slide-to-accept-success <slide-to-accept-success
slide-success-show="sendStatus === 'success'" slide-success-show="sendStatus === 'success'"
slide-success-on-confirm="onSuccessConfirm()"> slide-success-on-confirm="onSuccessConfirm()">
{{'Payment Sent' | translate}} {{successText}}
</slide-to-accept-success> </slide-to-accept-success>
</ion-modal-view> </ion-modal-view>

View file

@ -1,48 +0,0 @@
<div
class="topbar-container"
ng-include="'views/includes/topbar.html'"
ng-init="titleSection='Choose wallet'; closeToHome = true">
</div>
<div class="content p20v row payment-uri" ng-controller="paymentUriController as payment">
<div class="large-12 columns" ng-init="payment.init()">
<div class="panel text-center" ng-if="!payment.uri">
<h1 translate>Bitcoin URI is NOT valid!</h1>
</div>
<div ng-if="payment.uri" ng-init="payment.getWallets(payment.uri.network)">
<h1 translate>Make a payment to</h1>
<div class="panel size-14">
<div class="ellipsis"><b translate>Address</b>: {{payment.uri.address.toString()}}</div>
<div ng-show="payment.uri.amount"><b translate>Amount</b>: {{payment.uri.amount}}</div>
<div ng-show="payment.uri.message"><b translate>Message</b>: {{payment.uri.message}}</div>
<div ng-show="payment.uri.network == 'testnet'"><b translate>Network</b>: {{payment.uri.network}}</div>
</div>
<div ng-if="!wallets || !wallets.length">
<div class="box-notification">
<span class="text-warning">
<b translate>There are no wallets to make this payment</b>
<span ng-show="payment.uri.network == 'testnet'">[testnet]</span>
</span>
</div>
</div>
<div ng-if="wallets.length">
<h2 translate>Select a wallet</h2>
<ul class="no-bullet">
<li class="panel" ng-repeat="w in wallets">
<a ng-click="payment.selectWallet(w.id)">
<div class="avatar-wallet"
ng-class="{'wallet-background-color-default': !w.color}" ng-style="{'background-color':w.color}">
<i class="icon-wallet size-21"></i>
</div>
<div class="ellipsis">{{w.name || w.id}}</div>
<div class="size-12">{{w.m}} of {{w.n}}
<span ng-show="w.network=='testnet'">[Testnet]</span>
</div>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
<ion-view class="settings"> <ion-view class="settings" show-tabs>
<ion-nav-bar class="bar-royal"> <ion-nav-bar class="bar-royal">
<ion-nav-title> <ion-nav-title>
{{'Wallet Settings'|translate}} {{'Wallet Settings'|translate}}
@ -18,7 +18,7 @@
</span> </span>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
<a class="item item-icon-right" ui-sref="tabs.preferences.preferencesColor"> <a ng-if="!isWindowsPhoneApp" class="item item-icon-right" ui-sref="tabs.preferences.preferencesColor">
<span translate>Color</span> <span translate>Color</span>
<span class="item-note"> <span class="item-note">
<span class="settings-color-block" <span class="settings-color-block"

Some files were not shown because too many files have changed in this diff Show more