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

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

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,305 +42,346 @@ angular.module('copayApp.controllers').controller('confirmController', function(
$ionicConfig.views.swipeBackEnabled(false); $ionicConfig.views.swipeBackEnabled(false);
}); });
function exitWithError(err) {
$log.info('Error setting wallet selector:' + err);
popupService.showAlert(gettextCatalog.getString(), bwcError.msg(err), function() {
$ionicHistory.nextViewOptions({
disableAnimate: true,
historyRoot: true
});
$ionicHistory.clearHistory();
$state.go('tabs.send');
});
};
function setNoWallet(msg) {
$scope.wallet = null;
$scope.noWalletMessage = gettextCatalog.getString(msg);
$log.warn('Not ready to make the payment:' + msg);
$timeout(function() {
$scope.$apply();
});
};
$scope.$on("$ionicView.beforeEnter", function(event, data) { $scope.$on("$ionicView.beforeEnter", function(event, data) {
toAmount = data.stateParams.toAmount; function setWalletSelector(network, minAmount, cb) {
cachedSendMax = {};
// no min amount? (sendMax) => look for no empty wallets
minAmount = minAmount || 1;
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: network
});
if (!$scope.wallets || !$scope.wallets.length) {
setNoWallet('No wallets available');
return cb();
}
var filteredWallets = [];
var index = 0;
var walletsUpdated = 0;
lodash.each($scope.wallets, function(w) {
walletService.getStatus(w, {}, function(err, status) {
if (err || !status) {
$log.error(err);
} else {
walletsUpdated++;
w.status = status;
if (!status.availableBalanceSat)
$log.debug('No balance available in: ' + w.name);
if (status.availableBalanceSat > minAmount) {
filteredWallets.push(w);
}
}
if (++index == $scope.wallets.length) {
if (!walletsUpdated)
return cb('Could not update any wallet');
if (lodash.isEmpty(filteredWallets)) {
setNoWallet('Insufficent funds');
}
$scope.wallets = lodash.clone(filteredWallets);
return cb();
}
});
});
};
// Setup $scope
// Grab stateParams
tx = {
toAmount: parseInt(data.stateParams.toAmount),
sendMax: data.stateParams.useSendMax == 'true' ? true : false,
toAddress: data.stateParams.toAddress,
description: data.stateParams.description,
paypro: data.stateParams.paypro,
feeLevel: configFeeLevel,
spendUnconfirmed: walletConfig.spendUnconfirmed,
// Vanity tx info (not in the real tx)
recipientType: data.stateParams.recipientType || null,
toName: data.stateParams.toName,
toEmail: data.stateParams.toEmail,
toColor: data.stateParams.toColor,
network: (new bitcore.Address(data.stateParams.toAddress)).network.name,
txp: {},
};
// Other Scope vars
$scope.isCordova = isCordova;
$scope.isWindowsPhoneApp = isWindowsPhoneApp;
$scope.showAddress = false; $scope.showAddress = false;
$scope.useSendMax = data.stateParams.useSendMax == 'true' ? true : false;
$scope.recipientType = data.stateParams.recipientType || null; updateTx(tx, null, {}, function() {
$scope.toAddress = data.stateParams.toAddress;
$scope.toName = data.stateParams.toName; $scope.walletSelectorTitle = gettextCatalog.getString('Send from');
$scope.toEmail = data.stateParams.toEmail;
$scope.toColor = data.stateParams.toColor; setWalletSelector(tx.network, tx.toAmount, function(err) {
$scope.description = data.stateParams.description; if (err) {
$scope.paypro = data.stateParams.paypro; return exitWithError('Could not update wallets');
$scope.insufficientFunds = false; }
$scope.noMatchingWallet = false;
$scope.paymentExpired = { if ($scope.wallets.length > 1) {
value: false $scope.showWalletSelector();
}; } else if ($scope.wallets.length) {
$scope.remainingTimeStr = { setWallet($scope.wallets[0], tx);
value: null }
}; });
$scope.network = (new bitcore.Address($scope.toAddress)).network.name;
setFee(); });
resetValues();
setwallets();
applyButtonText();
}); });
function setFee(customFeeLevel, cb) {
feeService.getCurrentFeeValue($scope.network, customFeeLevel, function(err, currentFeePerKb) { function getSendMaxInfo(tx, wallet, cb) {
var config = configService.getSync().wallet; if (!tx.sendMax) return cb();
var configFeeLevel = (config.settings && config.settings.feeLevel) ? config.settings.feeLevel : 'normal';
feePerKb = currentFeePerKb; //ongoingProcess.set('retrievingInputs', true);
feeLevel = customFeeLevel ? customFeeLevel : configFeeLevel; walletService.getSendMaxInfo(wallet, {
$scope.feeLevel = feeService.feeOpts[feeLevel]; feePerKb: tx.feeRate,
if (cb) return cb(); excludeUnconfirmedUtxos: !tx.spendUnconfirmed,
returnInputs: true,
}, cb);
};
function getTxp(tx, wallet, dryRun, cb) {
// ToDo: use a credential's (or fc's) function for this
if (tx.description && !wallet.credentials.sharedEncryptingKey) {
var msg = gettextCatalog.getString('Could not add message to imported wallet without shared encrypting key');
$log.warn(msg);
return setSendError(msg);
}
if (tx.toAmount > Number.MAX_SAFE_INTEGER) {
var msg = gettextCatalog.getString('Amount too big');
$log.warn(msg);
return setSendError(msg);
}
var txp = {};
txp.outputs = [{
'toAddress': tx.toAddress,
'amount': tx.toAmount,
'message': tx.description
}];
if (tx.sendMaxInfo) {
txp.inputs = tx.sendMaxInfo.inputs;
txp.fee = tx.sendMaxInfo.fee;
} else {
txp.feeLevel = tx.feeLevel;
}
txp.message = tx.description;
if (tx.paypro) {
txp.payProUrl = tx.paypro.url;
}
txp.excludeUnconfirmedUtxos = !tx.spendUnconfirmed;
txp.dryRun = dryRun;
walletService.createTx(wallet, txp, function(err, ctxp) {
if (err) {
setSendError(err);
return cb(err);
}
return cb(null, ctxp);
});
};
function updateTx(tx, wallet, opts, cb) {
if (opts.clearCache) {
tx.txp = {};
}
$scope.tx = tx;
function updateAmount() {
if (!tx.toAmount) return;
// Amount
tx.amountStr = txFormatService.formatAmountStr(tx.toAmount);
tx.amountValueStr = tx.amountStr.split(' ')[0];
tx.amountUnitStr = tx.amountStr.split(' ')[1];
txFormatService.formatAlternativeStr(tx.toAmount, function(v) {
tx.alternativeAmountStr = v;
});
}
updateAmount();
refresh();
// 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() { function useSelectedWallet() {
if (!$scope.useSendMax) displayValues();
if (!$scope.useSendMax) {
showAmount(tx.toAmount);
}
$scope.onWalletSelect($scope.wallet); $scope.onWalletSelect($scope.wallet);
} }
function applyButtonText(multisig) { function setButtonText(isMultisig, isPayPro) {
$scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' '; $scope.buttonText = gettextCatalog.getString(isCordova && !isWindowsPhoneApp ? 'Slide' : 'Click') + ' ';
if ($scope.paypro) { if (isPayPro) {
$scope.buttonText += gettextCatalog.getString('to pay'); $scope.buttonText += gettextCatalog.getString('to pay');
} else if (multisig) { } else if (isMultisig) {
$scope.buttonText += gettextCatalog.getString('to accept'); $scope.buttonText += gettextCatalog.getString('to accept');
} else } else
$scope.buttonText += gettextCatalog.getString('to send'); $scope.buttonText += gettextCatalog.getString('to send');
}; };
function setwallets() {
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: $scope.network
});
if (!$scope.wallets || !$scope.wallets.length) {
$scope.noMatchingWallet = true;
displayValues();
$log.warn('No ' + $scope.network + ' wallets to make the payment');
$timeout(function() {
$scope.$apply();
});
return;
}
var filteredWallets = [];
var index = 0;
var enoughFunds = false;
var walletsUpdated = 0;
lodash.each($scope.wallets, function(w) {
walletService.getStatus(w, {}, function(err, status) {
if (err || !status) {
$log.error(err);
} else {
walletsUpdated++;
w.status = status;
if (!status.availableBalanceSat) $log.debug('No balance available in: ' + w.name);
if (status.availableBalanceSat > toAmount) {
filteredWallets.push(w);
enoughFunds = true;
}
}
if (++index == $scope.wallets.length) {
if (!lodash.isEmpty(filteredWallets)) {
$scope.wallets = lodash.clone(filteredWallets);
if ($scope.useSendMax) {
if ($scope.wallets.length > 1)
$scope.showWalletSelector();
else {
$scope.wallet = $scope.wallets[0];
$scope.getSendMaxInfo();
}
} else initConfirm();
} else {
// Were we able to update any wallet?
if (walletsUpdated) {
if (!enoughFunds) $scope.insufficientFunds = true;
displayValues();
$log.warn('No wallet available to make the payment');
} else {
popupService.showAlert(gettextCatalog.getString('Could not update wallets'), bwcError.msg(err), function() {
$ionicHistory.nextViewOptions({
disableAnimate: true,
historyRoot: true
});
$ionicHistory.clearHistory();
$state.go('tabs.send');
});
}
}
$timeout(function() {
$scope.$apply();
});
}
});
});
};
$scope.toggleAddress = function() { $scope.toggleAddress = function() {
$scope.showAddress = !$scope.showAddress; $scope.showAddress = !$scope.showAddress;
}; };
var initConfirm = function() {
if ($scope.paypro) _paymentTimeControl($scope.paypro.expires);
displayValues(); function showSendMaxWarning(sendMaxInfo) {
if ($scope.wallets.length > 1) $scope.showWalletSelector();
else setWallet($scope.wallets[0]);
$timeout(function() {
$scope.$apply();
});
};
function displayValues() { function verifyExcludedUtxos() {
toAmount = parseInt(toAmount); var warningMsg = [];
$scope.amountStr = txFormatService.formatAmountStr(toAmount); if (sendMaxInfo.utxosBelowFee > 0) {
$scope.displayAmount = getDisplayAmount($scope.amountStr); warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", {
$scope.displayUnit = getDisplayUnit($scope.amountStr); amountBelowFeeStr: txFormatService.formatAmountStr(sendMaxInfo.amountBelowFee)
txFormatService.formatAlternativeStr(toAmount, function(v) { }));
$scope.alternativeAmountStr = v;
});
};
function resetValues() {
$scope.displayAmount = $scope.displayUnit = $scope.fee = $scope.feeFiat = $scope.feeRateStr = $scope.alternativeAmountStr = $scope.insufficientFunds = $scope.noMatchingWallet = null;
$scope.showAddress = false;
};
$scope.getSendMaxInfo = function() {
resetValues();
var config = configService.getSync().wallet;
ongoingProcess.set('retrievingInputs', true);
walletService.getSendMaxInfo($scope.wallet, {
feePerKb: feePerKb,
excludeUnconfirmedUtxos: !config.spendUnconfirmed,
returnInputs: true,
}, function(err, resp) {
ongoingProcess.set('retrievingInputs', false);
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
} }
if (resp.amount == 0) { if (sendMaxInfo.utxosAboveMaxSize > 0) {
$scope.insufficientFunds = true; warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", {
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee')); amountAboveMaxSizeStr: txFormatService.formatAmountStr(sendMaxInfo.amountAboveMaxSize)
return; }));
} }
return warningMsg.join('\n');
};
$scope.sendMaxInfo = { var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", {
sendMax: true, fee: txFormatService.formatAmountStr(sendMaxInfo.fee)
amount: resp.amount,
inputs: resp.inputs,
fee: resp.fee,
feePerKb: feePerKb,
};
cachedSendMax[$scope.wallet.id] = $scope.sendMaxInfo;
var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", {
fee: txFormatService.formatAmountStr(resp.fee)
});
var warningMsg = verifyExcludedUtxos();
if (!lodash.isEmpty(warningMsg))
msg += '\n' + warningMsg;
popupService.showAlert(null, msg, function() {
setSendMaxValues(resp);
createTx($scope.wallet, true, function(err, txp) {
if (err) return;
cachedTxp[$scope.wallet.id] = txp;
apply(txp);
});
});
function verifyExcludedUtxos() {
var warningMsg = [];
if (resp.utxosBelowFee > 0) {
warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", {
amountBelowFeeStr: txFormatService.formatAmountStr(resp.amountBelowFee)
}));
}
if (resp.utxosAboveMaxSize > 0) {
warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", {
amountAboveMaxSizeStr: txFormatService.formatAmountStr(resp.amountAboveMaxSize)
}));
}
return warningMsg.join('\n');
};
}); });
}; var warningMsg = verifyExcludedUtxos();
function setSendMaxValues(data) { if (!lodash.isEmpty(warningMsg))
resetValues(); msg += '\n' + warningMsg;
var config = configService.getSync().wallet;
var unitToSatoshi = config.settings.unitToSatoshi;
var satToUnit = 1 / unitToSatoshi;
var unitDecimals = config.settings.unitDecimals;
$scope.amountStr = txFormatService.formatAmountStr(data.amount, true); popupService.showAlert(null, msg, function() {});
$scope.displayAmount = getDisplayAmount($scope.amountStr);
$scope.displayUnit = getDisplayUnit($scope.amountStr);
$scope.fee = txFormatService.formatAmountStr(data.fee);
txFormatService.formatAlternativeStr(data.fee, function(v) {
$scope.feeFiat = v;
});
toAmount = parseFloat((data.amount * satToUnit).toFixed(unitDecimals));
txFormatService.formatAlternativeStr(data.amount, function(v) {
$scope.alternativeAmountStr = v;
});
$scope.feeRateStr = (data.fee / (data.amount + data.fee) * 100).toFixed(2) + '%';
$timeout(function() {
$scope.$apply();
});
};
$scope.$on('accepted', function(event) {
$scope.approve();
});
$scope.showWalletSelector = function() {
$scope.walletSelectorTitle = gettextCatalog.getString('Send from');
if (!$scope.useSendMax && ($scope.insufficientFunds || $scope.noMatchingWallet)) return;
$scope.showWallets = true;
}; };
$scope.onWalletSelect = function(wallet) { $scope.onWalletSelect = function(wallet) {
if ($scope.useSendMax) { setWallet(wallet, tx);
$scope.wallet = wallet;
if (cachedSendMax[wallet.id]) {
$log.debug('Send max cached for wallet:', wallet.id);
setSendMaxValues(cachedSendMax[wallet.id]);
return;
}
$scope.getSendMaxInfo();
} else
setWallet(wallet);
applyButtonText(wallet.credentials.m > 1);
}; };
$scope.showDescriptionPopup = function() { $scope.showDescriptionPopup = function(tx) {
var message = gettextCatalog.getString('Add description'); var message = gettextCatalog.getString('Add description');
var opts = { var opts = {
defaultText: $scope.description defaultText: tx.description
}; };
popupService.showPrompt(null, message, opts, function(res) { popupService.showPrompt(null, message, opts, function(res) {
if (typeof res != 'undefined') $scope.description = res; if (typeof res != 'undefined') tx.description = res;
$timeout(function() { $timeout(function() {
$scope.$apply(); $scope.$apply();
}); });
}); });
}; };
function getDisplayAmount(amountStr) {
return $scope.amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return $scope.amountStr.split(' ')[1];
};
function _paymentTimeControl(expirationTime) { function _paymentTimeControl(expirationTime) {
$scope.paymentExpired.value = false; $scope.paymentExpired = false;
setExpirationTime(); setExpirationTime();
countDown = $interval(function() { countDown = $interval(function() {
@ -334,12 +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 { if (tx.paypro)
stop = $timeout(function() { _paymentTimeControl(tx.paypro.expires);
createTx(wallet, true, function(err, txp) {
if (err) return; updateTx(tx, wallet, {
cachedTxp[wallet.id] = txp; dryRun: true
apply(txp); }, function(err) {
}); $timeout(function() {
}, delayed ? 2000 : 1); $ionicScrollDelegate.resize();
} $scope.$apply();
}, 10);
});
$timeout(function() {
$ionicScrollDelegate.resize();
$scope.$apply();
}, 10);
}; };
var setSendError = function(msg) { var setSendError = function(msg) {
@ -382,75 +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 message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', {
amountStr: $scope.amountStr,
name: wallet.name
});
var okText = gettextCatalog.getString('Confirm');
var cancelText = gettextCatalog.getString('Cancel');
if (!spendingPassEnabled && !touchIdEnabled) { var amountUsd = parseFloat(txFormatService.formatToUSD(txp.amount));
if (isCordova) { if (amountUsd <= CONFIRM_LIMIT_USD)
if (bigAmount) { return cb();
popupService.showConfirm(null, message, okText, cancelText, function(ok) {
if (!ok) { var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', {
$scope.sendStatus = ''; amountStr: tx.amountStr,
$timeout(function() { name: wallet.name
$scope.$apply(); });
}); var okText = gettextCatalog.getString('Confirm');
return; var cancelText = gettextCatalog.getString('Cancel');
} popupService.showConfirm(null, message, okText, cancelText, function(ok) {
publishAndSign(wallet, txp, onSendStatusChange); return cb(!ok);
}); });
} else publishAndSign(wallet, txp, onSendStatusChange); };
} else {
popupService.showConfirm(null, message, okText, cancelText, function(ok) { function publishAndSign() {
if (!ok) { if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
$scope.sendStatus = ''; $log.info('No signing proposal: No private key');
return;
} return walletService.onlyPublish(wallet, txp, function(err) {
publishAndSign(wallet, txp, onSendStatusChange); if (err) setSendError(err);
}); }, onSendStatusChange);
} }
} else publishAndSign(wallet, txp, onSendStatusChange);
walletService.publishAndSign(wallet, txp, function(err, txp) {
if (err) return setSendError(err);
}, onSendStatusChange);
};
confirmTx(function(nok) {
if (nok) {
$scope.sendStatus = '';
$timeout(function() {
$scope.$apply();
});
return;
}
publishAndSign();
});
}); });
}; };
@ -543,68 +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();
$scope.$apply(); $timeout(function() {
$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() {
$state.go('tabs.scan'); var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
$state.go('tabs.scan');
return;
}
scannerService.useOldScanner(function(err, contents) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
incomingData.redir(contents);
});
}; };
$scope.showMore = function() { $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);
} }
@ -50,7 +51,7 @@ angular.module('copayApp.controllers').controller('topUpController', function($s
var statusChangeHandler = function (processName, showName, isOn) { var statusChangeHandler = function (processName, showName, isOn) {
$log.debug('statusChangeHandler: ', processName, showName, isOn); $log.debug('statusChangeHandler: ', processName, showName, isOn);
if ( processName == 'topup' && !isOn) { if (processName == 'topup' && !isOn) {
$scope.sendStatus = 'success'; $scope.sendStatus = 'success';
$timeout(function() { $timeout(function() {
$scope.$digest(); $scope.$digest();
@ -60,137 +61,237 @@ 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;
satToFiat(networkFeeSat, function(n) {
$scope.networkFee = Number(n);
$scope.totalAmount = $scope.amount + $scope.invoiceFee + $scope.networkFee;
$timeout(function() {
$scope.$digest();
});
});
});
});
};
var createInvoice = function(data, cb) {
bitpayCardService.topUp(cardId, data, function(err, invoiceId) {
if (err) {
return cb({
title: gettextCatalog.getString('Could not create the invoice'),
message: err
});
}
bitpayCardService.getInvoice(invoiceId, function(err, inv) {
if (err) {
return cb({
title: gettextCatalog.getString('Could not get the invoice'),
message: err
});
}
return cb(null, inv);
});
});
};
var createTx = function(wallet, invoice, message, cb) {
var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null;
if (!payProUrl) {
return cb({
title: gettextCatalog.getString('Error in Payment Protocol'),
message: gettextCatalog.getString('Invalid URL')
});
} }
var parsedAmount = txFormatService.parseAmount(
data.stateParams.amount,
data.stateParams.currency);
amount = parsedAmount.amount; var outputs = [];
currency = parsedAmount.currency; var toAddress = invoice.bitcoinAddress;
$scope.amountUnitStr = parsedAmount.amountUnitStr; var amountSat = parseInt(invoice.btcDue * 100000000); // BTC to Satoshi
$scope.network = bitpayService.getEnvironment().network; outputs.push({
$scope.wallets = profileService.getWallets({ 'toAddress': toAddress,
onlyComplete: true, 'amount': amountSat,
network: $scope.network, 'message': message
hasFunds: true,
minAmount: parsedAmount.amountSat
}); });
if (lodash.isEmpty($scope.wallets)) { var txp = {
showErrorAndBack(null, 'Insufficient funds'); toAddress: toAddress,
return; amount: amountSat,
outputs: outputs,
message: message,
payProUrl: payProUrl,
excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true,
feeLevel: configWallet.settings.feeLevel || 'normal'
};
walletService.createTx(wallet, txp, function(err, ctxp) {
if (err) {
return cb({
title: gettextCatalog.getString('Could not create transaction'),
message: bwcError.msg(err)
});
}
return cb(null, ctxp);
});
};
var calculateAmount = function(wallet, cb) {
// Global variables defined beforeEnter
var a = amount;
var c = currency;
if (useSendMax) {
sendMaxService.getInfo(wallet, function(err, maxValues) {
if (err) {
return cb({
title: null,
message: err
})
}
if (maxValues.amount == 0) {
return cb({message: gettextCatalog.getString('Insufficient funds for fee')});
}
var maxAmountBtc = Number((maxValues.amount / 100000000).toFixed(8));
createInvoice({amount: maxAmountBtc, currency: 'BTC'}, function(err, inv) {
if (err) return cb(err);
var invoiceFeeSat = parseInt((inv.buyerPaidBtcMinerFee * 100000000).toFixed());
var newAmountSat = maxValues.amount - invoiceFeeSat;
if (newAmountSat <= 0) {
return cb({message: gettextCatalog.getString('Insufficient funds for fee')});
}
return cb(null, newAmountSat, 'sat');
});
});
} else {
return cb(null, a, c);
} }
$scope.onWalletSelect($scope.wallets[0]); // Default first wallet };
var initializeTopUp = function(wallet, parsedAmount) {
$scope.amountUnitStr = parsedAmount.amountUnitStr;
var dataSrc = {
amount: parsedAmount.amount,
currency: parsedAmount.currency
};
ongoingProcess.set('loadingTxInfo', true);
createInvoice(dataSrc, function(err, invoice) {
if (err) {
ongoingProcess.set('loadingTxInfo', false);
showErrorAndBack(err.title, err.message);
return;
}
var invoiceFeeSat = (invoice.buyerPaidBtcMinerFee * 100000000).toFixed();
message = gettextCatalog.getString("Top up {{amountStr}} to debit card ({{cardLastNumber}})", {
amountStr: $scope.amountUnitStr,
cardLastNumber: $scope.lastFourDigits
});
createTx(wallet, invoice, message, function(err, ctxp) {
ongoingProcess.set('loadingTxInfo', false);
if (err) {
showErrorAndBack(err.title, err.message);
return;
}
// Save TX in memory
createdTx = ctxp;
$scope.totalAmountStr = txFormatService.formatAmountStr(ctxp.amount);
setTotalAmount(parsedAmount.amountSat, invoiceFeeSat, ctxp.fee);
});
});
};
$scope.$on("$ionicView.beforeLeave", function(event, data) {
$ionicConfig.views.swipeBackEnabled(true);
});
$scope.$on("$ionicView.enter", function(event, data) {
$ionicConfig.views.swipeBackEnabled(false);
});
$scope.$on("$ionicView.beforeEnter", function(event, data) {
cardId = data.stateParams.id;
useSendMax = data.stateParams.useSendMax;
amount = data.stateParams.amount;
currency = data.stateParams.currency;
bitpayCardService.get({ cardId: cardId, noRefresh: true }, function(err, card) { bitpayCardService.get({ cardId: cardId, noRefresh: true }, function(err, card) {
if (err) { if (err) {
showErrorAndBack(null, err); showErrorAndBack(null, err);
return; return;
} }
$scope.cardInfo = card[0]; bitpayCardService.setCurrencySymbol(card[0]);
bitpayCardService.setCurrencySymbol($scope.cardInfo); $scope.lastFourDigits = card[0].lastFourDigits;
bitpayCardService.getRates($scope.cardInfo.currency, function(err, data) { $scope.currencySymbol = card[0].currencySymbol;
if (err) $log.error(err); $scope.currencyIsoCode = card[0].currency;
$scope.rate = data.rate;
});
});
$scope.wallets = profileService.getWallets({
onlyComplete: true,
network: bitpayService.getEnvironment().network,
hasFunds: true
});
if (lodash.isEmpty($scope.wallets)) {
showErrorAndBack(null, gettextCatalog.getString('No wallets available'));
return;
}
bitpayCardService.getRates($scope.currencyIsoCode, function(err, r) {
if (err) $log.error(err);
$scope.rate = r.rate;
});
$scope.onWalletSelect($scope.wallets[0]); // Default first wallet
});
}); });
$scope.topUpConfirm = function() { $scope.topUpConfirm = function() {
var config = configService.getSync(); if (!createdTx) {
var configWallet = config.wallet; showError(null, gettextCatalog.getString('Transaction has not been created'));
var walletSettings = configWallet.settings; return;
}
var message = 'Add ' + amount + ' ' + currency + ' to debit card'; var title = gettextCatalog.getString('Confirm');
var okText = 'Confirm'; var okText = gettextCatalog.getString('OK');
var cancelText = 'Cancel'; var cancelText = gettextCatalog.getString('Cancel');
popupService.showConfirm(null, message, okText, cancelText, function(ok) { popupService.showConfirm(title, message, okText, cancelText, function(ok) {
if (!ok) return; if (!ok) {
$scope.sendStatus = '';
return;
}
var dataSrc = {
amount: amount,
currency: currency
};
ongoingProcess.set('topup', true, statusChangeHandler); ongoingProcess.set('topup', true, statusChangeHandler);
bitpayCardService.topUp(cardId, dataSrc, function(err, invoiceId) { publishAndSign($scope.wallet, createdTx, function() {}, function(err, txSent) {
if (err) { if (err) {
ongoingProcess.set('topup', false, statusChangeHandler); ongoingProcess.set('topup', false);
showErrorAndBack('Could not create the invoice', err); $scope.sendStatus = '';
showError(gettextCatalog.getString('Could not send transaction'), err);
return; return;
} }
ongoingProcess.set('topup', false, statusChangeHandler);
bitpayCardService.getInvoice(invoiceId, function(err, invoice) { });
if (err) { });
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Could not get the invoice', err);
return;
}
var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null;
if (!payProUrl) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Error in Payment Protocol', 'Invalid URL');
return;
}
payproService.getPayProDetails(payProUrl, function(err, payProDetails) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Error fetching invoice', err);
return;
}
var outputs = [];
var toAddress = payProDetails.toAddress;
var amountSat = payProDetails.amount;
var comment = 'Top up ' + amount + ' ' + currency + ' to Debit Card (' + $scope.cardInfo.lastFourDigits + ')';
outputs.push({
'toAddress': toAddress,
'amount': amountSat,
'message': comment
});
var txp = {
toAddress: toAddress,
amount: amountSat,
outputs: outputs,
message: comment,
payProUrl: payProUrl,
excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true,
feeLevel: walletSettings.feeLevel || 'normal'
};
walletService.createTx($scope.wallet, txp, function(err, ctxp) {
if (err) {
ongoingProcess.set('topup', false, statusChangeHandler);
showError('Could not create transaction', bwcError.msg(err));
return;
}
publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) {
ongoingProcess.set('topup', false, statusChangeHandler);
if (err) {
showError('Could not send transaction', err);
return;
}
});
});
}, true); // Disable loader
});
});
});
}; };
$scope.showWalletSelector = function() { $scope.showWalletSelector = function() {
@ -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); calculateAmount(wallet, function(err, a, c) {
sendMaxService.getInfo($scope.wallet, function(err, values) { ongoingProcess.set('retrievingInputs', false);
ongoingProcess.set('retrievingInputs', false); if (err) {
if (err) { createdTx = message = $scope.totalAmountStr = $scope.amountUnitStr = $scope.wallet = null;
showErrorAndBack(null, err); showError(err.title, err.message, function() {
return; $scope.showWalletSelector();
} });
var config = configService.getSync().wallet.settings; return;
var unitName = config.unitName; }
var amountUnit = txFormatService.satToUnit(values.amount); var parsedAmount = txFormatService.parseAmount(a, c);
var parsedAmount = txFormatService.parseAmount( initializeTopUp(wallet, parsedAmount);
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 = [];
@ -11,20 +11,22 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
$scope.wallet = profileService.getWallet(data.stateParams.walletId); $scope.wallet = profileService.getWallet(data.stateParams.walletId);
$scope.color = $scope.wallet.color; $scope.color = $scope.wallet.color;
$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;
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,20 +112,30 @@ 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() { $timeout(function() {
$scope.$apply(); $scope.$digest();
});
feeService.getFeeLevels(function(err, levels) {
if (err) return;
walletService.getLowAmount($scope.wallet, levels, function(err, amount) {
if (err) return;
$scope.btx.lowAmount = tx.amount < amount;
$timeout(function() {
$scope.$apply();
});
});
}); });
}); });
}; };
var updateTxDebounced = lodash.debounce(updateTx, 5000); var updateTxDebounced = lodash.debounce(updateTx, 5000);
$scope.showCommentPopup = function() { $scope.showCommentPopup = function() {
var opts = {}; var opts = {};
if ($scope.btx.message) { if ($scope.btx.message) {
@ -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,26 +9,49 @@ 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;
} }
}); });
// Destroy event // Destroy event
scope.$on('$destroy', function(){ scope.$on('$destroy', function() {
afterEnter(); afterEnter();
}); });
} }

