Improved bitpay account pairing and management of paired state and data.

This commit is contained in:
Andy Phillipson 2017-01-06 12:11:47 -05:00
parent 15d12823ab
commit 63bc3d8f63
23 changed files with 692 additions and 209 deletions

View File

@ -1,5 +1,5 @@
'use strict';
angular.module('copayApp.controllers').controller('bitpayCardIntroController', function($scope, $log, $state, $ionicHistory, storageService, externalLinkService, bitpayCardService, gettextCatalog, popupService, appIdentityService, bitpayService, lodash) {
angular.module('copayApp.controllers').controller('bitpayCardIntroController', function($scope, $log, $state, $ionicHistory, storageService, externalLinkService, bitpayCardService, gettextCatalog, popupService, bitpayAccountService) {
$scope.$on("$ionicView.beforeEnter", function(event, data) {
if (data.stateParams && data.stateParams.secret) {
@ -8,11 +8,8 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f
email: data.stateParams.email,
otp: data.stateParams.otp
};
var pairingReason = gettextCatalog.getString('BitPay Visa card');
bitpayService.pair(pairData, pairingReason, function(err, paired, apiContext) {
var pairingReason = gettextCatalog.getString('add your BitPay Visa card(s)');
bitpayAccountService.pair(pairData, pairingReason, function(err, paired, apiContext) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error pairing Bitpay Account'), err);
return;
@ -25,26 +22,28 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f
}
// Set flag for nextStep
storageService.setNextStep('BitpayCard', 'true', function(err) {});
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('tabs.home').then(function() {
if (cards[0]) {
if (data.cards[0]) {
$state.transitionTo('tabs.bitpayCard', {
id: cards[0].id
id: data.cards[0].id
});
}
});
});
}
});
} else {
appIdentityService.getIdentity(bitpayService.getEnvironment().network, function(err, appIdentity) {
if (err) popupService.showAlert(null, err);
else $log.info('App identity: OK');
});
}
bitpayAccountService.getAccounts(function(err, accounts) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
$scope.accounts = accounts;
});
});
$scope.bitPayCardInfo = function() {
@ -58,7 +57,37 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f
};
$scope.connectBitPayCard = function() {
var url = 'https://bitpay.com/visa/dashboard/add-to-bitpay-wallet-confirm';
externalLinkService.open(url);
if ($scope.accounts.length == 0) {
startPairBitPayAccount();
} else {
showAccountSelector();
}
};
var startPairBitPayAccount = function() {
var url = 'https://bitpay.com/visa/dashboard/add-to-bitpay-wallet-confirm';
externalLinkService.open(url);
};
var showAccountSelector = function() {
$scope.accountSelectorTitle = gettextCatalog.getString('From BitPay account');
$scope.showAccounts = ($scope.accounts != undefined);
};
$scope.onAccountSelect = function(account) {
if (account == undefined) {
startPairBitPayAccount();
} else {
bitpayCardService.fetchBitpayDebitCards(account.apiContext, function(err, data) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
}
storageService.setNextStep('BitpayCard', 'true', function(err) {
$state.go('tabs.home');
});
});
}
};
});

View File

@ -0,0 +1,75 @@
'use strict';
angular.module('copayApp.controllers').controller('preferencesBitpayServicesController',
function($rootScope, $scope, $state, $timeout, $ionicHistory, bitpayAccountService, bitpayCardService, popupService, gettextCatalog) {
$scope.removeAccount = function(account) {
var title = gettextCatalog.getString('Remove BitPay Account?');
var msg = gettextCatalog.getString('Removing your BitPay account will remove all associated BitPay account data from this device.<br/><br/>Are you sure you would like to remove your BitPay Account ({{email}}) from this device?', {
email: account.email
});
popupService.showConfirm(title, msg, null, null, function(res) {
if (res) {
removeAccount(account);
}
});
};
$scope.removeCard = function(card) {
var title = gettextCatalog.getString('Remove BitPay Card?');
var msg = gettextCatalog.getString('Are you sure you would like to remove your BitPay Card ({{lastFourDigits}}) from this device?', {
lastFourDigits: card.lastFourDigits
});
popupService.showConfirm(title, msg, null, null, function(res) {
if (res) {
removeCard(card);
}
});
};
var removeAccount = function(account) {
bitpayAccountService.removeAccount(account, function(err) {
if (err) {
return popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Could not remove account'));
}
setScope(function() {
// If there are no paired accounts then change views.
if ($scope.bitpayAccounts.length == 0) {
$state.go('tabs.settings').then(function() {
$ionicHistory.clearHistory();
$state.go('tabs.home');
});
}
});
});
};
var removeCard = function(card) {
bitpayCardService.removeCard(card, function(err) {
if (err) {
return popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Could not remove card'));
}
setScope();
});
};
var setScope = function(cb) {
bitpayAccountService.getAccounts(function(err, data) {
if (err) return;
$scope.bitpayAccounts = data;
bitpayCardService.getBitpayDebitCards(function(err, data) {
if (err) return;
$scope.bitpayCards = data;
if (cb) {
cb();
}
});
});
};
$scope.$on("$ionicView.beforeEnter", function(event, data) {
setScope();
});
});

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayCardService, storageService, glideraService, coinbaseService, gettextCatalog, buyAndSellService) {
angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $ionicModal, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayAccountService, bitpayCardService, storageService, glideraService, gettextCatalog) {
var updateConfig = function() {
var isCordova = platformInfo.isCordova;
@ -25,12 +25,27 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct
isoCode: config.wallet.settings.alternativeIsoCode
};
// TODO move this to a generic service
bitpayCardService.getCards(function(err, cards) {
$scope.bitpayCardEnabled = config.bitpayCard.enabled;
$scope.glideraEnabled = config.glidera.enabled && !isWindowsPhoneApp;
bitpayAccountService.getAccounts(function(err, data) {
if (err) $log.error(err);
$scope.bitpayCards = cards && cards.length > 0;
$scope.bitpayAccounts = !lodash.isEmpty(data);
});
if ($scope.bitpayCardEnabled) {
bitpayCardService.getBitpayDebitCards(function(err, cards) {
if (err) $log.error(err);
$scope.bitpayCards = cards && cards.length > 0;
});
}
if ($scope.glideraEnabled) {
storageService.getGlideraToken(glideraService.getEnvironment(), function(err, token) {
if (err) $log.error(err);
$scope.glideraToken = token;
});
}
});
};

