mirror of https://github.com/BTCPrivate/copay.git
Merge branch 'master' of github.com:bitpay/copay into feature/better-buy-and-sell
This commit is contained in:
commit
aaf9008394
|
@ -21,8 +21,8 @@
|
|||
"windowsAppId": "2d1002d7-ee34-4f60-bd29-0c871ba0c195",
|
||||
"pushSenderId": "1036948132229",
|
||||
"description": "Secure Bitcoin Wallet",
|
||||
"version": "1.3.2",
|
||||
"androidVersion": "132000",
|
||||
"version": "1.3.4",
|
||||
"androidVersion": "134000",
|
||||
"_extraCSS": null,
|
||||
"_enabledExtensions": {
|
||||
"coinbase": true,
|
||||
|
|
|
@ -3,27 +3,18 @@
|
|||
angular.module('copayApp.controllers').controller('advancedSettingsController', function($scope, $rootScope, $log, $window, lodash, configService, uxLanguage, platformInfo, pushNotificationsService, profileService, feeService, storageService, $ionicHistory, $timeout, $ionicScrollDelegate) {
|
||||
|
||||
var updateConfig = function() {
|
||||
|
||||
var config = configService.getSync();
|
||||
|
||||
$scope.spendUnconfirmed = {
|
||||
value: config.wallet.spendUnconfirmed
|
||||
};
|
||||
$scope.bitpayCardEnabled = {
|
||||
value: config.bitpayCard.enabled
|
||||
};
|
||||
$scope.amazonEnabled = {
|
||||
value: config.amazon.enabled
|
||||
};
|
||||
$scope.glideraEnabled = {
|
||||
value: config.glidera.enabled
|
||||
};
|
||||
$scope.coinbaseEnabled = {
|
||||
value: config.coinbaseV2
|
||||
};
|
||||
$scope.recentTransactionsEnabled = {
|
||||
value: config.recentTransactions.enabled
|
||||
};
|
||||
|
||||
$scope.hideNextSteps = {
|
||||
value: config.hideNextSteps.enabled
|
||||
};
|
||||
};
|
||||
|
||||
$scope.spendUnconfirmedChange = function() {
|
||||
|
@ -37,42 +28,11 @@ angular.module('copayApp.controllers').controller('advancedSettingsController',
|
|||
});
|
||||
};
|
||||
|
||||
$scope.bitpayCardChange = function() {
|
||||
$scope.nextStepsChange = function() {
|
||||
var opts = {
|
||||
bitpayCard: {
|
||||
enabled: $scope.bitpayCardEnabled.value
|
||||
}
|
||||
};
|
||||
configService.set(opts, function(err) {
|
||||
if (err) $log.debug(err);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.amazonChange = function() {
|
||||
var opts = {
|
||||
amazon: {
|
||||
enabled: $scope.amazonEnabled.value
|
||||
}
|
||||
};
|
||||
configService.set(opts, function(err) {
|
||||
if (err) $log.debug(err);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.glideraChange = function() {
|
||||
var opts = {
|
||||
glidera: {
|
||||
enabled: $scope.glideraEnabled.value
|
||||
}
|
||||
};
|
||||
configService.set(opts, function(err) {
|
||||
if (err) $log.debug(err);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.coinbaseChange = function() {
|
||||
var opts = {
|
||||
coinbaseV2: $scope.coinbaseEnabled.value
|
||||
hideNextSteps: {
|
||||
enabled: $scope.hideNextSteps.value
|
||||
},
|
||||
};
|
||||
configService.set(opts, function(err) {
|
||||
if (err) $log.debug(err);
|
||||
|
|
|
@ -9,16 +9,6 @@ angular.module('copayApp.controllers').controller('bitpayCardController', functi
|
|||
};
|
||||
$scope.network = bitpayService.getEnvironment().network;
|
||||
|
||||
var updateHistoryFromCache = function(cb) {
|
||||
bitpayCardService.getBitpayDebitCardsHistory($scope.cardId, function(err, data) {
|
||||
if (err || lodash.isEmpty(data)) return cb();
|
||||
$scope.historyCached = true;
|
||||
self.bitpayCardTransactionHistory = data.transactions;
|
||||
self.bitpayCardCurrentBalance = data.balance;
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
var setDateRange = function(preset) {
|
||||
var startDate, endDate;
|
||||
preset = preset || 'last30Days';
|
||||
|
@ -45,13 +35,19 @@ angular.module('copayApp.controllers').controller('bitpayCardController', functi
|
|||
};
|
||||
|
||||
var setGetStarted = function(history, cb) {
|
||||
if (lodash.isEmpty(history.transactionList)) {
|
||||
var dateRange = setDateRange('all');
|
||||
bitpayCardService.getHistory($scope.cardId, dateRange, function(err, history) {
|
||||
if (lodash.isEmpty(history.transactionList)) return cb(true);
|
||||
return cb(false);
|
||||
});
|
||||
} else return cb(false);
|
||||
|
||||
// Is the card new?
|
||||
if (!lodash.isEmpty(history.transactionList))
|
||||
return cb();
|
||||
|
||||
var dateRange = setDateRange('all');
|
||||
bitpayCardService.getHistory($scope.cardId, dateRange, function(err, history) {
|
||||
|
||||
if (!err && lodash.isEmpty(history.transactionList))
|
||||
self.getStated=true;
|
||||
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
this.update = function() {
|
||||
|
@ -59,18 +55,18 @@ angular.module('copayApp.controllers').controller('bitpayCardController', functi
|
|||
|
||||
$scope.loadingHistory = true;
|
||||
bitpayCardService.getHistory($scope.cardId, dateRange, function(err, history) {
|
||||
|
||||
$scope.loadingHistory = false;
|
||||
|
||||
if (err) {
|
||||
$log.error(err);
|
||||
self.bitpayCardTransactionHistory = null;
|
||||
self.bitpayCardCurrentBalance = null;
|
||||
self.balance = null;
|
||||
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Could not get transactions'));
|
||||
return;
|
||||
}
|
||||
|
||||
setGetStarted(history, function(getStarted) {
|
||||
self.getStarted = getStarted;
|
||||
setGetStarted(history, function() {
|
||||
|
||||
var txs = lodash.clone(history.txs);
|
||||
runningBalance = parseFloat(history.endingBalance);
|
||||
|
@ -83,18 +79,21 @@ angular.module('copayApp.controllers').controller('bitpayCardController', functi
|
|||
_runningBalance(txs[i]);
|
||||
}
|
||||
self.bitpayCardTransactionHistory = txs;
|
||||
self.bitpayCardCurrentBalance = history.currentCardBalance;
|
||||
self.balance = history.currentCardBalance;
|
||||
self.updatedOn = null;
|
||||
|
||||
if ($scope.dateRange.value == 'last30Days') {
|
||||
$log.debug('BitPay Card: store cache history');
|
||||
var cacheHistory = {
|
||||
balance: history.currentCardBalance,
|
||||
transactions: history.txs
|
||||
};
|
||||
bitpayCardService.setBitpayDebitCardsHistory($scope.cardId, cacheHistory, {}, function(err) {
|
||||
if (err) $log.error(err);
|
||||
$scope.historyCached = true;
|
||||
});
|
||||
|
||||
// TODO?
|
||||
// $log.debug('BitPay Card: storing cache history');
|
||||
// var cacheHistory = {
|
||||
// balance: history.currentCardBalance,
|
||||
// transactions: history.txs
|
||||
// };
|
||||
// bitpayCardService.setHistory($scope.cardId, cacheHistory, {}, function(err) {
|
||||
// if (err) $log.error(err);
|
||||
// $scope.historyCached = true;
|
||||
// });
|
||||
}
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
|
@ -136,24 +135,26 @@ angular.module('copayApp.controllers').controller('bitpayCardController', functi
|
|||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
$scope.cardId = data.stateParams.id;
|
||||
|
||||
if (!$scope.cardId) {
|
||||
var msg = gettextCatalog.getString('Bad param');
|
||||
$ionicHistory.nextViewOptions({
|
||||
disableAnimate: true
|
||||
});
|
||||
$state.go('tabs.home');
|
||||
popupService.showAlert(gettextCatalog.getString('Error'), msg);
|
||||
} else {
|
||||
updateHistoryFromCache(function() {
|
||||
self.update();
|
||||
});
|
||||
bitpayCardService.getBitpayDebitCards(function(err, cards) {
|
||||
if (err) return;
|
||||
$scope.card = lodash.find(cards, function(card) {
|
||||
return card.eid == $scope.cardId;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
bitpayCardService.get({
|
||||
cardId: $scope.cardId,
|
||||
noRefresh: true,
|
||||
}, function(err, cards) {
|
||||
|
||||
if (cards && cards[0]) {
|
||||
self.lastFourDigits = cards[0].lastFourDigits;
|
||||
self.balance = cards[0].balance;
|
||||
self.updatedOn = cards[0].updatedOn;
|
||||
}
|
||||
self.update();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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, appIdentityService, bitpayService, lodash) {
|
||||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
if (data.stateParams && data.stateParams.secret) {
|
||||
|
@ -18,27 +18,23 @@ angular.module('copayApp.controllers').controller('bitpayCardIntroController', f
|
|||
return;
|
||||
}
|
||||
if (paired) {
|
||||
bitpayCardService.fetchBitpayDebitCards(apiContext, function(err, data) {
|
||||
|
||||
bitpayCardService.sync(apiContext, function(err, cards) {
|
||||
if (err) {
|
||||
popupService.showAlert(gettextCatalog.getString('Error fetching Debit Cards'), err);
|
||||
popupService.showAlert(gettextCatalog.getString('Error updating Debit Cards'), err);
|
||||
return;
|
||||
}
|
||||
// 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 (cards[0]) {
|
||||
$state.transitionTo('tabs.bitpayCard', {
|
||||
id: cards[0].id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('buyAndSellCardController', function($scope, nextStepsService, $ionicScrollDelegate, buyAndSellService) {
|
||||
|
||||
$scope.services = buyAndSellService.getLinked();
|
||||
|
||||
$scope.toggle = function() {
|
||||
$scope.hide = !$scope.hide;
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
};
|
||||
});
|
|
@ -34,8 +34,6 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct
|
|||
};
|
||||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
coinbaseService.setCredentials();
|
||||
|
||||
$scope.isFiat = data.stateParams.currency ? true : false;
|
||||
[amount, currency, $scope.amountUnitStr] = coinbaseService.parseAmount(
|
||||
data.stateParams.amount,
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('buyandsellController', function($scope, $ionicHistory, configService) {
|
||||
angular.module('copayApp.controllers').controller('buyandsellController', function($scope, $ionicHistory, buyAndSellService, lodash) {
|
||||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
configService.whenAvailable(function(config) {
|
||||
$scope.isCoinbaseEnabled = config.coinbaseV2;
|
||||
$scope.isGlideraEnabled = config.glidera.enabled;
|
||||
$scope.services = buyAndSellService.get();
|
||||
|
||||
if (!$scope.isCoinbaseEnabled && !$scope.isGlideraEnabled)
|
||||
$ionicHistory.goBack();
|
||||
});
|
||||
if (lodash.isEmpty($scope.services))
|
||||
$ionicHistory.goBack();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -137,7 +137,6 @@ angular.module('copayApp.controllers').controller('coinbaseController', function
|
|||
var self = this;
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
$scope.showOauthForm = false;
|
||||
coinbaseService.setCredentials();
|
||||
if (data.stateParams && data.stateParams.code) {
|
||||
coinbaseService.getStoredToken(function(at) {
|
||||
if (!at) self.submitOauthCode(data.stateParams.code);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('homeIntegrationsController', function($scope, homeIntegrationsService, $ionicScrollDelegate, $timeout) {
|
||||
|
||||
$scope.hide = false;
|
||||
$scope.services = homeIntegrationsService.get();
|
||||
|
||||
$scope.toggle = function() {
|
||||
$scope.hide = !$scope.hide;
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('nextStepsController', function($scope, nextStepsService, $ionicScrollDelegate, $timeout) {
|
||||
|
||||
$scope.hide = false;
|
||||
$scope.services = nextStepsService.get();
|
||||
|
||||
$scope.toggle = function() {
|
||||
$scope.hide = !$scope.hide;
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
});
|
|
@ -1,19 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('preferencesBitpayCardController',
|
||||
function($scope, $state, $timeout, $ionicHistory, bitpayCardService, popupService, gettextCatalog) {
|
||||
function($scope, $state, $timeout, $ionicHistory, bitpayCardService, popupService, gettextCatalog, $log) {
|
||||
|
||||
$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);
|
||||
$log.info('Removing bitpay card:' + card.eid)
|
||||
if (res)
|
||||
remove(card.eid);
|
||||
});
|
||||
};
|
||||
|
||||
var remove = function(card) {
|
||||
bitpayCardService.remove(card, function(err) {
|
||||
var remove = function(cardEid) {
|
||||
bitpayCardService.remove(cardEid, function(err) {
|
||||
if (err) {
|
||||
return popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Could not remove card'));
|
||||
}
|
||||
|
@ -25,8 +27,9 @@ angular.module('copayApp.controllers').controller('preferencesBitpayCardControll
|
|||
};
|
||||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
bitpayCardService.getBitpayDebitCards(function(err, data) {
|
||||
bitpayCardService.getCards(function(err, data) {
|
||||
if (err) return;
|
||||
|
||||
$scope.bitpayCards = data;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ angular.module('copayApp.controllers').controller('preferencesCoinbaseController
|
|||
};
|
||||
|
||||
$scope.$on("$ionicView.enter", function(event, data){
|
||||
coinbaseService.setCredentials();
|
||||
ongoingProcess.set('connectingCoinbase', true);
|
||||
coinbaseService.init(function(err, data) {
|
||||
if (err || lodash.isEmpty(data)) {
|
||||
|
|
|
@ -117,8 +117,6 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', func
|
|||
};
|
||||
|
||||
$scope.$on("$ionicView.beforeEnter", function(event, data) {
|
||||
coinbaseService.setCredentials();
|
||||
|
||||
$scope.isFiat = data.stateParams.currency ? true : false;
|
||||
[amount, currency, $scope.amountUnitStr] = coinbaseService.parseAmount(
|
||||
data.stateParams.amount,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('tabHomeController',
|
||||
function($rootScope, $timeout, $scope, $state, $stateParams, $ionicModal, $ionicScrollDelegate, $window, gettextCatalog, lodash, popupService, ongoingProcess, externalLinkService, latestReleaseService, profileService, walletService, configService, $log, platformInfo, storageService, txpModalService, appConfigService, bitpayCardService, startupService, addressbookService, feedbackService, bwcError, coinbaseService) {
|
||||
function($rootScope, $timeout, $scope, $state, $stateParams, $ionicModal, $ionicScrollDelegate, $window, gettextCatalog, lodash, popupService, ongoingProcess, externalLinkService, latestReleaseService, profileService, walletService, configService, $log, platformInfo, storageService, txpModalService, appConfigService, startupService, addressbookService, feedbackService, bwcError, nextStepsService, buyAndSellService, homeIntegrationsService, bitpayCardService) {
|
||||
var wallet;
|
||||
var listeners = [];
|
||||
var notifications = [];
|
||||
|
@ -87,10 +87,7 @@ angular.module('copayApp.controllers').controller('tabHomeController',
|
|||
var wallet = profileService.getWallet(walletId);
|
||||
updateWallet(wallet);
|
||||
if ($scope.recentTransactionsEnabled) getNotifications();
|
||||
if ($scope.coinbaseEnabled && type == 'NewBlock' && n && n.data && n.data.network == 'livenet') {
|
||||
// Update Coinbase
|
||||
coinbaseService.updatePendingTransactions();
|
||||
}
|
||||
|
||||
}),
|
||||
$rootScope.$on('Local/TxAction', function(e, walletId) {
|
||||
$log.debug('Got action for wallet ' + walletId);
|
||||
|
@ -100,31 +97,29 @@ angular.module('copayApp.controllers').controller('tabHomeController',
|
|||
})
|
||||
];
|
||||
|
||||
|
||||
$scope.buyAndSellItems = buyAndSellService.getLinked();
|
||||
$scope.homeIntegrations = homeIntegrationsService.get();
|
||||
|
||||
bitpayCardService.get({}, function(err, cards) {
|
||||
$scope.bitpayCardItems = cards;
|
||||
});
|
||||
|
||||
configService.whenAvailable(function() {
|
||||
nextStep(function() {
|
||||
var config = configService.getSync();
|
||||
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova;
|
||||
var config = configService.getSync();
|
||||
$scope.recentTransactionsEnabled = config.recentTransactions.enabled;
|
||||
if ($scope.recentTransactionsEnabled) getNotifications();
|
||||
|
||||
$scope.glideraEnabled = config.glidera.enabled && !isWindowsPhoneApp;
|
||||
$scope.coinbaseEnabled = config.coinbaseV2 && !isWindowsPhoneApp;
|
||||
$scope.amazonEnabled = config.amazon.enabled;
|
||||
$scope.bitpayCardEnabled = config.bitpayCard.enabled;
|
||||
if (config.hideNextSteps.enabled) {
|
||||
$scope.nextStepsItems = null;
|
||||
} else {
|
||||
$scope.nextStepsItems = nextStepsService.get();
|
||||
}
|
||||
|
||||
var buyAndSellEnabled = !$scope.externalServices.BuyAndSell && ($scope.glideraEnabled || $scope.coinbaseEnabled);
|
||||
var amazonEnabled = !$scope.externalServices.AmazonGiftCards && $scope.amazonEnabled;
|
||||
var bitpayCardEnabled = !$scope.externalServices.BitpayCard && $scope.bitpayCardEnabled;
|
||||
|
||||
$scope.nextStepEnabled = buyAndSellEnabled || amazonEnabled || bitpayCardEnabled;
|
||||
$scope.recentTransactionsEnabled = config.recentTransactions.enabled;
|
||||
|
||||
if ($scope.recentTransactionsEnabled) getNotifications();
|
||||
|
||||
if ($scope.bitpayCardEnabled) bitpayCardCache();
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
});
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -223,6 +218,9 @@ angular.module('copayApp.controllers').controller('tabHomeController',
|
|||
} else {
|
||||
wallet.error = null;
|
||||
wallet.status = status;
|
||||
|
||||
// TODO service refactor? not in profile service
|
||||
profileService.setLastKnownBalance(wallet.id, wallet.status.totalBalanceStr, function() {});
|
||||
}
|
||||
if (++j == i) {
|
||||
updateTxps();
|
||||
|
@ -269,46 +267,6 @@ angular.module('copayApp.controllers').controller('tabHomeController',
|
|||
});
|
||||
};
|
||||
|
||||
var nextStep = function(cb) {
|
||||
var i = 0;
|
||||
var services = ['AmazonGiftCards', 'BitpayCard', 'BuyAndSell'];
|
||||
lodash.each(services, function(service) {
|
||||
storageService.getNextStep(service, function(err, value) {
|
||||
$scope.externalServices[service] = value == 'true' ? true : false;
|
||||
if (++i == services.length) return cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.shouldHideNextSteps = function() {
|
||||
$scope.hideNextSteps = !$scope.hideNextSteps;
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
$scope.$apply();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
var bitpayCardCache = function() {
|
||||
bitpayCardService.getBitpayDebitCards(function(err, data) {
|
||||
if (err) return;
|
||||
if (lodash.isEmpty(data)) {
|
||||
$scope.bitpayCards = null;
|
||||
return;
|
||||
}
|
||||
$scope.bitpayCards = data;
|
||||
$timeout(function() {
|
||||
$scope.$digest();
|
||||
}, 100);
|
||||
});
|
||||
bitpayCardService.getBitpayDebitCardsHistory(null, function(err, data) {
|
||||
if (err) return;
|
||||
if (lodash.isEmpty(data)) {
|
||||
$scope.cardsHistory = null;
|
||||
return;
|
||||
}
|
||||
$scope.cardsHistory = data;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onRefresh = function() {
|
||||
$timeout(function() {
|
||||
|
|
|
@ -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) {
|
||||
angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayCardService, storageService, glideraService, coinbaseService, gettextCatalog, buyAndSellService) {
|
||||
|
||||
var updateConfig = function() {
|
||||
var isCordova = platformInfo.isCordova;
|
||||
|
@ -16,6 +16,7 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct
|
|||
$scope.currentFeeLevel = feeService.getCurrentFeeLevel();
|
||||
|
||||
$scope.wallets = profileService.getWallets();
|
||||
$scope.buyAndSellServices = buyAndSellService.getLinked();
|
||||
|
||||
configService.whenAvailable(function(config) {
|
||||
$scope.unitName = config.wallet.settings.unitName;
|
||||
|
@ -24,30 +25,11 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct
|
|||
isoCode: config.wallet.settings.alternativeIsoCode
|
||||
};
|
||||
|
||||
$scope.bitpayCardEnabled = config.bitpayCard.enabled;
|
||||
$scope.glideraEnabled = config.glidera.enabled && !isWindowsPhoneApp;
|
||||
$scope.coinbaseEnabled = config.coinbaseV2 && !isWindowsPhoneApp;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.coinbaseEnabled) {
|
||||
coinbaseService.setCredentials();
|
||||
coinbaseService.getStoredToken(function(at) {
|
||||
$scope.coinbaseToken = at;
|
||||
});
|
||||
}
|
||||
// TODO move this to a generic service
|
||||
bitpayCardService.getCards(function(err, cards) {
|
||||
if (err) $log.error(err);
|
||||
$scope.bitpayCards = cards && cards.length > 0;
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1088,7 +1088,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
|
|||
}
|
||||
});
|
||||
})
|
||||
.run(function($rootScope, $state, $location, $log, $timeout, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService) {
|
||||
.run(function($rootScope, $state, $location, $log, $timeout, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, /* plugins START HERE => */ coinbaseService, glideraService, amazonService, bitpayCardService) {
|
||||
|
||||
uxLanguage.init();
|
||||
|
||||
|
@ -1153,50 +1153,40 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
|
|||
});
|
||||
}
|
||||
|
||||
$log.info('Verifying storage...');
|
||||
storageService.verify(function(err) {
|
||||
$log.info('Init profile...');
|
||||
// Try to open local profile
|
||||
profileService.loadAndBindProfile(function(err) {
|
||||
$ionicHistory.nextViewOptions({
|
||||
disableAnimate: true
|
||||
});
|
||||
if (err) {
|
||||
$log.error('Storage failed to verify: ' + err);
|
||||
// TODO - what next?
|
||||
} else {
|
||||
$log.info('Storage OK');
|
||||
}
|
||||
|
||||
$log.info('Init profile...');
|
||||
// Try to open local profile
|
||||
profileService.loadAndBindProfile(function(err) {
|
||||
$ionicHistory.nextViewOptions({
|
||||
disableAnimate: true
|
||||
});
|
||||
if (err) {
|
||||
if (err.message && err.message.match('NOPROFILE')) {
|
||||
$log.debug('No profile... redirecting');
|
||||
if (err.message && err.message.match('NOPROFILE')) {
|
||||
$log.debug('No profile... redirecting');
|
||||
$state.go('onboarding.welcome');
|
||||
} else if (err.message && err.message.match('NONAGREEDDISCLAIMER')) {
|
||||
if (lodash.isEmpty(profileService.getWallets())) {
|
||||
$log.debug('No wallets and no disclaimer... redirecting');
|
||||
$state.go('onboarding.welcome');
|
||||
} else if (err.message && err.message.match('NONAGREEDDISCLAIMER')) {
|
||||
if (lodash.isEmpty(profileService.getWallets())) {
|
||||
$log.debug('No wallets and no disclaimer... redirecting');
|
||||
$state.go('onboarding.welcome');
|
||||
} else {
|
||||
$log.debug('Display disclaimer... redirecting');
|
||||
$state.go('onboarding.disclaimer', {
|
||||
resume: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(err); // TODO
|
||||
$log.debug('Display disclaimer... redirecting');
|
||||
$state.go('onboarding.disclaimer', {
|
||||
resume: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
profileService.storeProfileIfDirty();
|
||||
$log.debug('Profile loaded ... Starting UX.');
|
||||
scannerService.gentleInitialize();
|
||||
$state.go('tabs.home');
|
||||
throw new Error(err); // TODO
|
||||
}
|
||||
} else {
|
||||
profileService.storeProfileIfDirty();
|
||||
$log.debug('Profile loaded ... Starting UX.');
|
||||
scannerService.gentleInitialize();
|
||||
$state.go('tabs.home');
|
||||
}
|
||||
|
||||
// After everything have been loaded, initialize handler URL
|
||||
$timeout(function() {
|
||||
openURLService.init();
|
||||
}, 1000);
|
||||
});
|
||||
// After everything have been loaded, initialize handler URL
|
||||
$timeout(function() {
|
||||
openURLService.init();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,24 +1,36 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services').factory('amazonService', function($http, $log, lodash, moment, storageService, configService, platformInfo) {
|
||||
angular.module('copayApp.services').factory('amazonService', function($http, $log, lodash, moment, storageService, configService, platformInfo, nextStepsService, homeIntegrationsService) {
|
||||
var root = {};
|
||||
var credentials = {};
|
||||
|
||||
var _setCredentials = function() {
|
||||
/*
|
||||
* Development: 'testnet'
|
||||
* Production: 'livenet'
|
||||
*/
|
||||
credentials.NETWORK = 'livenet';
|
||||
/*
|
||||
* Development: 'testnet'
|
||||
* Production: 'livenet'
|
||||
*/
|
||||
credentials.NETWORK = 'livenet';
|
||||
//credentials.NETWORK = 'testnet';
|
||||
|
||||
if (credentials.NETWORK == 'testnet') {
|
||||
credentials.BITPAY_API_URL = "https://test.bitpay.com";
|
||||
} else {
|
||||
credentials.BITPAY_API_URL = "https://bitpay.com";
|
||||
};
|
||||
if (credentials.NETWORK == 'testnet') {
|
||||
credentials.BITPAY_API_URL = "https://test.bitpay.com";
|
||||
} else {
|
||||
credentials.BITPAY_API_URL = "https://bitpay.com";
|
||||
};
|
||||
|
||||
var homeItem = {
|
||||
name: 'amazon',
|
||||
title: 'Amazon Gift Cards',
|
||||
icon: 'icon-amazon',
|
||||
sref: 'tabs.giftcards.amazon',
|
||||
};
|
||||
|
||||
var nextStepItem = {
|
||||
name: 'amazon',
|
||||
title: 'Buy a gift card',
|
||||
icon: 'icon-amazon',
|
||||
sref: 'tabs.giftcards.amazon',
|
||||
};
|
||||
|
||||
var _getBitPay = function(endpoint) {
|
||||
_setCredentials();
|
||||
return {
|
||||
method: 'GET',
|
||||
url: credentials.BITPAY_API_URL + endpoint,
|
||||
|
@ -29,7 +41,6 @@ angular.module('copayApp.services').factory('amazonService', function($http, $lo
|
|||
};
|
||||
|
||||
var _postBitPay = function(endpoint, data) {
|
||||
_setCredentials();
|
||||
return {
|
||||
method: 'POST',
|
||||
url: credentials.BITPAY_API_URL + endpoint,
|
||||
|
@ -41,7 +52,6 @@ angular.module('copayApp.services').factory('amazonService', function($http, $lo
|
|||
};
|
||||
|
||||
root.getNetwork = function() {
|
||||
_setCredentials();
|
||||
return credentials.NETWORK;
|
||||
};
|
||||
|
||||
|
@ -65,12 +75,12 @@ angular.module('copayApp.services').factory('amazonService', function($http, $lo
|
|||
inv = JSON.stringify(inv);
|
||||
|
||||
storageService.setAmazonGiftCards(network, inv, function(err) {
|
||||
|
||||
homeIntegrationsService.register(homeItem);
|
||||
nextStepsService.unregister(nextStepItem.name);
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Show pending task from the UI
|
||||
storageService.setNextStep('AmazonGiftCards', 'true', function(err) {});
|
||||
};
|
||||
|
||||
root.getPendingGiftCards = function(cb) {
|
||||
|
@ -144,6 +154,16 @@ angular.module('copayApp.services').factory('amazonService', function($http, $lo
|
|||
});
|
||||
};
|
||||
|
||||
return root;
|
||||
var register = function() {
|
||||
storageService.getAmazonGiftCards(root.getNetwork(), function(err, giftCards) {
|
||||
if (giftCards) {
|
||||
homeIntegrationsService.register(homeItem);
|
||||
} else {
|
||||
nextStepsService.register(nextStepItem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
register();
|
||||
return root;
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').factory('bitpayCardService', function($log, $rootScope, lodash, storageService, bitauthService, platformInfo, moment, appIdentityService, bitpayService) {
|
||||
angular.module('copayApp.services').factory('bitpayCardService', function($log, $rootScope, lodash, storageService, bitauthService, platformInfo, moment, appIdentityService, bitpayService, nextStepsService) {
|
||||
var root = {};
|
||||
|
||||
var _setError = function(msg, e) {
|
||||
|
@ -10,7 +10,7 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
};
|
||||
|
||||
var _processTransactions = function(invoices, history) {
|
||||
invoices = invoices || [];
|
||||
invoices = invoices || [];
|
||||
for (var i = 0; i < invoices.length; i++) {
|
||||
var matched = false;
|
||||
for (var j = 0; j < history.length; j++) {
|
||||
|
@ -22,8 +22,8 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
if (!matched && isInvoiceLessThanOneDayOld) {
|
||||
var isInvoiceUnderpaid = invoices[i].exceptionStatus === 'paidPartial';
|
||||
|
||||
if(['paid', 'confirmed', 'complete'].indexOf(invoices[i].status) >= 0
|
||||
|| (invoices[i].status === 'invalid' || isInvoiceUnderpaid)) {
|
||||
if (['paid', 'confirmed', 'complete'].indexOf(invoices[i].status) >= 0 ||
|
||||
(invoices[i].status === 'invalid' || isInvoiceUnderpaid)) {
|
||||
|
||||
history.unshift({
|
||||
timestamp: new Date(invoices[i].invoiceTime),
|
||||
|
@ -39,7 +39,7 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
return history;
|
||||
};
|
||||
|
||||
root.fetchBitpayDebitCards = function(apiContext, cb) {
|
||||
root.sync = function(apiContext, cb) {
|
||||
var json = {
|
||||
method: 'getDebitCards'
|
||||
};
|
||||
|
@ -47,65 +47,109 @@ 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});
|
||||
|
||||
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);
|
||||
});
|
||||
}, function(data) {
|
||||
return cb(_setError('BitPay Card Error: Get Debit Cards', data));
|
||||
});
|
||||
};
|
||||
|
||||
root.getHistory = function(cardId, params, cb) {
|
||||
// opts: range
|
||||
root.getHistory = function(cardId, opts, cb) {
|
||||
var invoices, transactions;
|
||||
params = params || {};
|
||||
opts = opts || {};
|
||||
|
||||
var json = {
|
||||
method: 'getInvoiceHistory',
|
||||
params: JSON.stringify(params)
|
||||
params: JSON.stringify(opts)
|
||||
};
|
||||
|
||||
appIdentityService.getIdentity(bitpayService.getEnvironment().network, function(err, appIdentity) {
|
||||
if (err) return cb(err);
|
||||
root.getBitpayDebitCards(function(err, data) {
|
||||
|
||||
root.getCards(function(err, data) {
|
||||
if (err) return cb(err);
|
||||
var card = lodash.find(data, {id : cardId});
|
||||
if (!card) return cb(_setError('Card not found'));
|
||||
var card = lodash.find(data, {
|
||||
id: cardId
|
||||
});
|
||||
|
||||
if (!card)
|
||||
return cb(_setError('Card not found'));
|
||||
|
||||
// Get invoices
|
||||
bitpayService.post('/api/v2/' + card.token, json, function(data) {
|
||||
$log.info('BitPay Get Invoices: SUCCESS');
|
||||
invoices = data.data.data || [];
|
||||
if (lodash.isEmpty(invoices)) $log.info('No invoices');
|
||||
|
||||
if (lodash.isEmpty(invoices))
|
||||
$log.info('No invoices');
|
||||
|
||||
json = {
|
||||
method: 'getTransactionHistory',
|
||||
params: JSON.stringify(params)
|
||||
params: JSON.stringify(opts)
|
||||
};
|
||||
// Get transactions list
|
||||
bitpayService.post('/api/v2/' + card.token, json, function(data) {
|
||||
$log.info('BitPay Get Transactions: SUCCESS');
|
||||
transactions = data.data.data || {};
|
||||
transactions['txs'] = _processTransactions(invoices, transactions.transactionList);
|
||||
|
||||
root.setLastKnownBalance(cardId, transactions.currentCardBalance, function() {});
|
||||
|
||||
return cb(data.data.error, transactions);
|
||||
}, function(data) {
|
||||
return cb(_setError('BitPay Card Error: Get Transactions', data));
|
||||
});
|
||||
}, function(data) {
|
||||
return cb(_setError('BitPay Card Error: Get Invoices', data));
|
||||
return cb(_setError('BitPay Card Error: Get Invoices', data));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
root.topUp = function(cardId, params, cb) {
|
||||
params = params || {};
|
||||
root.topUp = function(cardId, opts, cb) {
|
||||
opts = opts || {};
|
||||
var json = {
|
||||
method: 'generateTopUpInvoice',
|
||||
params: JSON.stringify(params)
|
||||
params: JSON.stringify(opts)
|
||||
};
|
||||
appIdentityService.getIdentity(bitpayService.getEnvironment().network, function(err, appIdentity) {
|
||||
if (err) return cb(err);
|
||||
root.getBitpayDebitCards(function(err, data) {
|
||||
|
||||
root.getCards(function(err, data) {
|
||||
if (err) return cb(err);
|
||||
var card = lodash.find(data, {id : cardId});
|
||||
if (!card) return cb(_setError('Card not found'));
|
||||
|
||||
var card = lodash.find(data, {
|
||||
id: cardId
|
||||
});
|
||||
|
||||
if (!card)
|
||||
return cb(_setError('Card not found'));
|
||||
|
||||
bitpayService.post('/api/v2/' + card.token, json, function(data) {
|
||||
$log.info('BitPay TopUp: SUCCESS');
|
||||
if(data.data.error) {
|
||||
if (data.data.error) {
|
||||
return cb(data.data.error);
|
||||
} else {
|
||||
return cb(null, data.data.data.invoice);
|
||||
|
@ -126,75 +170,50 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
});
|
||||
};
|
||||
|
||||
root.getBitpayDebitCards = function(cb) {
|
||||
storageService.getBitpayDebitCards(bitpayService.getEnvironment().network, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
return cb(null, data);
|
||||
});
|
||||
// get all cards, for all accounts.
|
||||
root.getCards = function(cb) {
|
||||
storageService.getBitpayDebitCards(bitpayService.getEnvironment().network, cb);
|
||||
};
|
||||
|
||||
root.setBitpayDebitCards = function(data, cb) {
|
||||
data = JSON.stringify(data);
|
||||
storageService.setBitpayDebitCards(bitpayService.getEnvironment().network, data, function(err) {
|
||||
if (err) return cb(err);
|
||||
root.getLastKnownBalance = function(cardId, cb) {
|
||||
storageService.getBalanceCache(cardId, cb);
|
||||
};
|
||||
|
||||
root.addLastKnownBalance = function(card, cb) {
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var showRange = 600; // 10min;
|
||||
|
||||
root.getLastKnownBalance(card.eid, function(err, data) {
|
||||
if (data) {
|
||||
data = JSON.parse(data);
|
||||
card.balance = data.balance;
|
||||
card.updatedOn = (data.updatedOn < now - showRange) ? data.updatedOn : null;
|
||||
}
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
root.getBitpayDebitCardsHistory = function(cardId, cb) {
|
||||
storageService.getBitpayDebitCardsHistory(bitpayService.getEnvironment().network, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
if (cardId) data = data[cardId];
|
||||
return cb(null, data);
|
||||
});
|
||||
root.setLastKnownBalance = function(cardId, balance, cb) {
|
||||
|
||||
storageService.setBalanceCache(cardId, {
|
||||
balance: balance,
|
||||
updatedOn: Math.floor(Date.now() / 1000),
|
||||
}, cb);
|
||||
};
|
||||
|
||||
root.setBitpayDebitCardsHistory = function(cardId, data, opts, cb) {
|
||||
storageService.getBitpayDebitCardsHistory(bitpayService.getEnvironment().network, function(err, oldData) {
|
||||
if (lodash.isString(oldData)) {
|
||||
oldData = JSON.parse(oldData);
|
||||
}
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
var inv = oldData || {};
|
||||
inv[cardId] = data;
|
||||
if (opts && opts.remove) {
|
||||
delete(inv[cardId]);
|
||||
}
|
||||
inv = JSON.stringify(inv);
|
||||
|
||||
storageService.setBitpayDebitCardsHistory(bitpayService.getEnvironment().network, inv, function(err) {
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
root.remove = function(card, cb) {
|
||||
storageService.removeBitpayDebitCard(bitpayService.getEnvironment().network, card, function(err) {
|
||||
root.remove = function(cardId, cb) {
|
||||
storageService.removeBitpayDebitCard(bitpayService.getEnvironment().network, cardId, function(err) {
|
||||
if (err) {
|
||||
$log.error('Error removing BitPay debit card: ' + err);
|
||||
// Continue, try to remove/cleanup card history
|
||||
return cb(err);
|
||||
}
|
||||
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();
|
||||
});
|
||||
register();
|
||||
storageService.removeBalanceCache(cardId, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
root.getRates = function(currency, cb) {
|
||||
bitpayService.get('/rates/' + currency, function(data) {
|
||||
$log.info('BitPay Get Rates: SUCCESS');
|
||||
|
@ -204,6 +223,39 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
root.get = function(opts, cb) {
|
||||
root.getCards(function(err, cards) {
|
||||
if (err) return;
|
||||
|
||||
if (lodash.isEmpty(cards)) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
if (opts.cardId) {
|
||||
cards = lodash.filter(cards, function(x) {
|
||||
return opts.cardId == x.eid;
|
||||
});
|
||||
}
|
||||
|
||||
// Async, no problem
|
||||
lodash.each(cards, function(x) {
|
||||
|
||||
root.addLastKnownBalance(x, function() {});
|
||||
|
||||
// async refresh
|
||||
if (!opts.noRefresh) {
|
||||
root.getHistory(x.id, {}, function(err, data) {
|
||||
if (err) return;
|
||||
root.addLastKnownBalance(x, function() {});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return cb(null, cards);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* CONSTANTS
|
||||
*/
|
||||
|
@ -1241,6 +1293,25 @@ angular.module('copayApp.services').factory('bitpayCardService', function($log,
|
|||
'bp002': 'default'
|
||||
};
|
||||
|
||||
var nextStepItem = {
|
||||
name: 'bitpaycard',
|
||||
title: 'Add Bitpay VISA Card',
|
||||
icon: 'icon-bitpay-card',
|
||||
sref: 'tabs.bitpayCardIntro',
|
||||
};
|
||||
|
||||
|
||||
var register = function() {
|
||||
root.getCards(function(err, cards) {
|
||||
if (lodash.isEmpty(cards)) {
|
||||
nextStepsService.register(nextStepItem);
|
||||
} else {
|
||||
nextStepsService.unregister(nextStepItem.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
register();
|
||||
return root;
|
||||
|
||||
});
|
||||
|
|
|
@ -136,13 +136,10 @@ angular.module('copayApp.services').factory('bitpayService', function($log, $htt
|
|||
};
|
||||
|
||||
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();
|
||||
});
|
||||
storageService.setBitpayAccount(root.getEnvironment().network, accountData, cb);
|
||||
};
|
||||
|
||||
|
||||
var _get = function(endpoint) {
|
||||
return {
|
||||
method: 'GET',
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').factory('buyAndSellService', function($log, nextStepsService, lodash, $ionicScrollDelegate, $timeout) {
|
||||
var root = {};
|
||||
var services = [];
|
||||
var linkedServices = [];
|
||||
|
||||
root.update = function() {
|
||||
|
||||
var newLinked = lodash.filter(services, function(x) {
|
||||
return x.linked;
|
||||
});
|
||||
|
||||
// This is to preserve linkedServices pointer
|
||||
while (linkedServices.length)
|
||||
linkedServices.pop();
|
||||
|
||||
while (newLinked.length)
|
||||
linkedServices.push(newLinked.pop());
|
||||
//
|
||||
|
||||
$log.debug('buyAndSell Service, updating nextSteps. linked/total: ' + linkedServices.length + '/' + services.length);
|
||||
|
||||
if (linkedServices.length == 0) {
|
||||
nextStepsService.register({
|
||||
title: 'Buy and Sell',
|
||||
name: 'buyandsell',
|
||||
icon: 'icon-buy-bitcoin',
|
||||
sref: 'tabs.buyandsell',
|
||||
});
|
||||
} else {
|
||||
nextStepsService.unregister({
|
||||
name: 'buyandsell',
|
||||
});
|
||||
};
|
||||
|
||||
$timeout(function() {
|
||||
$ionicScrollDelegate.resize();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
var updateNextStepsDebunced = lodash.debounce(root.update, 1000);
|
||||
|
||||
root.register = function(serviceInfo) {
|
||||
services.push(serviceInfo);
|
||||
$log.info('Adding Buy and Sell service:' + serviceInfo.name + ' linked:' + serviceInfo.linked);
|
||||
updateNextStepsDebunced();
|
||||
};
|
||||
|
||||
|
||||
root.updateLink = function(name, linked) {
|
||||
var service = lodash.find(services, function(x) {
|
||||
return x.name == name;
|
||||
});
|
||||
$log.info('Updating Buy and Sell service:' + name + ' linked:' + linked);
|
||||
service.linked = linked
|
||||
|
||||
root.update();
|
||||
};
|
||||
|
||||
|
||||
root.get = function() {
|
||||
return services;
|
||||
};
|
||||
|
||||
|
||||
root.getLinked = function() {
|
||||
return linkedServices;
|
||||
};
|
||||
|
||||
|
||||
return root;
|
||||
});
|
|
@ -44,7 +44,7 @@ angular.module('copayApp.services')
|
|||
body = gettextCatalog.getString('Insufficient funds');
|
||||
break;
|
||||
case 'CONNECTION_ERROR':
|
||||
body = gettextCatalog.getString('Network connection error');
|
||||
body = gettextCatalog.getString('Network error');
|
||||
break;
|
||||
case 'NOT_FOUND':
|
||||
body = gettextCatalog.getString('Wallet service not found');
|
||||
|
|
|
@ -1,37 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').factory('coinbaseService', function($http, $log, $window, $filter, platformInfo, lodash, storageService, configService, appConfigService, txFormatService) {
|
||||
angular.module('copayApp.services').factory('coinbaseService', function($http, $log, $window, $filter, platformInfo, lodash, storageService, configService, appConfigService, txFormatService, buyAndSellService, $rootScope) {
|
||||
var root = {};
|
||||
var credentials = {};
|
||||
var isCordova = platformInfo.isCordova;
|
||||
var isNW = platformInfo.isNW;
|
||||
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova;
|
||||
|
||||
root.priceSensitivity = [
|
||||
{
|
||||
value: 0.5,
|
||||
name: '0.5%'
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
name: '1%'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
name: '2%'
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
name: '5%'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
name: '10%'
|
||||
}
|
||||
];
|
||||
root.priceSensitivity = [{
|
||||
value: 0.5,
|
||||
name: '0.5%'
|
||||
}, {
|
||||
value: 1,
|
||||
name: '1%'
|
||||
}, {
|
||||
value: 2,
|
||||
name: '2%'
|
||||
}, {
|
||||
value: 5,
|
||||
name: '5%'
|
||||
}, {
|
||||
value: 10,
|
||||
name: '10%'
|
||||
}];
|
||||
|
||||
root.selectedPriceSensitivity = root.priceSensitivity[1];
|
||||
|
||||
root.setCredentials = function() {
|
||||
var setCredentials = function() {
|
||||
|
||||
if (!$window.externalServices || !$window.externalServices.coinbase) {
|
||||
return;
|
||||
|
@ -46,19 +41,19 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
credentials.NETWORK = 'livenet';
|
||||
|
||||
// Coinbase permissions
|
||||
credentials.SCOPE = ''
|
||||
+ 'wallet:accounts:read,'
|
||||
+ 'wallet:addresses:read,'
|
||||
+ 'wallet:addresses:create,'
|
||||
+ 'wallet:user:read,'
|
||||
+ 'wallet:user:email,'
|
||||
+ 'wallet:buys:read,'
|
||||
+ 'wallet:buys:create,'
|
||||
+ 'wallet:sells:read,'
|
||||
+ 'wallet:sells:create,'
|
||||
+ 'wallet:transactions:read,'
|
||||
+ 'wallet:transactions:send,'
|
||||
+ 'wallet:payment-methods:read';
|
||||
credentials.SCOPE = '' +
|
||||
'wallet:accounts:read,' +
|
||||
'wallet:addresses:read,' +
|
||||
'wallet:addresses:create,' +
|
||||
'wallet:user:read,' +
|
||||
'wallet:user:email,' +
|
||||
'wallet:buys:read,' +
|
||||
'wallet:buys:create,' +
|
||||
'wallet:sells:read,' +
|
||||
'wallet:sells:create,' +
|
||||
'wallet:transactions:read,' +
|
||||
'wallet:transactions:send,' +
|
||||
'wallet:payment-methods:read';
|
||||
|
||||
// NW has a bug with Window Object
|
||||
if (isCordova) {
|
||||
|
@ -72,8 +67,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
credentials.API = coinbase.sandbox.api;
|
||||
credentials.CLIENT_ID = coinbase.sandbox.client_id;
|
||||
credentials.CLIENT_SECRET = coinbase.sandbox.client_secret;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
credentials.HOST = coinbase.production.host;
|
||||
credentials.API = coinbase.production.api;
|
||||
credentials.CLIENT_ID = coinbase.production.client_id;
|
||||
|
@ -85,6 +79,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
if (data && data.access_token && data.refresh_token) {
|
||||
storageService.setCoinbaseToken(credentials.NETWORK, data.access_token, function() {
|
||||
storageService.setCoinbaseRefreshToken(credentials.NETWORK, data.refresh_token, function() {
|
||||
buyAndSellService.updateLink('coinbase', true);
|
||||
return cb(null, data.access_token);
|
||||
});
|
||||
});
|
||||
|
@ -107,8 +102,8 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
root.getAvailableCurrency = function() {
|
||||
var config = configService.getSync().wallet.settings;
|
||||
// ONLY "USD"
|
||||
switch(config.alternativeIsoCode) {
|
||||
default : return 'USD'
|
||||
switch (config.alternativeIsoCode) {
|
||||
default: return 'USD'
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -141,14 +136,14 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
}
|
||||
|
||||
root.getOauthCodeUrl = function() {
|
||||
return credentials.HOST
|
||||
+ '/oauth/authorize?response_type=code&client_id='
|
||||
+ credentials.CLIENT_ID
|
||||
+ '&redirect_uri='
|
||||
+ credentials.REDIRECT_URI
|
||||
+ '&state=SECURE_RANDOM&scope='
|
||||
+ credentials.SCOPE
|
||||
+ '&meta[send_limit_amount]=1000&meta[send_limit_currency]=USD&meta[send_limit_period]=day';
|
||||
return credentials.HOST +
|
||||
'/oauth/authorize?response_type=code&client_id=' +
|
||||
credentials.CLIENT_ID +
|
||||
'&redirect_uri=' +
|
||||
credentials.REDIRECT_URI +
|
||||
'&state=SECURE_RANDOM&scope=' +
|
||||
credentials.SCOPE +
|
||||
'&meta[send_limit_amount]=1000&meta[send_limit_currency]=USD&meta[send_limit_period]=day';
|
||||
};
|
||||
|
||||
root.getToken = function(code, cb) {
|
||||
|
@ -160,9 +155,9 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
'Accept': 'application/json'
|
||||
},
|
||||
data: {
|
||||
grant_type : 'authorization_code',
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id : credentials.CLIENT_ID,
|
||||
client_id: credentials.CLIENT_ID,
|
||||
client_secret: credentials.CLIENT_SECRET,
|
||||
redirect_uri: credentials.REDIRECT_URI
|
||||
}
|
||||
|
@ -171,7 +166,6 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
$http(req).then(function(data) {
|
||||
$log.info('Coinbase Authorization Access Token: SUCCESS');
|
||||
// Show pending task from the UI
|
||||
storageService.setNextStep('BuyAndSell', 'true', function(err) {});
|
||||
_afterTokenReceived(data.data, cb);
|
||||
}, function(data) {
|
||||
$log.error('Coinbase Authorization Access Token: ERROR ' + data.statusText);
|
||||
|
@ -188,8 +182,8 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
'Accept': 'application/json'
|
||||
},
|
||||
data: {
|
||||
grant_type : 'refresh_token',
|
||||
client_id : credentials.CLIENT_ID,
|
||||
grant_type: 'refresh_token',
|
||||
client_id: credentials.CLIENT_ID,
|
||||
client_secret: credentials.CLIENT_SECRET,
|
||||
redirect_uri: credentials.REDIRECT_URI,
|
||||
refresh_token: refreshToken
|
||||
|
@ -219,6 +213,19 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
});
|
||||
};
|
||||
|
||||
root.isActive = function(cb) {
|
||||
|
||||
if (isWindowsPhoneApp)
|
||||
return cb();
|
||||
|
||||
if (lodash.isEmpty(credentials.CLIENT_ID))
|
||||
return cb();
|
||||
|
||||
storageService.getCoinbaseToken(credentials.NETWORK, function(err, accessToken) {
|
||||
return cb(err, !!accessToken);
|
||||
});
|
||||
}
|
||||
|
||||
root.init = lodash.throttle(function(cb) {
|
||||
if (lodash.isEmpty(credentials.CLIENT_ID)) {
|
||||
return cb('Coinbase is Disabled');
|
||||
|
@ -238,7 +245,10 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
if (err) return cb(err);
|
||||
_getMainAccountId(newToken, function(err, accountId) {
|
||||
if (err) return cb(err);
|
||||
return cb(null, {accessToken: newToken, accountId: accountId});
|
||||
return cb(null, {
|
||||
accessToken: newToken,
|
||||
accountId: accountId
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -246,7 +256,10 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
return cb(err);
|
||||
}
|
||||
} else {
|
||||
return cb(null, {accessToken: accessToken, accountId: accountId});
|
||||
return cb(null, {
|
||||
accessToken: accessToken,
|
||||
accountId: accountId
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -410,7 +423,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
var data = {
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
payment_method: data.payment_method || null,
|
||||
payment_method: data.payment_method || null,
|
||||
commit: data.commit || false,
|
||||
quote: data.quote || false
|
||||
};
|
||||
|
@ -598,9 +611,10 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
};
|
||||
|
||||
root.updatePendingTransactions = lodash.throttle(function() {
|
||||
$log.debug('Updating pending transactions...');
|
||||
root.setCredentials();
|
||||
var pendingTransactions = { data: {} };
|
||||
$log.debug('Updating coinbase pending transactions...');
|
||||
var pendingTransactions = {
|
||||
data: {}
|
||||
};
|
||||
root.getPendingTransactions(pendingTransactions);
|
||||
}, 20000);
|
||||
|
||||
|
@ -707,6 +721,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
|
||||
root.logout = function(cb) {
|
||||
storageService.removeCoinbaseToken(credentials.NETWORK, function() {
|
||||
buyAndSellService.updateLink('coinbase', false);
|
||||
storageService.removeCoinbaseRefreshToken(credentials.NETWORK, function() {
|
||||
storageService.removeCoinbaseTxs(credentials.NETWORK, function() {
|
||||
return cb();
|
||||
|
@ -715,6 +730,33 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $
|
|||
});
|
||||
};
|
||||
|
||||
return root;
|
||||
var register = function() {
|
||||
|
||||
root.isActive(function(err, isActive){
|
||||
if (err) return;
|
||||
|
||||
buyAndSellService.register({
|
||||
name: 'coinbase',
|
||||
logo: 'img/coinbase-logo.png',
|
||||
location: '33 Countries',
|
||||
sref: 'tabs.buyandsell.coinbase',
|
||||
configSref: 'tabs.preferences.coinbase',
|
||||
linked: isActive,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setCredentials();
|
||||
register();
|
||||
|
||||
$rootScope.$on('bwsEvent', function(e, walletId, type, n) {
|
||||
if (type == 'NewBlock' && n && n.data && n.data.network == 'livenet') {
|
||||
root.isActive(function(err,isActive){
|
||||
// Update Coinbase
|
||||
if (isActive)
|
||||
root.updatePendingTransactions();
|
||||
});
|
||||
}
|
||||
});
|
||||
return root;
|
||||
});
|
||||
|
|
|
@ -43,26 +43,14 @@ angular.module('copayApp.services').factory('configService', function(storageSer
|
|||
},
|
||||
|
||||
// External services
|
||||
glidera: {
|
||||
enabled: true,
|
||||
testnet: false
|
||||
},
|
||||
|
||||
coinbaseV2: true,
|
||||
|
||||
bitpayCard: {
|
||||
enabled: true
|
||||
},
|
||||
|
||||
amazon: {
|
||||
enabled: true
|
||||
},
|
||||
|
||||
|
||||
recentTransactions: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
hideNextSteps: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
rates: {
|
||||
url: 'https://insight.bitpay.com:443/api/rates',
|
||||
},
|
||||
|
@ -191,18 +179,11 @@ angular.module('copayApp.services').factory('configService', function(storageSer
|
|||
if (!configCache.wallet.settings.unitCode) {
|
||||
configCache.wallet.settings.unitCode = defaultConfig.wallet.settings.unitCode;
|
||||
}
|
||||
if (!configCache.glidera) {
|
||||
configCache.glidera = defaultConfig.glidera;
|
||||
}
|
||||
if (!configCache.coinbaseV2) {
|
||||
configCache.coinbaseV2 = defaultConfig.coinbaseV2;
|
||||
}
|
||||
if (!configCache.amazon) {
|
||||
configCache.amazon = defaultConfig.amazon;
|
||||
}
|
||||
if (!configCache.bitpayCard) {
|
||||
configCache.bitpayCard = defaultConfig.bitpayCard;
|
||||
|
||||
if (!configCache.hideNextSteps) {
|
||||
configCache.hideNextSteps = defaultConfig.hideNextSteps;
|
||||
}
|
||||
|
||||
if (!configCache.recentTransactions) {
|
||||
configCache.recentTransactions = defaultConfig.recentTransactions;
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ angular.module('copayApp.services')
|
|||
if (lodash.isObject(v))
|
||||
v = JSON.stringify(v);
|
||||
|
||||
if (!lodash.isString(v)) {
|
||||
if (v && !lodash.isString(v)) {
|
||||
v = v.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').factory('glideraService', function($http, $log, $window, platformInfo, storageService) {
|
||||
angular.module('copayApp.services').factory('glideraService', function($http, $log, $window, platformInfo, storageService, buyAndSellService) {
|
||||
var root = {};
|
||||
var credentials = {};
|
||||
var isCordova = platformInfo.isCordova;
|
||||
var isWindowsPhoneApp = platformInfo.isWP && platformInfo.isCordova;
|
||||
|
||||
var _setCredentials = function() {
|
||||
var setCredentials = function() {
|
||||
if (!$window.externalServices || !$window.externalServices.glidera) {
|
||||
return;
|
||||
}
|
||||
|
@ -17,6 +18,7 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
* Production: 'livenet'
|
||||
*/
|
||||
credentials.NETWORK = 'livenet';
|
||||
//credentials.NETWORK = 'testnet';
|
||||
|
||||
if (credentials.NETWORK == 'testnet') {
|
||||
credentials.HOST = glidera.sandbox.host;
|
||||
|
@ -44,7 +46,6 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
};
|
||||
|
||||
root.getEnvironment = function() {
|
||||
_setCredentials();
|
||||
return credentials.NETWORK;
|
||||
};
|
||||
|
||||
|
@ -57,19 +58,17 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
}
|
||||
|
||||
root.getOauthCodeUrl = function() {
|
||||
_setCredentials();
|
||||
return credentials.HOST + '/oauth2/auth?response_type=code&client_id=' + credentials.CLIENT_ID + '&redirect_uri=' + credentials.REDIRECT_URI;
|
||||
};
|
||||
|
||||
root.removeToken = function(cb) {
|
||||
_setCredentials();
|
||||
storageService.removeGlideraToken(credentials.NETWORK, function() {
|
||||
buyAndSellService.updateLink('glidera', false);
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
root.getToken = function(code, cb) {
|
||||
_setCredentials();
|
||||
var req = {
|
||||
method: 'POST',
|
||||
url: credentials.HOST + '/api/v1/oauth/token',
|
||||
|
@ -98,7 +97,6 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
};
|
||||
|
||||
var _get = function(endpoint, token) {
|
||||
_setCredentials();
|
||||
return {
|
||||
method: 'GET',
|
||||
url: credentials.HOST + '/api/v1' + endpoint,
|
||||
|
@ -216,7 +214,6 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
};
|
||||
|
||||
var _post = function(endpoint, token, twoFaCode, data) {
|
||||
_setCredentials();
|
||||
return {
|
||||
method: 'POST',
|
||||
url: credentials.HOST + '/api/v1' + endpoint,
|
||||
|
@ -293,7 +290,6 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
};
|
||||
|
||||
root.init = function(accessToken, cb) {
|
||||
_setCredentials();
|
||||
$log.debug('Init Glidera...');
|
||||
|
||||
var glidera = {
|
||||
|
@ -312,6 +308,8 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
getToken(function(err, accessToken) {
|
||||
if (err || !accessToken) return cb();
|
||||
else {
|
||||
buyAndSellService.updateLink('glidera', true);
|
||||
|
||||
root.getAccessTokenPermissions(accessToken, function(err, p) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
|
@ -325,6 +323,25 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l
|
|||
});
|
||||
};
|
||||
|
||||
return root;
|
||||
|
||||
var register = function() {
|
||||
if (isWindowsPhoneApp) return;
|
||||
|
||||
storageService.getGlideraToken(credentials.NETWORK, function(err, token) {
|
||||
if (err) return cb(err);
|
||||
|
||||
buyAndSellService.register({
|
||||
name: 'glidera',
|
||||
logo: 'img/glidera-logo.png',
|
||||
location: 'US Only',
|
||||
sref: 'tabs.buyandsell.glidera',
|
||||
configSref: 'tabs.preferences.glidera',
|
||||
linked: !!token,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setCredentials();
|
||||
register();
|
||||
return root;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services').factory('', function(configService, $log) {
|
||||
var root = {};
|
||||
var services = [];
|
||||
|
||||
root.register = function(serviceInfo) {
|
||||
$log.info('Adding homeIntegration entry:' + serviceInfo.name);
|
||||
services.push(serviceInfo);
|
||||
};
|
||||
|
||||
root.unregister = function(serviceName) {
|
||||
services = lodash.filter(services, function(x) {
|
||||
return x.name != serviceName
|
||||
});
|
||||
};
|
||||
|
||||
root.get = function() {
|
||||
return services;
|
||||
};
|
||||
|
||||
return root;
|
||||
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services').factory('homeIntegrationsService', function(configService, $log) {
|
||||
var root = {};
|
||||
var services = [];
|
||||
|
||||
root.register = function(serviceInfo) {
|
||||
$log.info('Adding home Integrations entry:' + serviceInfo.name);
|
||||
services.push(serviceInfo);
|
||||
};
|
||||
|
||||
root.unregister = function(serviceName) {
|
||||
services = lodash.filter(services, function(x) {
|
||||
return x.name != serviceName
|
||||
});
|
||||
};
|
||||
|
||||
root.get = function() {
|
||||
return services;
|
||||
};
|
||||
|
||||
return root;
|
||||
|
||||
});
|
|
@ -43,16 +43,17 @@ angular.module('copayApp.services')
|
|||
};
|
||||
|
||||
root.set = function(k, v, cb) {
|
||||
|
||||
if (lodash.isObject(v)) {
|
||||
v = JSON.stringify(v);
|
||||
}
|
||||
if (v && !lodash.isString(v)) {
|
||||
v = v.toString();
|
||||
}
|
||||
|
||||
if (isChromeApp || isNW) {
|
||||
var obj = {};
|
||||
|
||||
if (lodash.isObject(v)) {
|
||||
v = JSON.stringify(v);
|
||||
}
|
||||
if (!lodash.isString(v)) {
|
||||
v = v.toString();
|
||||
}
|
||||
|
||||
obj[k] = v;
|
||||
|
||||
chrome.storage.local.set(obj, cb);
|
||||
|
@ -60,7 +61,6 @@ angular.module('copayApp.services')
|
|||
ls.setItem(k, v);
|
||||
return cb();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
root.remove = function(k, cb) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services').factory('nextStepsService', function(configService, $log, lodash) {
|
||||
var root = {};
|
||||
var services = [];
|
||||
|
||||
root.register = function(serviceInfo) {
|
||||
$log.info('Adding NextSteps entry:' + serviceInfo.name);
|
||||
|
||||
if (!lodash.find(services, function(x) {
|
||||
return x.name == serviceInfo.name;
|
||||
})) {
|
||||
services.push(serviceInfo);
|
||||
}
|
||||
};
|
||||
|
||||
root.unregister = function(serviceName) {
|
||||
|
||||
var newS = lodash.filter(services, function(x) {
|
||||
return x.name != serviceName;
|
||||
});
|
||||
|
||||
// Found?
|
||||
if (newS.length == services.length) return;
|
||||
|
||||
$log.info('Removing NextSteps entry:' + serviceName);
|
||||
// This is to preserve services pointer
|
||||
while (services.length)
|
||||
services.pop();
|
||||
|
||||
while (newS.length)
|
||||
services.push(newS.pop());
|
||||
};
|
||||
|
||||
root.get = function() {
|
||||
return services;
|
||||
};
|
||||
|
||||
return root;
|
||||
|
||||
});
|
|
@ -84,6 +84,7 @@ angular.module('copayApp.services').factory('openURLService', function($rootScop
|
|||
$log.debug('Registering Browser handlers base:' + base);
|
||||
navigator.registerProtocolHandler('bitcoin', url, 'Copay Bitcoin Handler');
|
||||
navigator.registerProtocolHandler('web+copay', url, 'Copay Wallet Handler');
|
||||
navigator.registerProtocolHandler('web+bitpay', url, 'Bitpay Wallet Handler');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -747,6 +747,31 @@ angular.module('copayApp.services')
|
|||
storageService.storeProfile(root.profile, cb);
|
||||
};
|
||||
|
||||
root.getLastKnownBalance = function(wid, cb) {
|
||||
storageService.getBalanceCache(wid, cb);
|
||||
};
|
||||
|
||||
root.addLastKnownBalance = function(wallet, cb) {
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var showRange = 600; // 10min;
|
||||
|
||||
root.getLastKnownBalance(wallet.id, function(err, data) {
|
||||
if (data) {
|
||||
data = JSON.parse(data);
|
||||
wallet.cachedBalance = data.balance;
|
||||
wallet.cachedBalanceUpdatedOn = (data.updatedOn < now - showRange) ? data.updatedOn : null;
|
||||
}
|
||||
return cb();
|
||||
});
|
||||
};
|
||||
|
||||
root.setLastKnownBalance = function(wid, balance, cb) {
|
||||
storageService.setBalanceCache(wid, {
|
||||
balance: balance,
|
||||
updatedOn: Math.floor(Date.now() / 1000),
|
||||
}, cb);
|
||||
};
|
||||
|
||||
root.getWallets = function(opts) {
|
||||
|
||||
if (opts && !lodash.isObject(opts))
|
||||
|
@ -780,6 +805,12 @@ angular.module('copayApp.services')
|
|||
});
|
||||
} else {}
|
||||
|
||||
// Add cached balance async
|
||||
lodash.each(ret, function(x) {
|
||||
root.addLastKnownBalance(x, function() {});
|
||||
});
|
||||
|
||||
|
||||
return lodash.sortBy(ret, [
|
||||
|
||||
function(x) {
|
||||
|
@ -796,7 +827,7 @@ angular.module('copayApp.services')
|
|||
root.getNotifications = function(opts, cb) {
|
||||
opts = opts || {};
|
||||
|
||||
var TIME_STAMP = 60 * 60 * 6;
|
||||
var TIME_STAMP = 60 * 60 * 6;
|
||||
var MAX = 30;
|
||||
|
||||
var typeFilter = {
|
||||
|
|
|
@ -24,6 +24,9 @@ angular.module('copayApp.services')
|
|||
}, cb);
|
||||
};
|
||||
|
||||
// This is only used in Copay, we used to encrypt profile
|
||||
// using device's UUID.
|
||||
|
||||
var decryptOnMobile = function(text, cb) {
|
||||
var json;
|
||||
try {
|
||||
|
@ -74,312 +77,8 @@ angular.module('copayApp.services')
|
|||
});
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// UPGRADING STORAGE
|
||||
//
|
||||
// Upgraders are executed in numerical order per the '##_' object key prefix. Each upgrader will run.
|
||||
// Each upgrader should detect storage configuraton and fail-safe; no upgrader should damage the ability
|
||||
// of another to function properly (in order). Each upgrader should upgrade storage incrementally; storage
|
||||
// upgrade is not complete until all upgraders have run.
|
||||
//
|
||||
// 1. Write a function to upgrade the desired storage key(s). The function should have the protocol:
|
||||
//
|
||||
// _upgrade_x(key, network, cb), where:
|
||||
//
|
||||
// `x` is the name of the storage key
|
||||
// `key` is the name of the storage key being upgraded
|
||||
// `network` is one of 'livenet', 'testnet'
|
||||
//
|
||||
// 2. Add the storage key to `_upgraders` object using the name of the key as the `_upgrader` object key
|
||||
// with the value being the name of the upgrade function (e.g., _upgrade_x). In order to avoid conflicts
|
||||
// when a storage key is involved in multiple upgraders as well as predicte the order in which upgrades
|
||||
// occur the `_upgrader` object key should be prefixed with '##_' (e.g., '01_') to create a unique and
|
||||
// sortable name. This format is interpreted by the _upgrade() function.
|
||||
//
|
||||
// 3. Any dependency functions called by upgraders should be copied/factored out and remain unchanged as
|
||||
// long as the upgrader remains in effect. By convention the dependency function is prefixed by '##_' to
|
||||
// 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 bitpayAccounts-x to bitpayAccounts-v2-x
|
||||
'03_bitpayAccounts-v2' : _validate_bitpayAccounts_v2 // 2017-01: Validate keys on bitpayAccounts-v2-x, remove if not valid
|
||||
};
|
||||
|
||||
function _upgrade_bitpayDebitCards(key, network, cb) {
|
||||
key += '-' + network;
|
||||
storage.get(key, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (data != null) {
|
||||
// Needs upgrade
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
_00_setBitpayDebitCards(network, data, function(err) {
|
||||
if (err) return cb(err);
|
||||
storage.remove(key, function() {
|
||||
cb(null, 'replaced with \'bitpayAccounts\'');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function _upgrade_bitpayCardCredentials(key, network, cb) {
|
||||
key += '-' + network;
|
||||
storage.get(key, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (data != null) {
|
||||
// Needs upgrade
|
||||
_01_setAppIdentity(network, data, function(err) {
|
||||
if (err) return cb(err);
|
||||
storage.remove(key, function() {
|
||||
cb(null, 'replaced with \'appIdentity\'');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function _upgrade_bitpayAccounts(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 = '';
|
||||
_asyncEach(Object.keys(data), function(key, callback) {
|
||||
// Keys are account emails
|
||||
if (!data[key]['bitpayApi-' + network]) {
|
||||
// Needs upgrade
|
||||
upgraded += ' ' + key;
|
||||
var acctData = {
|
||||
acct: data[key],
|
||||
token: data[key]['bitpayDebitCards-' + network].token,
|
||||
email: key
|
||||
};
|
||||
_02_setBitpayAccount(network, acctData, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
_02_setBitpayDebitCards(network, data[key]['bitpayDebitCards-' + network], function(err) {
|
||||
if (err) return cb(err);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
callback();
|
||||
}, function() {
|
||||
// done
|
||||
// Remove obsolete key.
|
||||
storage.remove('bitpayAccounts-' + network, function() {
|
||||
if (upgraded.length > 0) {
|
||||
cb(null, 'upgraded to \'bitpayAccounts-v2-' + network + '\':' + upgraded);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function _validate_bitpayAccounts_v2(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 verified = '';
|
||||
var toRemove = [];
|
||||
_asyncEach(Object.keys(data), function(key, callback) {
|
||||
// Verify account API data
|
||||
if (!data[key]['bitpayApi-' + network] ||
|
||||
!data[key]['bitpayApi-' + network].token) {
|
||||
// Invalid entry - one or more keys are missing
|
||||
toRemove.push(key);
|
||||
return callback();
|
||||
}
|
||||
// Verify debit cards
|
||||
if (Array.isArray(data[key]['bitpayDebitCards-' + network])) {
|
||||
for (var i = 0; i < data[key]['bitpayDebitCards-' + network].length; i++) {
|
||||
if (!data[key]['bitpayDebitCards-' + network][i].token ||
|
||||
!data[key]['bitpayDebitCards-' + network][i].eid ||
|
||||
!data[key]['bitpayDebitCards-' + network][i].id ||
|
||||
!data[key]['bitpayDebitCards-' + network][i].lastFourDigits) {
|
||||
// Invalid entry - one or more keys are missing
|
||||
toRemove.push(key);
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
verified += ' ' + key;
|
||||
return callback();
|
||||
}, function() {
|
||||
// done, remove invalid account entrys
|
||||
if (toRemove.length > 0) {
|
||||
var removed = '';
|
||||
for (var i = 0; i < toRemove.length; i++) {
|
||||
removed += ' ' + toRemove[i];
|
||||
delete data[toRemove[i]];
|
||||
}
|
||||
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(data), function(err) {
|
||||
if (err) return cb(err);
|
||||
// Ensure next step for cards is visible
|
||||
storage.get('bitpayAccounts-v2-' + network, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isEmpty(data)) {
|
||||
root.removeNextStep('BitpayCard', function(err) {});
|
||||
}
|
||||
});
|
||||
cb(null, 'removed invalid account records, please re-pair cards for these accounts:' + removed + '; ' +
|
||||
'the following accounts validated OK: ' + (verified.length > 0 ? verified : 'none'));
|
||||
});
|
||||
} else {
|
||||
cb(null, (verified.length > 0 ? 'accounts OK: ' + verified : 'no accounts found'));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// UPGRADER DEPENDENCIES
|
||||
// These functions remain as long as the upgrader remains in effect.
|
||||
//
|
||||
var _00_setBitpayDebitCards = function(network, data, cb) {
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
if (lodash.isEmpty(data) || !data.email) return cb('No card(s) to set');
|
||||
storage.get('bitpayAccounts-' + 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]['bitpayDebitCards-' + network] = data;
|
||||
storage.set('bitpayAccounts-' + network, JSON.stringify(bitpayAccounts), cb);
|
||||
});
|
||||
};
|
||||
|
||||
var _01_setAppIdentity = function(network, data, cb) {
|
||||
storage.set('appIdentity-' + network, data, cb);
|
||||
};
|
||||
|
||||
var _02_setBitpayAccount = function(network, data, cb) {
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
if (lodash.isEmpty(data) || !data.email || !data.acct) 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] = data.acct;
|
||||
bitpayAccounts[data.email]['bitpayApi-' + network] = {};
|
||||
bitpayAccounts[data.email]['bitpayApi-' + network].token = data.token;
|
||||
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb);
|
||||
});
|
||||
};
|
||||
|
||||
var _02_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-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]['bitpayDebitCards-' + network] = data.cards;
|
||||
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb);
|
||||
});
|
||||
};
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// IMPORTANT: This function is designed to block execution until it completes.
|
||||
// Ideally storage should not be used until it has been verified.
|
||||
root.verify = function(cb) {
|
||||
_upgrade(function(err) {
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
function _handleUpgradeError(key, err) {
|
||||
$log.error('Failed to upgrade storage for \'' + key + '\': ' + err);
|
||||
};
|
||||
|
||||
function _handleUpgradeSuccess(key, msg) {
|
||||
$log.info('Storage upgraded for \'' + key + '\': ' + msg);
|
||||
};
|
||||
|
||||
// IMPORTANT: This function is designed to block execution until it completes.
|
||||
// Ideally storage should not be used until it has been verified.
|
||||
function _upgrade(cb) {
|
||||
var errorCount = 0;
|
||||
var errorMessage = undefined;
|
||||
var keys = Object.keys(_upgraders).sort();
|
||||
var networks = ['livenet', 'testnet'];
|
||||
_asyncEach(keys, function(key, callback_keys) {
|
||||
_asyncEach(networks, function(network, callback_networks) {
|
||||
var storagekey = key.split('_')[1];
|
||||
_upgraders[key](storagekey, network, function(err, msg) {
|
||||
if (err) {
|
||||
_handleUpgradeError(storagekey + '-' + network, err);
|
||||
errorCount++;
|
||||
errorMessage = errorCount + ' storage upgrade failures';
|
||||
}
|
||||
if (msg) _handleUpgradeSuccess(storagekey + '-' + network, msg);
|
||||
callback_networks();
|
||||
});
|
||||
}, function() {
|
||||
// done - networks
|
||||
callback_keys();
|
||||
});
|
||||
}, function() {
|
||||
//done - keys
|
||||
cb(errorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
function _asyncEach(iterableList, callback, done) {
|
||||
var i = -1;
|
||||
var length = iterableList.length;
|
||||
|
||||
function loop() {
|
||||
i++;
|
||||
if (i === length) {
|
||||
done();
|
||||
return;
|
||||
} else if (i < length) {
|
||||
callback(iterableList[i], loop);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
loop();
|
||||
};
|
||||
|
||||
// This is only use in Copay, for very old instalations
|
||||
// in which we use to use localStorage instead of fileStorage
|
||||
root.tryToMigrate = function(cb) {
|
||||
if (!shouldUseFileStorage) return cb();
|
||||
|
||||
|
@ -645,51 +344,35 @@ angular.module('copayApp.services')
|
|||
storage.remove('coinbaseTxs-' + network, cb);
|
||||
};
|
||||
|
||||
root.setBitpayDebitCardsHistory = function(network, data, cb) {
|
||||
storage.set('bitpayDebitCardsHistory-' + network, data, cb);
|
||||
root.setBalanceCache = function(cardId, data, cb) {
|
||||
storage.set('balanceCache-' + cardId, data, cb);
|
||||
};
|
||||
|
||||
root.getBitpayDebitCardsHistory = function(network, cb) {
|
||||
storage.get('bitpayDebitCardsHistory-' + network, cb);
|
||||
root.getBalanceCache = function(cardId, cb) {
|
||||
storage.get('balanceCache-' + cardId, cb);
|
||||
};
|
||||
|
||||
root.removeBitpayDebitCardHistory = function(network, card, cb) {
|
||||
root.getBitpayDebitCardsHistory(network, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
data = data || {};
|
||||
delete data[card.eid];
|
||||
root.setBitpayDebitCardsHistory(network, JSON.stringify(data), cb);
|
||||
});
|
||||
root.removeBalanceCache = function(cardId, cb) {
|
||||
storage.remove('balanceCache-' + cardId, cb);
|
||||
};
|
||||
|
||||
// data: {
|
||||
// cards: [
|
||||
// eid: card id
|
||||
// id: card id
|
||||
// lastFourDigits: card number
|
||||
// token: card token
|
||||
// ]
|
||||
// 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-v2-' + network, function(err, bitpayAccounts) {
|
||||
root.setBitpayDebitCards = function(network, email, cards, cb) {
|
||||
|
||||
root.getBitpayAccounts(network, function(err, allAccounts) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(bitpayAccounts)) {
|
||||
bitpayAccounts = JSON.parse(bitpayAccounts);
|
||||
|
||||
if (!allAccounts[email]) {
|
||||
return cb('Cannot set cards for unknown account ' + email);
|
||||
}
|
||||
bitpayAccounts = bitpayAccounts || {};
|
||||
bitpayAccounts[data.email] = bitpayAccounts[data.email] || {};
|
||||
bitpayAccounts[data.email]['bitpayDebitCards-' + network] = data.cards;
|
||||
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb);
|
||||
|
||||
allAccounts[email].cards = cards;
|
||||
storage.set('bitpayAccounts-v2-' + network, allAccounts, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -702,24 +385,24 @@ angular.module('copayApp.services')
|
|||
// email: account email
|
||||
// ]
|
||||
root.getBitpayDebitCards = function(network, cb) {
|
||||
storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) {
|
||||
if (lodash.isString(bitpayAccounts)) {
|
||||
bitpayAccounts = JSON.parse(bitpayAccounts);
|
||||
}
|
||||
bitpayAccounts = bitpayAccounts || {};
|
||||
var cards = [];
|
||||
_asyncEach(Object.keys(bitpayAccounts), function(email, callback) {
|
||||
// For the UI, add the account email to the card object.
|
||||
var acctCards = bitpayAccounts[email]['bitpayDebitCards-' + network] || [];
|
||||
for (var i = 0; i < acctCards.length; i++) {
|
||||
acctCards[i].email = email;
|
||||
}
|
||||
cards = cards.concat(acctCards);
|
||||
callback();
|
||||
}, function() {
|
||||
// done
|
||||
cb(err, cards);
|
||||
|
||||
root.getBitpayAccounts(network, function(err, allAccounts) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var allCards = [];
|
||||
|
||||
lodash.each(allAccounts, function(account, email) {
|
||||
|
||||
// Add account's email to card list, for convenience
|
||||
var cards = lodash.clone(account.cards);
|
||||
lodash.each(cards, function(x) {
|
||||
x.email = email;
|
||||
});
|
||||
|
||||
allCards = allCards.concat(cards);
|
||||
});
|
||||
|
||||
return cb(null, allCards);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -729,95 +412,101 @@ angular.module('copayApp.services')
|
|||
// lastFourDigits: card number
|
||||
// token: card token
|
||||
// }
|
||||
root.removeBitpayDebitCard = function(network, card, cb) {
|
||||
if (lodash.isString(card)) {
|
||||
card = JSON.parse(card);
|
||||
}
|
||||
card = card || {};
|
||||
if (lodash.isEmpty(card) || !card.eid) return cb('No card to remove');
|
||||
storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) {
|
||||
if (err) cb(err);
|
||||
if (lodash.isString(bitpayAccounts)) {
|
||||
bitpayAccounts = JSON.parse(bitpayAccounts);
|
||||
}
|
||||
bitpayAccounts = bitpayAccounts || {};
|
||||
_asyncEach(Object.keys(bitpayAccounts), function(email, callback) {
|
||||
var data = bitpayAccounts[email]['bitpayDebitCards-' + network];
|
||||
var newCards = lodash.reject(data, {
|
||||
'eid': card.eid
|
||||
});
|
||||
data = {};
|
||||
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', callback());
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
});
|
||||
});
|
||||
}, function() {
|
||||
// done
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
root.removeBitpayDebitCard = function(network, cardEid, cb) {
|
||||
|
||||
// data: {
|
||||
// email: account email
|
||||
// token: account token
|
||||
// }
|
||||
root.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]['bitpayApi-' + network] = bitpayAccounts[data.email]['bitpayApi-' + network] || {};
|
||||
bitpayAccounts[data.email]['bitpayApi-' + network].token = data.token;
|
||||
storage.set('bitpayAccounts-v2-' + network, JSON.stringify(bitpayAccounts), cb);
|
||||
root.getBitpayAccounts(network, function(err, allAccounts){
|
||||
|
||||
lodash.each(allAccounts, function(account){
|
||||
account.cards = lodash.reject(account.cards, {
|
||||
'eid': cardEid
|
||||
});
|
||||
});
|
||||
|
||||
storage.set('bitpayAccounts-v2-' + network, allAccounts, cb);
|
||||
});
|
||||
};
|
||||
|
||||
// cb(err, accounts)
|
||||
// accounts: {
|
||||
// email_1: {
|
||||
// bitpayApi-<network>: {
|
||||
// token: account token
|
||||
// }
|
||||
// bitpayDebitCards-<network>: {
|
||||
// token: account token
|
||||
// cards: {
|
||||
// <card-data>
|
||||
// }
|
||||
// }
|
||||
// ...
|
||||
// email_n: {
|
||||
// bitpayApi-<network>: {
|
||||
// token: account token
|
||||
// }
|
||||
// bitpayDebitCards-<network>: {
|
||||
// token: account token
|
||||
// cards: {
|
||||
// <card-data>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
root.getBitpayAccounts = function(network, cb) {
|
||||
storage.get('bitpayAccounts-v2-' + network, function(err, bitpayAccounts) {
|
||||
storage.get('bitpayAccounts-v2-' + network, function(err, allAccountsStr) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(bitpayAccounts)) {
|
||||
bitpayAccounts = JSON.parse(bitpayAccounts);
|
||||
}
|
||||
cb(err, bitpayAccounts);
|
||||
|
||||
var allAccounts = {};
|
||||
try {
|
||||
allAccounts = JSON.parse(allAccountsStr);
|
||||
} catch (e) {};
|
||||
|
||||
var anyMigration;
|
||||
|
||||
lodash.each(allAccounts, function(account, email) {
|
||||
|
||||
// Migrate old `'bitpayApi-' + network` key, if exists
|
||||
if (!account.token && account['bitpayApi-' + network].token) {
|
||||
|
||||
$log.info('Migrating all bitpayApi-network branch');
|
||||
account.token = account['bitpayApi-' + network].token;
|
||||
account.cards = lodash.clone(account['bitpayApi-' + network].cards);
|
||||
if (!account.cards) {
|
||||
account.cards = lodash.clone(account['bitpayDebitCards-' + network]);
|
||||
}
|
||||
|
||||
delete account['bitpayDebitCards-' + network];
|
||||
delete account['bitpayApi-' + network];
|
||||
anyMigration = true;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (anyMigration) {
|
||||
storage.set('bitpayAccounts-v2-' + network, allAccounts, function(){
|
||||
return cb(err, allAccounts);
|
||||
});
|
||||
} else
|
||||
return cb(err, allAccounts);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// data: {
|
||||
// email: account email
|
||||
// token: account token
|
||||
// }
|
||||
root.setBitpayAccount = function(network, data, cb) {
|
||||
|
||||
if (!lodash.isObject(data) || !data.email || !data.token)
|
||||
return cb('No account to set');
|
||||
|
||||
var email = data.email;
|
||||
var token = data.token;
|
||||
|
||||
|
||||
root.getBitpayAccounts(network, function(err, allAccounts) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var account = allAccounts[email] || {};
|
||||
account.token = token;
|
||||
|
||||
allAccounts[email] = account;
|
||||
|
||||
$log.info('Storing BitPay accounts with new account:' + email);
|
||||
storage.set('bitpayAccounts-v2-' + network, allAccounts, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -828,10 +517,7 @@ angular.module('copayApp.services')
|
|||
root.getAppIdentity = function(network, cb) {
|
||||
storage.get('appIdentity-' + network, function(err, data) {
|
||||
if (err) return cb(err);
|
||||
if (lodash.isString(data)) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
cb(err, data);
|
||||
cb(err, JSON.parse(data || '{}'));
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -7,24 +7,6 @@
|
|||
|
||||
<ion-content>
|
||||
<div class="settings-list list">
|
||||
<div class="item item-divider" translate>Enabled Integrations</div>
|
||||
|
||||
<ion-toggle ng-show="!isWP" ng-model="bitpayCardEnabled.value" toggle-class="toggle-balanced" ng-change="bitpayCardChange()">
|
||||
<span class="toggle-label" translate>Enable BitPay Card Integration</span>
|
||||
</ion-toggle>
|
||||
|
||||
<ion-toggle ng-show="!isWP" ng-model="amazonEnabled.value" toggle-class="toggle-balanced" ng-change="amazonChange()">
|
||||
<span class="toggle-label" translate>Enable Amazon Integration</span>
|
||||
</ion-toggle>
|
||||
|
||||
<ion-toggle ng-show="!isWP" ng-model="glideraEnabled.value" toggle-class="toggle-balanced" ng-change="glideraChange()">
|
||||
<span class="toggle-label" translate>Enable Glidera Service</span>
|
||||
</ion-toggle>
|
||||
|
||||
<ion-toggle ng-show="!isWP" ng-model="coinbaseEnabled.value" toggle-class="toggle-balanced" ng-change="coinbaseChange()">
|
||||
<span class="toggle-label" translate>Enable Coinbase Service</span>
|
||||
</ion-toggle>
|
||||
|
||||
<div class="item item-divider" translate>Wallet Operation</div>
|
||||
|
||||
<ion-toggle class="has-comment" ng-model="spendUnconfirmed.value" toggle-class="toggle-balanced" ng-change="spendUnconfirmedChange()">
|
||||
|
@ -41,6 +23,13 @@
|
|||
If enabled, the Recent Transactions card - a list of transactions occuring across all wallets - will appear in the Home tab.
|
||||
</div>
|
||||
|
||||
|
||||
<ion-toggle ng-model="hideNextSteps.value" toggle-class="toggle-balanced" ng-change="nextStepsChange()">
|
||||
<span class="toggle-label" translate>Hide Next Steps Card</span>
|
||||
</ion-toggle>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-view>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<ion-nav-bar class="bar-royal">
|
||||
<ion-nav-back-button>
|
||||
</ion-nav-back-button>
|
||||
<ion-nav-title>BitPay Visa<sup>®</sup> Card ({{card.lastFourDigits}})</ion-nav-title>
|
||||
<ion-nav-title>BitPay Visa<sup>®</sup> Card ({{bitpayCard.lastFourDigits}})</ion-nav-title>
|
||||
</ion-nav-bar>
|
||||
|
||||
<ion-content>
|
||||
|
@ -14,8 +14,13 @@
|
|||
<div class="amount-wrapper" ng-show="!error">
|
||||
<div class="amount-bg"></div>
|
||||
<div class="amount">
|
||||
<div ng-if="bitpayCard.bitpayCardCurrentBalance" ng-click="bitpayCard.update()">
|
||||
<div class="amount__balance">${{bitpayCard.bitpayCardCurrentBalance}}</div>
|
||||
<div ng-if="bitpayCard.balance" ng-click="bitpayCard.update()">
|
||||
<div class="amount__balance">${{bitpayCard.balance}}</div>
|
||||
|
||||
<div class="size-12 m5r" ng-if="bitpayCard.updatedOn">
|
||||
{{bitpayCard.updatedOn * 1000 | amTimeAgo}}
|
||||
</div>
|
||||
|
||||
<a class="button button-primary button-small m5t size-14"
|
||||
style="padding: 0.5em 1em;"
|
||||
ui-sref="tabs.bitpayCard.amount({'cardId': cardId, 'toName': 'BitPay Card'})">
|
||||
|
@ -23,7 +28,7 @@
|
|||
{{'Add Funds'|translate}}
|
||||
</a>
|
||||
</div>
|
||||
<div ng-if="!bitpayCard.bitpayCardCurrentBalance" class="m10t">
|
||||
<div ng-if="!bitpayCard.balance" class="m10t">
|
||||
<strong class="size-36">...</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,17 +14,12 @@
|
|||
<div class="explain-description" translate>Buy and sell bitcoin directly from your wallet by connecting your exchange accounts.</div>
|
||||
</div>
|
||||
<div class="item item-divider"></div>
|
||||
|
||||
<a class="item item-icon-right" ui-sref="tabs.buyandsell.coinbase" ng-show="isCoinbaseEnabled">
|
||||
<img src="img/coinbase-logo.png" width="90">
|
||||
<span class="item-note" translate>33 Countries</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<a class="item item-icon-right" ui-sref="tabs.buyandsell.glidera" ng-show="isGlideraEnabled">
|
||||
<img src="img/glidera-logo.png" width="90">
|
||||
<span class="item-note" translate>US Only</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<div ng-repeat="service in services">
|
||||
<div class="item item-icon-right" ui-sref="{{service.sref}}">
|
||||
<img ng-src="{{service.logo}}" width="90">
|
||||
<span class="item-note" translate>{{service.location}}</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-view>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
<div class="list card">
|
||||
<div class="item item-icon-right item-heading">
|
||||
<span translate>Cards</span>
|
||||
<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"
|
||||
ui-sref="tabs.bitpayCard({id:card.id})"
|
||||
class="item item-sub item-icon-left item-big-icon-left item-icon-right">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-bitpay-card"></div>
|
||||
</i>
|
||||
<span>BitPay Visa® Card ({{card.lastFourDigits}})</span>
|
||||
<p>{{card.balance ? '$' + card.balance : 'Add funds to get started'|translate}} {{card.updatedOn ? (' · ' + (card.updatedOn * 1000 | amTimeAgo)) : ''}}</p>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<div class="list card" ng-controller="buyAndSellCardController">
|
||||
<div class="item item-sub item-icon-right item-heading">
|
||||
<span translate>Buy & Sell Bitcoin</span>
|
||||
<a ui-sref="tabs.buyandsell"><i class="icon ion-ios-plus-empty list-add-button"></i></a>
|
||||
</div>
|
||||
<div ng-repeat="service in services">
|
||||
<a ui-sref="{{service.sref}}" class="item item-extra-padding item-sub item-icon-right">
|
||||
<img ng-src="{{service.logo}}" width="90">
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,20 @@
|
|||
<div class="list card" ng-controller="homeIntegrationsController">
|
||||
<div class="item item-icon-right item-heading" ng-click="toggle()" >
|
||||
<span translate>Services</span>
|
||||
<i class="icon bp-arrow-up" ng-show="!hide"></i>
|
||||
<i class="icon bp-arrow-down" ng-show="hide"></i>
|
||||
</div>
|
||||
<div ng-show="!hide">
|
||||
<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">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg {{service.icon}}"></div>
|
||||
</i>
|
||||
<span>{{service.title || service.name}}</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<div class="list card" ng-controller="nextStepsController">
|
||||
<div class="item item-icon-right item-heading" ng-click="toggle()" >
|
||||
<span translate>Next steps</span>
|
||||
<i class="icon bp-arrow-up" ng-show="!hide"></i>
|
||||
<i class="icon bp-arrow-down" ng-show="hide"></i>
|
||||
</div>
|
||||
<div ng-show="!hide">
|
||||
<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">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg {{service.icon}}"></div>
|
||||
</i>
|
||||
<span>{{service.title || service.name}}</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,7 +90,8 @@
|
|||
Incomplete
|
||||
</span>
|
||||
<span ng-if="wallet.isComplete()">
|
||||
<span ng-if="!wallet.balanceHidden">{{wallet.status.totalBalanceStr}}</span>
|
||||
<span ng-if="!wallet.balanceHidden"> {{wallet.status.totalBalanceStr ? wallet.status.totalBalanceStr : ( wallet.cachedBalance ? wallet.cachedBalance + (wallet.cachedBalanceUpdatedOn ? ' · ' + ( wallet.cachedBalanceUpdatedOn * 1000 | amTimeAgo) : '') : '' ) }} </span>
|
||||
|
||||
<span ng-if="wallet.balanceHidden" translate>[Balance Hidden]</span>
|
||||
<span class="tab-home__wallet__multisig-number" ng-if="wallet.n > 1">
|
||||
{{wallet.m}}-of-{{wallet.n}}
|
||||
|
@ -104,84 +105,10 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list card" ng-if="bitpayCardEnabled && bitpayCards[0] && externalServices.BitpayCard">
|
||||
<div class="item item-icon-right item-heading">
|
||||
<span translate>Cards</span>
|
||||
<a ui-sref="tabs.bitpayCardIntro"><i class="icon ion-ios-plus-empty list-add-button"></i></a>
|
||||
</div>
|
||||
<div>
|
||||
<a ng-repeat="card in bitpayCards"
|
||||
ui-sref="tabs.bitpayCard({id:card.id})"
|
||||
class="item item-sub item-icon-left item-big-icon-left item-icon-right">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-bitpay-card"></div>
|
||||
</i>
|
||||
<span>BitPay Visa® Card ({{card.lastFourDigits}})</span>
|
||||
<p>{{cardsHistory[card.id].balance ? '$' + cardsHistory[card.id].balance : 'Add funds to get started'|translate}}</p>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list card" ng-if="wallets[0] && externalServices.BuyAndSell && (glideraEnabled || coinbaseEnabled)">
|
||||
<div class="item item-sub item-icon-right item-heading">
|
||||
<span translate>Buy & Sell Bitcoin</span>
|
||||
<a ui-sref="tabs.buyandsell"><i class="icon ion-ios-plus-empty list-add-button"></i></a>
|
||||
</div>
|
||||
<div>
|
||||
<a ng-if="glideraEnabled" ui-sref="tabs.buyandsell.glidera" class="item item-extra-padding item-sub item-icon-right">
|
||||
<img src="img/glidera-logo.png" width="90"/>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<a ng-if="coinbaseEnabled" ui-sref="tabs.buyandsell.coinbase" class="item item-extra-padding item-sub item-icon-right">
|
||||
<img src="img/coinbase-logo.png" width="90">
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list card" ng-if="wallets[0] && externalServices.AmazonGiftCards && amazonEnabled">
|
||||
<a class="item item-sub item-icon-left item-icon-right item-big-icon-left" ui-sref="tabs.giftcards.amazon">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-amazon"></div>
|
||||
</i>
|
||||
<span>Amazon.com Gift Cards</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="list card"
|
||||
ng-show="nextStepEnabled && wallets[0]">
|
||||
<div class="item item-icon-right item-heading" ng-click="shouldHideNextSteps()">
|
||||
<span translate>Next steps</span>
|
||||
<i class="icon bp-arrow-up" ng-show="!hideNextSteps"></i>
|
||||
<i class="icon bp-arrow-down" ng-show="hideNextSteps"></i>
|
||||
</div>
|
||||
<div ng-show="!hideNextSteps">
|
||||
<a ui-sref="tabs.bitpayCardIntro" ng-if="!externalServices.BitpayCard && bitpayCardEnabled" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-bitpay-card"></div>
|
||||
</i>
|
||||
<span translate>Add BitPay Visa® Card</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<a ng-if="!externalServices.BuyAndSell && (coinbaseEnabled || glideraEnabled)" ui-sref="tabs.buyandsell" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-buy-bitcoin"></div>
|
||||
</i>
|
||||
<span translate>Buy or Sell Bitcoin</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<a ui-sref="tabs.giftcards.amazon" ng-if="!externalServices.AmazonGiftCards && amazonEnabled" class="item item-sub item-icon-left item-big-icon-left item-icon-right next-step">
|
||||
<i class="icon big-icon-svg">
|
||||
<div class="bg icon-amazon"></div>
|
||||
</i>
|
||||
<span translate>Buy a gift card</span>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ng-hide" ng-show="wallets[0] && bitpayCardItems.length>0" ng-include="'views/includes/bitpayCardsCard.html'"></div>
|
||||
<div class="ng-hide" ng-show="wallets[0] && buyAndSellItems.length>0" ng-include="'views/includes/buyAndSellCard.html'"></div>
|
||||
<div class="ng-hide" ng-show="homeIntegrations.length>0" ng-include="'views/includes/homeIntegrations.html'"></div>
|
||||
<div class="ng-hide" ng-show="nextStepsItems.length>0" ng-include="'views/includes/nextSteps.html'"></div>
|
||||
|
||||
</ion-content>
|
||||
</ion-view>
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
</a>
|
||||
|
||||
<a class=" item item-icon-left item-icon-right"
|
||||
ng-if="bitpayCardEnabled && bitpayCards"
|
||||
ng-if="bitpayCards"
|
||||
ui-sref="tabs.preferences.bitpayCard">
|
||||
<i class="icon big-icon-svg circle">
|
||||
<div class="bg icon-bitpay-card"></div>
|
||||
|
@ -126,19 +126,13 @@
|
|||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
<a class="item item-icon-right"
|
||||
ng-if="glideraEnabled && glideraToken"
|
||||
ui-sref="tabs.preferences.glidera">
|
||||
<img src="img/glidera-logo.png" width="90"/>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
<a class="item item-icon-right"
|
||||
ng-if="coinbaseEnabled && coinbaseToken"
|
||||
ui-sref="tabs.preferences.coinbase">
|
||||
<img src="img/coinbase-logo.png" width="90"/>
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
<div ng-repeat="service in buyAndSellServices">
|
||||
<a class="item item-icon-right"
|
||||
ui-sref="{{service.configSref}}">
|
||||
<img ng-src="{{service.logo}}" width="90">
|
||||
<i class="icon bp-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item item-divider"></div>
|
||||
|
||||
|
|
Loading…
Reference in New Issue