View file

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

View file

@ -9,7 +9,7 @@ angular.module('copayApp.directives')
scope: { 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,10 +12,13 @@ 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() {
if(scope.isShown) { if (scope.isShown) {
elm.style.display = 'flex'; elm.style.display = 'flex';
$timeout(function() { $timeout(function() {
scope.fillScreen = true; scope.fillScreen = true;
@ -24,7 +27,7 @@ angular.module('copayApp.directives')
}); });
scope.onConfirmButtonClick = function() { scope.onConfirmButtonClick = function() {
scope.onConfirm(); scope.onConfirm();
if(scope.hideOnConfirm) { if (scope.hideOnConfirm) {
scope.fillScreen = false; scope.fillScreen = false;
elm.style.display = 'none'; elm.style.display = 'none';
} }

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

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

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',
@ -303,14 +327,14 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
}; };
root.getBuyOrder = function(token, accountId, buyId, cb) { root.getBuyOrder = function(token, accountId, buyId, cb) {
if (!token) return cb('Invalid Token'); if (!token) return cb('Invalid Token');
$http(_get('/accounts/' + accountId + '/buys/' + buyId, token)).then(function(data) { $http(_get('/accounts/' + accountId + '/buys/' + buyId, token)).then(function(data) {
$log.info('Coinbase Buy Info: SUCCESS'); $log.info('Coinbase Buy Info: SUCCESS');
return cb(null, data.data); return cb(null, data.data);
}, function(data) { }, function(data) {
$log.error('Coinbase Buy Info: ERROR ' + data.statusText); $log.error('Coinbase Buy Info: ERROR ' + data.statusText);
return cb(data.data); return cb(data.data);
}); });
}; };
root.getTransaction = function(token, accountId, transactionId, cb) { root.getTransaction = function(token, accountId, transactionId, cb) {
@ -657,13 +681,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) { var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) {
if (!tx) return; if (!tx) return;
var desc = appConfigService.nameCase + ' Wallet'; var desc = appConfigService.nameCase + ' Wallet';
var data = { _getNetAmount(tx.amount.amount, function(err, amountBTC, feeBTC) {
to: tx.toAddr,
amount: tx.amount.amount,
currency: tx.amount.currency,
description: desc
};
root.sendTo(accessToken, accountId, data, function(err, res) {
if (err) { if (err) {
_savePendingTransaction(tx, { _savePendingTransaction(tx, {
status: 'error', status: 'error',
@ -672,8 +690,18 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
if (err) $log.debug(err); if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions); _updateTxs(coinbasePendingTransactions);
}); });
} else { return;
if (res.data && !res.data.id) { }
var data = {
to: tx.toAddr,
amount: amountBTC,
currency: tx.amount.currency,
description: desc,
fee: feeBTC
};
root.sendTo(accessToken, accountId, data, function(err, res) {
if (err) {
_savePendingTransaction(tx, { _savePendingTransaction(tx, {
status: 'error', status: 'error',
error: err error: err
@ -681,19 +709,29 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
if (err) $log.debug(err); if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions); _updateTxs(coinbasePendingTransactions);
}); });
return; } else {
} if (res.data && !res.data.id) {
root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) { _savePendingTransaction(tx, {
_savePendingTransaction(tx, { status: 'error',
remove: true error: err
}, function(err) { }, function(err) {
_savePendingTransaction(sendTx.data, {}, function(err) {
if (err) $log.debug(err); if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions); _updateTxs(coinbasePendingTransactions);
}); });
return;
}
root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) {
_savePendingTransaction(tx, {
remove: true
}, function(err) {
_savePendingTransaction(sendTx.data, {}, function(err) {
if (err) $log.debug(err);
_updateTxs(coinbasePendingTransactions);
});
});
}); });
}); }
} });
}); });
}; };
@ -723,7 +761,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
var register = function() { var register = function() {
root.isActive(function(err, isActive){ root.isActive(function(err, isActive) {
if (err) return; if (err) return;
buyAndSellService.register({ buyAndSellService.register({
@ -742,7 +780,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
$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') {
root.isActive(function(err,isActive){ root.isActive(function(err, isActive) {
// Update Coinbase // Update Coinbase
if (isActive) if (isActive)
root.updatePendingTransactions(); root.updatePendingTransactions();

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,9 +208,9 @@ 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) {
if (!credentials.walletId || !credentials.m) if (!credentials.walletId || !credentials.m)
return cb('bindWallet should receive credentials JSON'); return cb('bindWallet should receive credentials JSON');

View file

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

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>
@ -567,9 +586,9 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
root.getTx = function(wallet, txid, cb) { root.getTx = function(wallet, txid, cb) {
function finish(list){ function finish(list) {
var tx = lodash.find(list, { var tx = lodash.find(list, {
txid: txid txid: txid
}); });
if (!tx) return cb('Could not get transaction'); if (!tx) return cb('Could not get transaction');
@ -602,7 +621,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
}); });
}; };
root.getTxHistory = function(wallet, opts, cb) { root.getTxHistory = function(wallet, opts, cb) {
opts = opts || {}; opts = opts || {};
@ -873,6 +892,81 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
}); });
}; };
// These 2 functions were taken from
// https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js#L243
function getEstimatedSizeForSingleInput(wallet) {
switch (wallet.credentials.addressType) {
case 'P2PKH':
return 147;
default:
case 'P2SH':
return wallet.m * 72 + wallet.n * 36 + 44;
}
};
root.getEstimatedTxSize = function(wallet, nbOutputs) {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.02;
var overhead = 4 + 4 + 9 + 9;
var inputSize = getEstimatedSizeForSingleInput(wallet);
var outputSize = 34;
var nbInputs = 1; //Assume 1 input
var nbOutputs = nbOutputs || 2; // Assume 2 outputs
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
return parseInt((size * (1 + safetyMargin)).toFixed(0));
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getMinFee = function(wallet, feeLevels, nbOutputs) {
var lowLevelRate = (lodash.find(feeLevels[wallet.network], {
level: 'normal',
}).feePerKB / 1000).toFixed(0);
var size = root.getEstimatedTxSize(wallet, nbOutputs);
return size * lowLevelRate;
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getLowAmount = function(wallet, feeLevels, nbOutputs) {
var minFee = root.getMinFee(wallet,feeLevels, nbOutputs);
return parseInt( minFee / LOW_AMOUNT_RATIO);
};
root.getLowUtxos = function(wallet, levels, cb) {
wallet.getUtxos({}, function(err, resp) {
if (err || !resp || !resp.length) return cb();
var minFee = root.getMinFee(wallet, levels, resp.length);
var balance = lodash.sum(resp, 'satoshis');
// for 2 outputs
var lowAmount = root.getLowAmount(wallet, levels);
var lowUtxos = lodash.filter(resp, function(x) {
return x.satoshis < lowAmount;
});
var totalLow = lodash.sum(lowUtxos, 'satoshis');
return cb(err, {
allUtxos: resp || [],
lowUtxos: lowUtxos || [],
warning: minFee / balance > TOTAL_LOW_WARNING_RATIO,
minFee: minFee,
});
});
};
root.getAddress = function(wallet, forceNew, cb) { 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;
@ -107,7 +108,7 @@
margin-top: 5px; margin-top: 5px;
font-size: 14px; font-size: 14px;
color: $item-label-color; color: $item-label-color;
} }
.item-divider { .item-divider {
padding-top: 1.2rem; padding-top: 1.2rem;
color: $item-label-color; color: $item-label-color;

View file

@ -11,7 +11,7 @@
stroke: black; stroke: black;
fill: black; fill: black;
} }
.add-bottom-for-cta { .add-bottom-for-cta {
bottom: 92px; bottom: 92px;
} }
@ -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;
} }
@ -80,7 +76,7 @@
color: $item-label-color; color: $item-label-color;
margin-bottom: 8px; margin-bottom: 8px;
} }
.capitalized { .capitalized {
text-transform: capitalize; text-transform: capitalize;
} }

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>
@ -33,9 +33,9 @@
<div class="item item-icon-right view-all" ng-if="viewAll.value" ng-click="viewAllAddresses()"> <div class="item item-icon-right view-all" ng-if="viewAll.value" ng-click="viewAllAddresses()">
<span translate>View All Addresses</span> <span translate>View All Addresses</span>
<i class="icon ion-ios-arrow-thin-right"></i> <i class="icon ion-ios-arrow-thin-right"></i>
</div> </div>
<div class="item item-divider item-icon-right" ng-click="newAddress()"> <div class="item item-divider item-icon-right" ng-click="newAddress()">
<span translate>Unused Addresses</span> <span translate>Unused Addresses</span>
<i class="icon ion-ios-plus-empty"></i> <i class="icon ion-ios-plus-empty"></i>
</div> </div>
@ -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>
@ -16,7 +16,7 @@
</ion-spinner> </ion-spinner>
<h2 ng-if="item.amount"> <h2 ng-if="item.amount">
{{item.amount | currency : '$ ' : 2}} {{item.currency}} {{item.amount | currency : '$ ' : 2}} {{item.currency}}
</h2> </h2>
<p> <p>
<span class="assertive" ng-if="item.status == 'FAILURE' || item.status == 'RESEND'">Error</span> <span class="assertive" ng-if="item.status == 'FAILURE' || item.status == 'RESEND'">Error</span>
<span class="assertive" ng-if="item.status == 'expired'">Expired</span> <span class="assertive" ng-if="item.status == 'expired'">Expired</span>

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

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

View file

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

View file

@ -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,14 +1,13 @@
<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>
<ion-nav-title>{{wallet.name}}</ion-nav-title> <ion-nav-title>{{wallet.name}}</ion-nav-title>
<ion-nav-buttons side="secondary"> <ion-nav-buttons side="secondary">
<button class="button-share ng-hide" ng-show="isCordova && secret" ng-click="shareSecret()"> <button class="button-share ng-hide" ng-show="isCordova && secret" ng-click="shareSecret()">
<i class="icon" <i class="icon"
ng-class="{ ng-class="{
'ion-ios-upload-outline': shareIcon == 'iOS', 'ion-ios-upload-outline': shareIcon == 'iOS',
'ion-android-share-alt': shareIcon != 'iOS' 'ion-android-share-alt': shareIcon != 'iOS'
}"></i> }"></i>
</button> </button>
@ -34,7 +33,7 @@
</div> </div>
</div> </div>
<button ng-if="secret" class="button-cancel" ng-click="showDeletePopup()"> <button ng-if="secret" class="button-cancel" ng-click="showDeletePopup()">
<span translate>Cancel invitation</span> <span translate>Cancel invitation</span>
</button> </button>
</div> </div>
</div> </div>

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>
@ -13,7 +13,7 @@
Refresh Refresh
</button> </button>
</ion-nav-buttons> </ion-nav-buttons>
<ion-content scroll="false" class="ng-hide" ng-show="!account.token"> <ion-content scroll="false" class="ng-hide" ng-show="!account.token">
<div class="integration-onboarding"> <div class="integration-onboarding">
<div class="integration-onboarding-logo"> <div class="integration-onboarding-logo">
<img src="img/glidera-logo.png"> <img src="img/glidera-logo.png">

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,21 +1,16 @@
<div class="item item-icon-right item-heading">
<div class="list card"> <span translate>Cards</span>
<div class="item item-icon-right item-heading"> <a ui-sref="tabs.bitpayCardIntro"><i class="icon ion-ios-plus-empty list-add-button"></i></a>
<span translate>Cards</span> </div>
<a ui-sref="tabs.bitpayCardIntro"><i class="icon ion-ios-plus-empty list-add-button"></i></a> <div>
</div> <a ng-repeat="card in bitpayCardItems track by $index"
<div> ui-sref="tabs.bitpayCard({id:card.id})"
<a ng-repeat="card in bitpayCardItems track by $index" class="item item-sub item-icon-left item-big-icon-left item-icon-right">
ui-sref="tabs.bitpayCard({id:card.id})" <i class="icon big-icon-svg">
class="item item-sub item-icon-left item-big-icon-left item-icon-right"> <div class="bg icon-bitpay-card"></div>
<i class="icon big-icon-svg"> </i>
<div class="bg icon-bitpay-card"></div> <span>BitPay Visa&reg; Card ({{card.lastFourDigits}})</span>
</i> <p>{{card.balance ? card.currencySymbol + card.balance : 'Add funds to get started'|translate}} {{card.updatedOn ? (' &middot; ' + (card.updatedOn * 1000 | amTimeAgo)) : ''}}</p>
<span>BitPay Visa&reg; Card ({{card.lastFourDigits}})</span> <i class="icon bp-arrow-right"></i>
<p>{{card.balance ? card.currencySymbol + card.balance : 'Add funds to get started'|translate}} {{card.updatedOn ? (' &middot; ' + (card.updatedOn * 1000 | amTimeAgo)) : ''}}</p> </a>
<i class="icon bp-arrow-right"></i>
</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,19 +1,18 @@
<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>
<i class="icon bp-arrow-down" ng-show="hide"></i> <i class="icon bp-arrow-down" ng-show="hide"></i>
</div> </div>
<div ng-show="!hide"> <div ng-show="!hide">
<div ng-repeat="service in services track by $index"> <div ng-repeat="service in services track by $index">
<a ui-sref="{{service.sref}}" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step"> <a ui-sref="{{service.sref}}" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step">
<i class="icon big-icon-svg"> <i class="icon big-icon-svg">
<div class="bg {{service.icon}}"></div> <div class="bg {{service.icon}}"></div>
</i> </i>
<span>{{service.title || service.name}}</span> <span>{{service.title || service.name}}</span>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
</div>
</div> </div>
</div> </div>
</div>

View file

@ -1,19 +1,18 @@
<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>
<i class="icon bp-arrow-down" ng-show="hide"></i> <i class="icon bp-arrow-down" ng-show="hide"></i>
</div> </div>
<div ng-show="!hide"> <div ng-show="!hide">
<div ng-repeat="service in services track by $index"> <div ng-repeat="service in services track by $index">
<a ui-sref="{{service.sref}}" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step"> <a ui-sref="{{service.sref}}" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step">
<i class="icon big-icon-svg"> <i class="icon big-icon-svg">
<div class="bg {{service.icon}}"></div> <div class="bg {{service.icon}}"></div>
</i> </i>
<span>{{service.title || service.name}}</span> <span>{{service.title || service.name}}</span>
<i class="icon bp-arrow-right"></i> <i class="icon bp-arrow-right"></i>
</a> </a>
</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