diff --git a/src/js/controllers/bitpayCardIntro.js b/src/js/controllers/bitpayCardIntro.js index 59eb0a29b..f7fcae4db 100644 --- a/src/js/controllers/bitpayCardIntro.js +++ b/src/js/controllers/bitpayCardIntro.js @@ -1,5 +1,5 @@ 'use strict'; -angular.module('copayApp.controllers').controller('bitpayCardIntroController', function($scope, $log, $state, $ionicHistory, storageService, externalLinkService, bitpayCardService, gettextCatalog, popupService, appIdentityService, bitpayService) { +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) { @@ -9,7 +9,7 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f otp: data.stateParams.otp }; var pairingReason = gettextCatalog.getString('add your BitPay Visa® card(s)'); - bitpayService.pair(pairData, pairingReason, function(err, paired, apiContext) { + bitpayAccountService.pair(pairData, pairingReason, function(err, paired, apiContext) { if (err) { popupService.showAlert(gettextCatalog.getString('Error'), err); return; @@ -22,29 +22,28 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f } // Set flag for nextStep storageService.setNextStep('BitpayCard', 'true', function(err) {}); - // Save data - bitpayCardService.setBitpayDebitCards(data, function(err) { - if (err) return; - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $state.go('tabs.home').then(function() { - if (data.cards[0]) { - $state.transitionTo('tabs.bitpayCard', { - id: data.cards[0].id - }); - } - }); + $ionicHistory.nextViewOptions({ + disableAnimate: true + }); + $state.go('tabs.home').then(function() { + if (data.cards[0]) { + $state.transitionTo('tabs.bitpayCard', { + id: data.cards[0].id + }); + } }); }); } }); - } else { - appIdentityService.getIdentity(bitpayService.getEnvironment(), 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'); + }); + }); + } + }; + }); diff --git a/src/js/controllers/preferencesBitpayCard.js b/src/js/controllers/preferencesBitpayCard.js deleted file mode 100644 index 6b43b62b6..000000000 --- a/src/js/controllers/preferencesBitpayCard.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('preferencesBitpayCardController', - function($scope, $state, $timeout, $ionicHistory, bitpayCardService, popupService, gettextCatalog) { - - $scope.remove = function(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(null, msg, null, null, function(res) { - if (res) remove(card); - }); - }; - - var remove = function(card) { - bitpayCardService.remove(card, function(err) { - if (err) { - return popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Could not remove card')); - } - $ionicHistory.clearHistory(); - $timeout(function() { - $state.go('tabs.home'); - }, 100); - }); - }; - - $scope.$on("$ionicView.beforeEnter", function(event, data) { - bitpayCardService.getBitpayDebitCards(function(err, data) { - if (err) return; - $scope.bitpayCards = data; - }); - }); - - }); diff --git a/src/js/controllers/preferencesBitpayServices.js b/src/js/controllers/preferencesBitpayServices.js new file mode 100644 index 000000000..c6e0343f5 --- /dev/null +++ b/src/js/controllers/preferencesBitpayServices.js @@ -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.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(); + }); + + }); diff --git a/src/js/controllers/tab-settings.js b/src/js/controllers/tab-settings.js index b5573155e..e93ecf00e 100644 --- a/src/js/controllers/tab-settings.js +++ b/src/js/controllers/tab-settings.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $ionicModal, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayCardService, storageService, glideraService, gettextCatalog) { +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; @@ -27,6 +27,11 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct $scope.bitpayCardEnabled = config.bitpayCard.enabled; $scope.glideraEnabled = config.glidera.enabled && !isWindowsPhoneApp; + bitpayAccountService.getAccounts(function(err, data) { + if (err) $log.error(err); + $scope.bitpayAccounts = !lodash.isEmpty(data); + }); + if ($scope.bitpayCardEnabled) { bitpayCardService.getBitpayDebitCards(function(err, cards) { if (err) $log.error(err); diff --git a/src/js/directives/accountSelector.js b/src/js/directives/accountSelector.js new file mode 100644 index 000000000..4e7937b4a --- /dev/null +++ b/src/js/directives/accountSelector.js @@ -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); + }; + } + }; + }); diff --git a/src/js/routes.js b/src/js/routes.js index 138c12ba2..e27e3fa4b 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1045,12 +1045,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' } } }); diff --git a/src/js/services/bitpayAccountService.js b/src/js/services/bitpayAccountService.js new file mode 100644 index 000000000..232bd17d8 --- /dev/null +++ b/src/js/services/bitpayAccountService.js @@ -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.{{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; + +}); diff --git a/src/js/services/bitpayCardService.js b/src/js/services/bitpayCardService.js index d26b35c9b..026656537 100644 --- a/src/js/services/bitpayCardService.js +++ b/src/js/services/bitpayCardService.js @@ -47,7 +47,14 @@ 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'); - return cb(data.data.error, {token: apiContext.token, cards: data.data.data, email: apiContext.pairData.email}); + // 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)); }); @@ -178,12 +185,24 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log, }); }; - root.remove = function(card, cb) { + root.removeCard = function(card, cb) { storageService.removeBitpayDebitCard(bitpayService.getEnvironment().network, card, function(err) { if (err) { $log.error('Error removing BitPay debit card: ' + err); - // Continue, try to remove/cleanup card history + // Continue, try to remove/cleanup next step and card history } + // 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); diff --git a/src/js/services/bitpayService.js b/src/js/services/bitpayService.js index d2dc831dc..28035c9b6 100644 --- a/src/js/services/bitpayService.js +++ b/src/js/services/bitpayService.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.services').factory('bitpayService', function($log, $http, platformInfo, appIdentityService, bitauthService, storageService, gettextCatalog, popupService) { +angular.module('copayApp.services').factory('bitpayService', function($log, $http, appIdentityService, bitauthService) { var root = {}; var NETWORK = 'livenet'; @@ -12,96 +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); - $http(_postAuth('/api/v2/', json, appIdentity)).then(function(data) { - if (data && data.data.error) return cb(data.data.error); - $log.info('BitPay service BitAuth create token: SUCCESS'); - 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.{{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: 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); @@ -112,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) { @@ -121,22 +33,17 @@ 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) { - var data = JSON.stringify(accountData); - storageService.setBitpayAccount(root.getEnvironment().network, data, function(err) { - if (err) return cb(err); - return cb(); }); }; @@ -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; }); diff --git a/src/js/services/configService.js b/src/js/services/configService.js index 5bd6c3cc6..028168a05 100644 --- a/src/js/services/configService.js +++ b/src/js/services/configService.js @@ -220,6 +220,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); diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index aafc4b759..bb8d310af 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -102,9 +102,10 @@ angular.module('copayApp.services') // match the upgrader key. // var _upgraders = { - '00_bitpayDebitCards' : _upgrade_bitpayDebitCards, // 2016-11: Upgrade bitpayDebitCards-x to bitpayAccounts-x - '01_bitpayCardCredentials' : _upgrade_bitpayCardCredentials, // 2016-11: Upgrade bitpayCardCredentials-x to appIdentity-x - '02_bitpayAccounts' : _upgrade_bitpayAccounts // 2016-12: Upgrade tpayAccounts-x to bitpayAccounts-v2-x + '00_bitpayDebitCards' : _upgrade_bitpayDebitCards, // 2016-11: Upgrade bitpayDebitCards-x to bitpayAccounts-x + '01_bitpayCardCredentials' : _upgrade_bitpayCardCredentials, // 2016-11: Upgrade bitpayCardCredentials-x to appIdentity-x + '02_bitpayAccounts' : _upgrade_bitpayAccounts, // 2016-12: Upgrade bitpayAccounts-x to bitpayAccounts-v2-x + '03_bitpayAccounts' : _upgrade_bitpayAccounts_basicInfo // 2017-01: Upgrade bitpayAccounts-v2-x to bitpayAccounts-v3-x }; function _upgrade_bitpayDebitCards(key, network, cb) { @@ -184,6 +185,44 @@ angular.module('copayApp.services') }); }); }; + + function _upgrade_bitpayAccounts_basicInfo(key, network, cb) { + key += '-' + network; + storage.get(key, function(err, data) { + if (err) return cb(err); + if (lodash.isString(data)) { + data = JSON.parse(data); + } + data = data || {}; + var upgraded = ''; + Object.keys(data).forEach(function(key) { + // Keys are account emails + if (!data[key]['basicInfo-' + network]) { + // Needs upgrade + upgraded += ' ' + key; + var acctData = { + email: key, + basicInfo: { + givenName: key, // Just set the first name to the account email + familyName: '' + } + }; + _03_setBitpayAccount(network, acctData, function(err) { + if (err) return cb(err); + }); + } + }); + // Remove obsolete key. + storage.remove('bitpayAccounts-v2-' + network, function() { + if (upgraded.length > 0) { + cb(null, 'upgraded to \'bitpayAccounts-v3-' + network + '\':' + upgraded); + } else { + cb(); + } + }); + }); + }; + // //////////////////////////////////////////////////////////////////////////// // @@ -248,6 +287,24 @@ angular.module('copayApp.services') storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb); }); }; + + var _03_setBitpayAccount = function(network, data, cb) { + if (lodash.isString(data)) { + data = JSON.parse(data); + } + data = data || {}; + if (lodash.isEmpty(data) || !data.email) return cb('No account to set'); + storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + if (err) return cb(err); + if (lodash.isString(bitpayAccounts)) { + bitpayAccounts = JSON.parse(bitpayAccounts); + } + bitpayAccounts = bitpayAccounts || {}; + bitpayAccounts[data.email] = bitpayAccounts[data.email] || {}; + bitpayAccounts[data.email]['basicInfo-' + network] = data.basicInfo; + storage.set('bitpayAccounts-v3-' + network, JSON.stringify(bitpayAccounts), cb); + }); + }; // //////////////////////////////////////////////////////////////////////////// @@ -589,7 +646,7 @@ angular.module('copayApp.services') } data = data || {}; if (lodash.isEmpty(data) || !data.email) return cb('Cannot set cards: no account to set'); - storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) { if (err) return cb(err); if (lodash.isString(bitpayAccounts)) { bitpayAccounts = JSON.parse(bitpayAccounts); @@ -597,7 +654,7 @@ angular.module('copayApp.services') bitpayAccounts = bitpayAccounts || {}; bitpayAccounts[data.email] = bitpayAccounts[data.email] || {}; bitpayAccounts[data.email]['bitpayDebitCards-' + network] = data.cards; - storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb); + storage.set('bitpayAccounts-v3-' + network, JSON.stringify(bitpayAccounts), cb); }); }; @@ -610,7 +667,7 @@ angular.module('copayApp.services') // email: account email // ] root.getBitpayDebitCards = function(network, cb) { - storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) { if (lodash.isString(bitpayAccounts)) { bitpayAccounts = JSON.parse(bitpayAccounts); } @@ -640,7 +697,7 @@ angular.module('copayApp.services') } card = card || {}; if (lodash.isEmpty(card) || !card.eid) return cb('No card to remove'); - storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) { if (err) cb(err); if (lodash.isString(bitpayAccounts)) { bitpayAccounts = JSON.parse(bitpayAccounts); @@ -655,16 +712,7 @@ angular.module('copayApp.services') data.cards = newCards; data.email = email; root.setBitpayDebitCards(network, data, function(err) { - if (err) cb(err); - // If there are no more cards in storage then re-enable the next step entry. - root.getBitpayDebitCards(network, function(err, cards) { - if (err) cb(err); - if (cards.length == 0) { - root.removeNextStep('BitpayCard', cb); - } else { - cb(); - } - }); + cb(err); }); }); }); @@ -680,7 +728,7 @@ angular.module('copayApp.services') } data = data || {}; if (lodash.isEmpty(data) || !data.email) return cb('No account to set'); - storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) { if (err) return cb(err); if (lodash.isString(bitpayAccounts)) { bitpayAccounts = JSON.parse(bitpayAccounts); @@ -689,7 +737,11 @@ angular.module('copayApp.services') bitpayAccounts[data.email] = bitpayAccounts[data.email] || {}; bitpayAccounts[data.email]['bitpayApi-' + network] = bitpayAccounts[data.email]['bitpayApi-' + network] || {}; bitpayAccounts[data.email]['bitpayApi-' + network].token = data.token; - storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb); + + bitpayAccounts[data.email]['basicInfo-' + network] = bitpayAccounts[data.email]['basicInfo-' + network] || {}; + bitpayAccounts[data.email]['basicInfo-' + network].givenName = data.givenName; + bitpayAccounts[data.email]['basicInfo-' + network].familyName = data.familyName; + storage.set('bitpayAccounts-v3-' + network, JSON.stringify(bitpayAccounts), cb); }); }; @@ -714,7 +766,7 @@ angular.module('copayApp.services') // } // } root.getBitpayAccounts = function(network, cb) { - storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) { + storage.get('bitpayAccounts-v3-' + network, function(err, bitpayAccounts) { if (err) return cb(err); if (lodash.isString(bitpayAccounts)) { bitpayAccounts = JSON.parse(bitpayAccounts); @@ -723,6 +775,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); }; diff --git a/src/sass/views/bitpayCardIntro.scss b/src/sass/views/bitpayCardIntro.scss index a70a73105..bbb7d769e 100644 --- a/src/sass/views/bitpayCardIntro.scss +++ b/src/sass/views/bitpayCardIntro.scss @@ -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; diff --git a/src/sass/views/bitpayCardPreferences.scss b/src/sass/views/bitpayServicesPreferences.scss similarity index 58% rename from src/sass/views/bitpayCardPreferences.scss rename to src/sass/views/bitpayServicesPreferences.scss index 7488f9964..11f0c43f3 100644 --- a/src/sass/views/bitpayCardPreferences.scss +++ b/src/sass/views/bitpayServicesPreferences.scss @@ -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; } } } diff --git a/src/sass/views/includes/accountSelector.scss b/src/sass/views/includes/accountSelector.scss new file mode 100644 index 000000000..220361051 --- /dev/null +++ b/src/sass/views/includes/accountSelector.scss @@ -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; + } + + } + } + +} diff --git a/src/sass/views/tab-settings.scss b/src/sass/views/tab-settings.scss index 7ecc5abc7..1f8492f59 100644 --- a/src/sass/views/tab-settings.scss +++ b/src/sass/views/tab-settings.scss @@ -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; diff --git a/src/sass/views/views.scss b/src/sass/views/views.scss index 351a93298..5b19318c9 100644 --- a/src/sass/views/views.scss +++ b/src/sass/views/views.scss @@ -14,7 +14,7 @@ @import "advancedSettings"; @import "bitpayCard"; @import "bitpayCardIntro"; -@import "bitpayCardPreferences"; +@import "bitpayServicesPreferences"; @import "address-book"; @import "addresses"; @import "wallet-backup-phrase"; @@ -41,6 +41,7 @@ @import "includes/tx-status"; @import "includes/itemSelector"; @import "includes/walletSelector"; +@import "includes/accountSelector"; @import "integrations/coinbase"; @import "integrations/glidera"; @import "custom-amount"; diff --git a/www/img/icon-account-add.svg b/www/img/icon-account-add.svg new file mode 100644 index 000000000..826fbec29 --- /dev/null +++ b/www/img/icon-account-add.svg @@ -0,0 +1,17 @@ + + + + Untitled + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/www/img/icon-account.svg b/www/img/icon-account.svg new file mode 100644 index 000000000..91c6e890f --- /dev/null +++ b/www/img/icon-account.svg @@ -0,0 +1,14 @@ + + + + Untitled + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/www/img/icon-unlink.svg b/www/img/icon-unlink.svg new file mode 100644 index 000000000..1330e50c3 --- /dev/null +++ b/www/img/icon-unlink.svg @@ -0,0 +1,26 @@ + + + + icon-link + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/views/bitpayCardIntro.html b/www/views/bitpayCardIntro.html index 3a972c86c..28d777d76 100644 --- a/www/views/bitpayCardIntro.html +++ b/www/views/bitpayCardIntro.html @@ -39,4 +39,11 @@ + + diff --git a/www/views/includes/accountSelector.html b/www/views/includes/accountSelector.html new file mode 100644 index 000000000..a8da1fb1e --- /dev/null +++ b/www/views/includes/accountSelector.html @@ -0,0 +1,34 @@ + + + {{title}} + + + + + + + + {{a.firstName}} {{a.lastName}} + + + {{a.email}} + + + + + + + + + + + + + Add account + + + + + + diff --git a/www/views/preferencesBitpayCard.html b/www/views/preferencesBitpayCard.html deleted file mode 100644 index 138327698..000000000 --- a/www/views/preferencesBitpayCard.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - BitPay Visa® Cards - - - - - - Cards - - - - xxxx-xxxx-xxxx-{{card.lastFourDigits}} - - - {{card.email}} - - - - - - diff --git a/www/views/preferencesBitpayServices.html b/www/views/preferencesBitpayServices.html new file mode 100644 index 000000000..7fe4a5994 --- /dev/null +++ b/www/views/preferencesBitpayServices.html @@ -0,0 +1,37 @@ + + + + + BitPay + + + + + BitPay Visa® Cards + + + + xxxx-xxxx-xxxx-{{card.lastFourDigits}} + + + {{card.email}} + + + + + + + Accounts + + + + {{account.firstName}} {{account.lastName}} + + + {{account.email}} + + + + + + diff --git a/www/views/tab-settings.html b/www/views/tab-settings.html index 92a0c0eae..86aad67e9 100644 --- a/www/views/tab-settings.html +++ b/www/views/tab-settings.html @@ -117,12 +117,12 @@ + ng-if="bitpayAccounts || (bitpayCardEnabled && bitpayCards)" + ui-sref="tabs.preferences.bitpayServices"> - + - BitPay Visa® Card + BitPay
+ {{a.email}} + +