View File

@ -0,0 +1,28 @@
'use strict';
angular.module('copayApp.directives')
.directive('accountSelector', function($timeout) {
return {
restrict: 'E',
templateUrl: 'views/includes/accountSelector.html',
transclude: true,
scope: {
title: '=accountSelectorTitle',
show: '=accountSelectorShow',
accounts: '=accountSelectorAccounts',
selectedAccount: '=accountSelectorSelectedAccount',
onSelect: '=accountSelectorOnSelect'
},
link: function(scope, element, attrs) {
scope.hide = function() {
scope.show = false;
};
scope.selectAccount = function(account) {
$timeout(function() {
scope.hide();
}, 100);
scope.onSelect(account);
};
}
};
});

View File

@ -1078,12 +1078,12 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
paypro: null
}
})
.state('tabs.preferences.bitpayCard', {
url: '/bitpay-card',
.state('tabs.preferences.bitpayServices', {
url: '/bitpay-services',
views: {
'tab-settings@tabs': {
controller: 'preferencesBitpayCardController',
templateUrl: 'views/preferencesBitpayCard.html'
controller: 'preferencesBitpayServicesController',
templateUrl: 'views/preferencesBitpayServices.html'
}
}
});

View File

@ -0,0 +1,206 @@
'use strict';
angular.module('copayApp.services').factory('bitpayAccountService', function($log, lodash, platformInfo, appIdentityService, bitpayService, bitpayCardService, storageService, gettextCatalog, popupService) {
var root = {};
/*
* Pair this app with the bitpay server using the specified pairing data.
* An app identity will be created if one does not already exist.
* Pairing data is provided by an input URI provided by the bitpay server.
*
* pairData - data needed to complete the pairing process
* {
* secret: shared pairing secret
* email: email address associated with bitpay account
* otp: two-factor one-time use password
* }
*
* pairingReason - text string to be embedded into popup message. If `null` then the reason
* message is not shown to the UI.
* "To {{reason}} you must pair this app with your BitPay account ({{email}})."
*
* cb - callback after completion
* callback(err, paired, apiContext)
*
* err - something unexpected happened which prevented the pairing
*
* paired - boolean indicating whether the pairing was compledted by the user
*
* apiContext - the context needed for making future api calls
* {
* token: api token for use in future calls
* pairData: the input pair data
* appIdentity: the identity of this app
* }
*/
root.pair = function(pairData, pairingReason, cb) {
checkOtp(pairData, function(otp) {
pairData.otp = otp;
var deviceName = 'Unknown device';
if (platformInfo.isNW) {
deviceName = require('os').platform();
} else if (platformInfo.isCordova) {
deviceName = device.model;
}
var json = {
method: 'createToken',
params: {
secret: pairData.secret,
version: 2,
deviceName: deviceName,
code: pairData.otp
}
};
bitpayService.postAuth(json, function(data) {
if (data && data.data.error) {
return cb(data.data.error);
}
var apiContext = {
token: data.data.data,
pairData: pairData,
appIdentity: data.appIdentity
};
$log.info('BitPay service BitAuth create token: SUCCESS');
fetchBasicInfo(apiContext, function(err, basicInfo) {
if (err) return cb(err);
var title = gettextCatalog.getString('Add BitPay Account?');
var msgDetail = 'Add this BitPay account ({{email}})?';
if (pairingReason) {
msgDetail = 'To {{reason}} you must first add your BitPay account.<br/><br/>{{email}}';
}
var msg = gettextCatalog.getString(msgDetail, {
reason: pairingReason,
email: pairData.email
});
var ok = gettextCatalog.getString('Add Account');
var cancel = gettextCatalog.getString('Go back');
popupService.showConfirm(title, msg, ok, cancel, function(res) {
if (res) {
var acctData = {
token: apiContext.token,
email: pairData.email,
givenName: basicInfo.givenName,
familyName: basicInfo.familyName
};
setBitpayAccount(acctData, function(err) {
return cb(err, true, apiContext);
});
} else {
$log.info('User cancelled BitPay pairing process');
return cb(null, false);
}
});
});
}, function(data) {
return cb(_setError('BitPay service BitAuth create token: ERROR ', data));
});
});
};
var checkOtp = function(pairData, cb) {
if (pairData.otp) {
var msg = gettextCatalog.getString('Enter Two Factor for your BitPay account');
popupService.showPrompt(null, msg, null, function(res) {
cb(res);
});
} else {
cb();
}
};
var fetchBasicInfo = function(apiContext, cb) {
var json = {
method: 'getBasicInfo'
};
// Get basic account information
bitpayService.post('/api/v2/' + apiContext.token, json, function(data) {
if (data && data.data.error) return cb(data.data.error);
$log.info('BitPay Account Get Basic Info: SUCCESS');
return cb(null, data.data.data);
}, function(data) {
return cb(_setError('BitPay Account Error: Get Basic Info', data));
});
};
// Returns account objects as stored.
root.getAccountsAsStored = function(cb) {
storageService.getBitpayAccounts(bitpayService.getEnvironment().network, cb);
};
// Returns an array where each element represents an account including all information required for fetching data
// from the server for each account (apiContext).
root.getAccounts = function(cb) {
root.getAccountsAsStored(function(err, accounts) {
if (err || !accounts) {
return cb(err, []);
}
appIdentityService.getIdentity(bitpayService.getEnvironment().network, function(err, appIdentity) {
if (err) {
return cb(err);
}
var accountsArray = [];
lodash.forEach(Object.keys(accounts), function(key) {
accounts[key].bitpayDebitCards = accounts[key]['bitpayDebitCards-' + bitpayService.getEnvironment().network];
accounts[key].email = key;
accounts[key].firstName = accounts[key]['basicInfo-' + bitpayService.getEnvironment().network].givenName;
accounts[key].lastName = accounts[key]['basicInfo-' + bitpayService.getEnvironment().network].familyName;
accounts[key].apiContext = {
token: accounts[key]['bitpayApi-' + bitpayService.getEnvironment().network].token,
pairData: {
email: key
},
appIdentity: appIdentity
};
// Remove environment keyed attributes.
delete accounts[key]['bitpayApi-' + bitpayService.getEnvironment().network];
delete accounts[key]['bitpayDebitCards-' + bitpayService.getEnvironment().network];
accountsArray.push(accounts[key]);
});
return cb(null, accountsArray);
});
});
};
var setBitpayAccount = function(account, cb) {
var data = JSON.stringify(account);
storageService.setBitpayAccount(bitpayService.getEnvironment().network, data, function(err) {
if (err) {
return cb(err);
}
return cb();
});
};
root.removeAccount = function(account, cb) {
storageService.removeBitpayAccount(bitpayService.getEnvironment().network, account, function(err) {
if (err) {
$log.error('Error removing BitPay account: ' + err);
// Continue, try to remove next step if necessary
}
storageService.getBitpayDebitCards(bitpayService.getEnvironment().network, function(err, cards) {
if (err) {
$log.error('Error attempting to get BitPay debit cards after account removal: ' + err);
}
if (cards.length == 0) {
storageService.removeNextStep('BitpayCard', cb);
} else {
cb();
}
});
});
};
var _setError = function(msg, e) {
$log.error(msg);
var error = (e && e.data && e.data.error) ? e.data.error : msg;
return error;
};
return root;
});

View File

@ -47,28 +47,13 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
bitpayService.post('/api/v2/' + apiContext.token, json, function(data) {
if (data && data.data.error) return cb(data.data.error);
$log.info('BitPay Get Debit Cards: SUCCESS');
var cards = [];
lodash.each(data.data.data, function(x) {
var n = {};
if (!x.eid || !x.id || !x.lastFourDigits || !x.token) {
$log.warn('BAD data from Bitpay card' + JSON.stringify(x));
return;
}
n.eid = x.eid;
n.id = x.id;
n.lastFourDigits = x.lastFourDigits;
n.token = x.token;
cards.push(n);
});
storageService.setBitpayDebitCards(bitpayService.getEnvironment().network, apiContext.pairData.email, cards, function(err) {
register();
return cb(err, cards);
// Cache card data in storage
var cardData = {
cards: data.data.data,
email: apiContext.pairData.email
}
root.setBitpayDebitCards(cardData, function(err) {
return cb(err, {token: apiContext.token, cards: data.data.data, email: apiContext.pairData.email});
});
}, function(data) {
return cb(_setError('BitPay Card Error: Get Debit Cards', data));
@ -201,15 +186,32 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
}, cb);
};
root.remove = function(cardId, cb) {
storageService.removeBitpayDebitCard(bitpayService.getEnvironment().network, cardId, function(err) {
root.removeCard = function(card, cb) {
storageService.removeBitpayDebitCard(bitpayService.getEnvironment().network, card, function(err) {
if (err) {
$log.error('Error removing BitPay debit card: ' + err);
return cb(err);
// Continue, try to remove/cleanup next step and card history
}
register();
storageService.removeBalanceCache(cardId, cb);
// Next two items in parallel
//
// If there are no more cards in storage then re-enable the next step entry
storageService.getBitpayDebitCards(bitpayService.getEnvironment().network, function(err, cards) {
if (err) {
$log.error('Error getting BitPay debit cards after remove: ' + err);
// Continue, try to remove next step if necessary
}
if (cards.length == 0) {
storageService.removeNextStep('BitpayCard', cb);
}
});
storageService.removeBitpayDebitCardHistory(bitpayService.getEnvironment().network, card, function(err) {
if (err) {
$log.error('Error removing BitPay debit card transaction history: ' + err);
return cb(err);
}
$log.info('Successfully removed BitPay debit card');
return cb();
});
});
};

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.services').factory('bitpayService', function($log, $http, platformInfo, appIdentityService, bitauthService, storageService, gettextCatalog, popupService, ongoingProcess) {
angular.module('copayApp.services').factory('bitpayService', function($log, $http, appIdentityService, bitauthService) {
var root = {};
var NETWORK = 'livenet';
@ -12,99 +12,6 @@ angular.module('copayApp.services').factory('bitpayService', function($log, $htt
};
};
/*
* Pair this app with the bitpay server using the specified pairing data.
* An app identity will be created if one does not already exist.
* Pairing data is provided by an input URI provided by the bitpay server.
*
* pairData - data needed to complete the pairing process
* {
* secret: shared pairing secret
* email: email address associated with bitpay account
* otp: two-factor one-time use password
* }
*
* pairingReason - text string to be embedded into popup message. If `null` then the reason
* message is not shown to the UI.
* "To {{reason}} you must pair this app with your BitPay account ({{email}})."
*
* cb - callback after completion
* callback(err, paired, apiContext)
*
* err - something unexpected happened which prevented the pairing
*
* paired - boolean indicating whether the pairing was compledted by the user
*
* apiContext - the context needed for making future api calls
* {
* token: api token for use in future calls
* pairData: the input pair data
* appIdentity: the identity of this app
* }
*/
root.pair = function(pairData, pairingReason, cb) {
checkOtp(pairData, function(otp) {
pairData.otp = otp;
var deviceName = 'Unknown device';
if (platformInfo.isNW) {
deviceName = require('os').platform();
} else if (platformInfo.isCordova) {
deviceName = device.model;
}
var json = {
method: 'createToken',
params: {
secret: pairData.secret,
version: 2,
deviceName: deviceName,
code: pairData.otp
}
};
appIdentityService.getIdentity(root.getEnvironment().network, function(err, appIdentity) {
if (err) return cb(err);
ongoingProcess.set('fetchingBitPayAccount', true);
$http(_postAuth('/api/v2/', json, appIdentity)).then(function(data) {
ongoingProcess.set('fetchingBitPayAccount', false);
if (data && data.data.error) return cb(data.data.error);
$log.info('BitPay service BitAuth create token: SUCCESS');
var title = gettextCatalog.getString('Link BitPay Account?');
var msgDetail = 'Link BitPay account ({{email}})?';
if (pairingReason) {
msgDetail = 'To add your {{reason}} please link your BitPay account {{email}}';
}
var msg = gettextCatalog.getString(msgDetail, {
reason: pairingReason,
email: pairData.email
});
var ok = gettextCatalog.getString('Confirm');
var cancel = gettextCatalog.getString('Cancel');
popupService.showConfirm(title, msg, ok, cancel, function(res) {
if (res) {
var acctData = {
token: data.data.data,
email: pairData.email
};
setBitpayAccount(acctData, function(err) {
if (err) return cb(err);
return cb(null, true, {
token: acctData.token,
pairData: pairData,
appIdentity: appIdentity
});
});
} else {
$log.info('User cancelled BitPay pairing process');
return cb(null, false);
}
});
}, function(data) {
return cb(_setError('BitPay service BitAuth create token: ERROR ', data));
});
});
});
};
root.get = function(endpoint, successCallback, errorCallback) {
$http(_get(endpoint)).then(function(data) {
successCallback(data);
@ -115,7 +22,9 @@ angular.module('copayApp.services').factory('bitpayService', function($log, $htt
root.post = function(endpoint, json, successCallback, errorCallback) {
appIdentityService.getIdentity(root.getEnvironment().network, function(err, appIdentity) {
if (err) return errorCallback(err);
if (err) {
return errorCallback(err);
}
$http(_post(endpoint, json, appIdentity)).then(function(data) {
successCallback(data);
}, function(data) {
@ -124,22 +33,20 @@ angular.module('copayApp.services').factory('bitpayService', function($log, $htt
});
};
var checkOtp = function(pairData, cb) {
if (pairData.otp) {
var msg = gettextCatalog.getString('Enter Two Factor for your BitPay account');
popupService.showPrompt(null, msg, null, function(res) {
cb(res);
root.postAuth = function(json, successCallback, errorCallback) {
appIdentityService.getIdentity(root.getEnvironment().network, function(err, appIdentity) {
if (err) {
return errorCallback(err);
}
$http(_postAuth('/api/v2/', json, appIdentity)).then(function(data) {
data.appIdentity = appIdentity;
successCallback(data);
}, function(data) {
errorCallback(data);
});
} else {
cb();
}
});
};
var setBitpayAccount = function(accountData, cb) {
storageService.setBitpayAccount(root.getEnvironment().network, accountData, cb);
};
var _get = function(endpoint) {
return {
method: 'GET',
@ -184,12 +91,6 @@ angular.module('copayApp.services').factory('bitpayService', function($log, $htt
return ret;
};
var _setError = function(msg, e) {
$log.error(msg);
var error = (e && e.data && e.data.error) ? e.data.error : msg;
return error;
};
return root;
});

View File

@ -190,6 +190,9 @@ angular.module('copayApp.services').factory('configService', function(storageSer
if (!configCache.pushNotifications) {
configCache.pushNotifications = defaultConfig.pushNotifications;
}
if (!configCache.bitpayAccount) {
configCache.bitpayAccount = defaultConfig.bitpayAccount;
}
} else {
configCache = lodash.clone(defaultConfig);

View File

@ -362,17 +362,25 @@ angular.module('copayApp.services')
// lastFourDigits: card number
// token: card token
// ]
root.setBitpayDebitCards = function(network, email, cards, cb) {
root.getBitpayAccounts(network, function(err, allAccounts) {
// email: account email
// token: account token
// }
root.setBitpayDebitCards = function(network, data, cb) {
if (lodash.isString(data)) {
data = JSON.parse(data);
}
data = data || {};
if (lodash.isEmpty(data) || !data.email) return cb('Cannot set cards: no account to set');
storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) {
if (err) return cb(err);
if (!allAccounts[email]) {
return cb('Cannot set cards for unknown account ' + email);
}
allAccounts[email].cards = cards;
storage.set('bitpayAccounts-v2-' + network, allAccounts, cb);
bitpayAccounts = bitpayAccounts || {};
bitpayAccounts[data.email] = bitpayAccounts[data.email] || {};
bitpayAccounts[data.email]['bitpayDebitCards-' + network] = data.cards;
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb);
});
};
@ -385,7 +393,6 @@ angular.module('copayApp.services')
// email: account email
// ]
root.getBitpayDebitCards = function(network, cb) {
root.getBitpayAccounts(network, function(err, allAccounts) {
if (err) return cb(err);
@ -510,6 +517,28 @@ angular.module('copayApp.services')
});
};
// account: {
// email: account email
// apiContext: the context needed for making future api calls
// bitpayDebitCards: an array of cards
// }
root.removeBitpayAccount = function(network, account, cb) {
if (lodash.isString(account)) {
account = JSON.parse(account);
}
account = account || {};
if (lodash.isEmpty(account)) return cb('No account to remove');
storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) {
if (err) cb(err);
if (lodash.isString(bitpayAccounts)) {
bitpayAccounts = JSON.parse(bitpayAccounts);
}
bitpayAccounts = bitpayAccounts || {};
delete bitpayAccounts[account.email];
storage.set('bitpayAccounts-v3-' + network, JSON.stringify(bitpayAccounts), cb);
});
};
root.setAppIdentity = function(network, data, cb) {
storage.set('appIdentity-' + network, data, cb);
};

View File

@ -1,4 +1,5 @@
#bitpayCard-intro {
@extend .deflash-blue;
background: url(../img/onboarding-welcome-bg.png), linear-gradient(to bottom, rgba(30, 49, 134, 1) 0%, rgba(17, 27, 73, 1) 100%);
background-position: top center;
background-size: contain;

View File

@ -1,4 +1,4 @@
#bitpayCardPreferences {
#bitpayServicesPreferences {
.item {
.item-title {
display: block;
@ -14,9 +14,13 @@
&.item-icon-right {
.icon-hotspot {
right: 0px;
padding-left: 11px;
padding-right: 11px;
}
padding-left: 50px;
}
}
.icon-unlink {
background-image: url("../img/icon-unlink.svg");
background-repeat: no-repeat;
background-position: center;
}
}
}

View File

@ -0,0 +1,79 @@
account-selector {
$border-color: #EFEFEF;
text-align: left;
.bp-action-sheet__sheet {
padding-left: 2rem;
padding-right: .75rem;
}
.account-selector {
.account {
border: 0;
padding-right: 0;
padding-top: 0;
padding-left: 65px;
padding-bottom: 0;
margin-bottom: 1px;
overflow: visible;
> i {
padding: 0;
margin-left: -5px;
> img {
height: 39px;
width: 39px;
padding: 4px;
background-color: $royal;
&.icon-add {
background-color: $light-gray;
}
}
}
}
.account-inner {
display: flex;
position: relative;
padding-top: 16px;
padding-bottom: 16px;
&::after {
display: block;
position: absolute;
width: 100%;
height: 1px;
background: $border-color;
bottom: 0;
right: 0;
content: '';
}
.check {
padding: 0 1.2rem;
}
}
.account-details {
flex-grow: 1;
.account-name {
padding-bottom: 5px;
}
.account-email {
color: #3A3A3A;
font-family: "Roboto-Light";
}
.account-add {
padding-bottom: 16px;
padding-top: 11px;
}
}
}
}

View File

@ -1,8 +1,7 @@
.settings {
@extend .deflash-blue;
.icon-bitpay-card {
background-image: url("../img/icon-card.svg");
background-color: #1e3186;
.icon-bitpay {
background-image: url("../img/icon-bitpay.svg");
}
.item {
color: $dark-gray;

View File

@ -14,8 +14,8 @@
@import "advancedSettings";
@import "bitpayCard";
@import "bitpayCardIntro";
@import "bitpayCardPreferences";
@import "buyandsell";
@import "bitpayServicesPreferences";
@import "address-book";
@import "addresses";
@import "wallet-backup-phrase";
@ -42,5 +42,6 @@
@import "includes/tx-status";
@import "includes/itemSelector";
@import "includes/walletSelector";
@import "includes/accountSelector";
@import "integrations/integrations";
@import "custom-amount";

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200px" height="1200px" viewBox="0 0 1200 1200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>Untitled</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(196.000000, 110.000000)" fill="#FFFFFF">
<path d="M412.2,529.7 C266.2,529.7 147.3,410.9 147.3,264.8 C147.3,118.8 266.1,5.68434189e-14 412.2,5.68434189e-14 C558.2,0 677,118.8 677,264.8 C677,410.9 558.2,529.7 412.2,529.7 Z M412.2,59 C298.7,59 206.4,151.3 206.4,264.8 C206.4,378.3 298.7,470.6 412.2,470.6 C525.7,470.6 618,378.3 618,264.8 C618,151.4 525.7,59 412.2,59 Z" id="Shape"></path>
<path d="M151.9,976.1 C131.7,976.1 73.9,972.9 38.3,939.7 C-2.2,902 -0.4,839.9 1.1,821.7 C16.3,640.2 202.7,486.9 408.1,486.9 C492.4,486.9 573.6,512.1 642.8,559.6 C652.1,566 661.2,572.8 670,579.9 C682.6,590.2 684.5,608.8 674.3,621.4 C664,634 645.4,635.9 632.8,625.7 C625.3,619.6 617.4,613.7 609.5,608.2 C550.1,567.4 480.5,545.8 408.2,545.8 C232.2,545.8 72.8,674.4 60,826.5 C58,850.8 62.9,881.9 78.6,896.4 C98.7,915.2 143.4,917.5 157.3,916.8 L647,916.8 C663.3,916.8 676.5,930 676.5,946.3 C676.5,962.6 663.2,976 646.9,976 L158.6,976 C157.7,976.1 155.3,976.1 151.9,976.1 Z" id="Shape"></path>
<path d="M773.3,949.6 C773.3,962.3 763,972.7 750.2,972.7 L750.2,972.7 C737.5,972.7 727.1,962.4 727.1,949.6 L727.1,635.9 C727.1,623.2 737.4,612.8 750.2,612.8 L750.2,612.8 C762.9,612.8 773.3,623.1 773.3,635.9 L773.3,949.6 Z" id="Shape"></path>
<path d="M750.2,980 C733.4,980 719.8,966.3 719.8,949.6 L719.8,635.9 C719.8,619.1 733.5,605.5 750.2,605.5 C767,605.5 780.6,619.2 780.6,635.9 L780.6,949.6 C780.7,966.3 767,980 750.2,980 Z M750.2,620.2 C741.5,620.2 734.5,627.2 734.5,635.9 L734.5,949.6 C734.5,958.2 741.5,965.3 750.2,965.3 C758.8,965.3 765.9,958.3 765.9,949.6 L765.9,635.9 C765.9,627.2 758.9,620.2 750.2,620.2 Z" id="Shape"></path>
<path d="M907.1,769.7 C919.8,769.7 930.2,780 930.2,792.8 L930.2,792.8 C930.2,805.5 919.9,815.9 907.1,815.9 L593.4,815.9 C580.7,815.9 570.3,805.6 570.3,792.8 L570.3,792.8 C570.3,780.1 580.6,769.7 593.4,769.7 L907.1,769.7 Z" id="Shape"></path>
<path d="M907.1,823.2 L593.4,823.2 C576.6,823.2 563,809.5 563,792.8 C563,776 576.7,762.4 593.4,762.4 L907.1,762.4 C923.9,762.4 937.5,776.1 937.5,792.8 C937.5,809.5 923.9,823.2 907.1,823.2 Z M593.4,777 C584.8,777 577.7,784 577.7,792.7 C577.7,801.3 584.7,808.4 593.4,808.4 L907.1,808.4 C915.7,808.4 922.8,801.4 922.8,792.7 C922.8,784 915.8,777 907.1,777 L593.4,777 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

14
www/img/icon-account.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200px" height="1200px" viewBox="0 0 1200 1200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>Untitled</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(196.000000, 110.000000)" fill="#FFFFFF">
<path d="M412.2,529.7 C266.2,529.7 147.3,410.9 147.3,264.8 C147.3,118.8 266.1,5.68434189e-14 412.2,5.68434189e-14 C558.2,0 677,118.8 677,264.8 C677,410.9 558.2,529.7 412.2,529.7 Z M412.2,59 C298.7,59 206.4,151.3 206.4,264.8 C206.4,378.3 298.7,470.6 412.2,470.6 C525.7,470.6 618,378.3 618,264.8 C618,151.4 525.7,59 412.2,59 Z" id="Shape"></path>
<path d="M151.9,976.1 C131.7,976.1 73.9,972.9 38.3,939.7 C-2.2,902 -0.4,839.9 1.1,821.7 C16.3,640.2 202.7,486.9 408.1,486.9 C492.4,486.9 573.6,512.1 642.8,559.6 C652.1,566 661.2,572.8 670,579.9 C682.6,590.2 684.5,608.8 674.3,621.4 C664,634 645.4,635.9 632.8,625.7 C625.3,619.6 617.4,613.7 609.5,608.2 C550.1,567.4 480.5,545.8 408.2,545.8 C232.2,545.8 72.8,674.4 60,826.5 C58,850.8 62.9,881.9 78.6,896.4 C98.7,915.2 143.4,917.5 157.3,916.8 L647,916.8 C663.3,916.8 676.5,930 676.5,946.3 C676.5,962.6 663.2,976 646.9,976 L158.6,976 C157.7,976.1 155.3,976.1 151.9,976.1 Z" id="Shape"></path>
<path d="M280.9,976.1 C260.7,976.1 202.9,972.9 167.3,939.7 C126.8,902 128.6,839.9 130.1,821.7 C145.3,640.2 331.7,486.9 537.1,486.9 C621.4,486.9 702.6,512.1 771.8,559.6 C781.1,566 790.2,572.8 799,579.9 C811.6,590.2 813.5,608.8 803.3,621.4 C793,634 774.4,635.9 761.8,625.7 C754.3,619.6 746.4,613.7 738.5,608.2 C679.1,567.4 609.5,545.8 537.2,545.8 C361.2,545.8 201.8,674.4 189,826.5 C187,850.8 191.9,881.9 207.6,896.4 C227.7,915.2 272.4,917.5 286.3,916.8 L776,916.8 C792.3,916.8 805.5,930 805.5,946.3 C805.5,962.6 792.2,976 775.9,976 L287.6,976 C286.7,976.1 284.3,976.1 280.9,976.1 Z" id="Shape-Copy" transform="translate(469.680886, 731.500000) scale(-1, 1) translate(-469.680886, -731.500000) "></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

26
www/img/icon-unlink.svg Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>icon-link</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-link" transform="translate(1.000000, 1.000000)" stroke="#666677">
<g id="Page-1">
<g id="Icons">
<g id="ui-24px-outline-2_link-69" transform="translate(0.384615, 0.000000)">
<g id="Group-2" transform="translate(12.000000, 12.000000)">
<path d="M0.3,5.7 L5.7,0.3" id="Shape" transform="translate(3.000000, 3.000000) scale(-1, 1) translate(-3.000000, -3.000000) "></path>
<path d="M0.3,5.7 L5.7,0.3" id="Shape"></path>
</g>
<g id="Group">
<path d="M8.1,3.6 L10.35,1.35 C12.06,-0.36 14.94,-0.36 16.65,1.35 L16.65,1.35 C18.36,3.06 18.36,5.94 16.65,7.65 L14.4,9.9" id="Shape"></path>
<path d="M9.9,14.4 L7.65,16.65 C5.94,18.36 3.06,18.36 1.35,16.65 L1.35,16.65 C-0.36,14.94 -0.36,12.06 1.35,10.35 L3.6,8.1" id="Shape"></path>
<path d="M6.3,11.7 L11.7,6.3" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -39,4 +39,11 @@
</button>
</div>
</ion-content>
<account-selector
account-selector-title="accountSelectorTitle"
account-selector-accounts="accounts"
account-selector-selected-account="account"
account-selector-show="showAccounts"
account-selector-on-select="onAccountSelect">
</account-selector>
</ion-view>

View File

@ -0,0 +1,34 @@
<action-sheet action-sheet-show="show" class="account-selector">
<img class="back-arrow" src="img/icon-back-arrow.svg" ng-click="hide()">
<div class="header">{{title}}</div>
<a ng-repeat="a in accounts track by $index"
class="item item-icon-left item-big-icon-left item-icon-right account" ng-click="selectAccount(a)">
<i class="icon big-icon-svg">
<img src="img/icon-account.svg" class="bg">
</i>
<div class="account-inner">
<div class="account-details">
<div class="account-name">
{{a.firstName}} {{a.lastName}}
</div>
<p class="account-email" ng-if="a.email">
<span>{{a.email}}</span>
&nbsp;
</p>
</div>
</div>
</a>
<a class="item item-icon-left item-big-icon-left item-icon-right account" ng-click="selectAccount()">
<i class="icon big-icon-svg">
<img src="img/icon-account-add.svg" class="bg icon-add">
</i>
<div class="account-inner">
<div class="account-details">
<div class="account-add" translate>
Add account
</div>
</div>
</div>
</a>
</action-sheet>

View File

@ -1,24 +0,0 @@
<ion-view id="bitpayCardPreferences">
<ion-nav-bar class="bar-royal">
<ion-nav-back-button>
</ion-nav-back-button>
<ion-nav-title>BitPay Visa&reg; Cards</ion-nav-title>
</ion-nav-bar>
<ion-content>
<div class="list">
<div class="item item-divider" translate>
Cards
</div>
<div class="item item-icon-right" ng-repeat="card in bitpayCards">
<span class="item-title">
xxxx-xxxx-xxxx-{{card.lastFourDigits}}
</span>
<span class="item-subtitle">
{{card.email}}
</span>
<i class="icon ion-trash-b icon-hotspot assertive" ng-click="remove(card)"></i>
</div>
</div>
</ion-content>
</ion-view>

View File

@ -0,0 +1,37 @@
<ion-view id="bitpayServicesPreferences">
<ion-nav-bar class="bar-royal">
<ion-nav-back-button>
</ion-nav-back-button>
<ion-nav-title>BitPay</ion-nav-title>
</ion-nav-bar>
<ion-content>
<div class="list" ng-if="bitpayCards.length > 0">
<div class="item item-divider" translate>
BitPay Visa&reg; Cards
</div>
<div class="item item-icon-right" ng-repeat="card in bitpayCards">
<span class="item-title">
xxxx-xxxx-xxxx-{{card.lastFourDigits}}
</span>
<span class="item-subtitle">
{{card.email}}
</span>
<div class="icon icon-unlink icon-hotspot" ng-click="removeCard(card)"></div>
</div>
</div>
<div class="list">
<div class="item item-divider" translate>
Accounts
</div>
<div class="item item-icon-right" ng-repeat="account in bitpayAccounts">
<span class="item-title">
{{account.firstName}} {{account.lastName}}
</span>
<span class="item-subtitle">
{{account.email}}
</span>
<div class="icon icon-unlink icon-hotspot" ng-click="removeAccount(account)"></div>
</div>
</div>
</ion-content>
</ion-view>

View File

@ -117,12 +117,12 @@
</a>
<a class=" item item-icon-left item-icon-right"
ng-if="bitpayCards"
ui-sref="tabs.preferences.bitpayCard">
ng-if="bitpayAccounts || (bitpayCardEnabled && bitpayCards)"
ui-sref="tabs.preferences.bitpayServices">
<i class="icon big-icon-svg circle">
<div class="bg icon-bitpay-card"></div>
<div class="bg icon-bitpay"></div>
</i>
<span>BitPay Visa&reg; Card</span>
<span>BitPay</span>
<i class="icon bp-arrow-right"></i>
</a>