diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 2a07c3ba5..259c4930b 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -20,6 +20,9 @@ angular.module('copayApp.controllers').controller('amountController', function($ $scope.isGlidera = data.stateParams.isGlidera; $scope.glideraAccessToken = data.stateParams.glideraAccessToken; + // Go to... + $scope.nextStep = data.stateParams.nextStep; + $scope.cardId = data.stateParams.cardId; $scope.showMenu = $ionicHistory.backView() && $ionicHistory.backView().stateName == 'tabs.send'; var isWallet = data.stateParams.isWallet || 'false'; @@ -27,13 +30,13 @@ angular.module('copayApp.controllers').controller('amountController', function($ $scope.toAddress = data.stateParams.toAddress; $scope.toName = data.stateParams.toName; $scope.toEmail = data.stateParams.toEmail; - $scope.showAlternativeAmount = !!$scope.cardId || !!$scope.isGiftCard || !!$scope.isGlidera; + $scope.showAlternativeAmount = !!$scope.cardId || !!$scope.isGiftCard || !!$scope.isGlidera || !!$scope.nextStep; $scope.toColor = data.stateParams.toColor; $scope.showSendMax = false; $scope.customAmount = data.stateParams.customAmount; - if (!$scope.cardId && !$scope.isGiftCard && !$scope.isGlidera && !data.stateParams.toAddress) { + if (!$scope.cardId && !$scope.isGiftCard && !$scope.isGlidera && !$scope.nextStep && !data.stateParams.toAddress) { $log.error('Bad params at amount') throw ('bad params'); } @@ -72,7 +75,11 @@ angular.module('copayApp.controllers').controller('amountController', function($ var config = configService.getSync().wallet.settings; $scope.unitName = config.unitName; - $scope.alternativeIsoCode = !!$scope.cardId || !!$scope.isGiftCard ? 'USD' : config.alternativeIsoCode; + if (data.stateParams.currency) { + $scope.alternativeIsoCode = data.stateParams.currency; + } else { + $scope.alternativeIsoCode = !!$scope.cardId || !!$scope.isGiftCard ? 'USD' : config.alternativeIsoCode; + } $scope.specificAmount = $scope.specificAlternativeAmount = ''; $scope.isCordova = platformInfo.isCordova; unitToSatoshi = config.unitToSatoshi; @@ -350,6 +357,11 @@ angular.module('copayApp.controllers').controller('amountController', function($ isGlidera: $scope.isGlidera, glideraAccessToken: $scope.glideraAccessToken }); + } else if ($scope.nextStep) { + $state.transitionTo($scope.nextStep, { + amount: _amount, + currency: $scope.showAlternativeAmount ? $scope.alternativeIsoCode : '' + }); } else { var amount = $scope.showAlternativeAmount ? fromFiat(_amount) : _amount; if ($scope.customAmount) { diff --git a/src/js/controllers/buyCoinbase.js b/src/js/controllers/buyCoinbase.js index e48dea1c7..587ef778e 100644 --- a/src/js/controllers/buyCoinbase.js +++ b/src/js/controllers/buyCoinbase.js @@ -1,175 +1,210 @@ 'use strict'; -angular.module('copayApp.controllers').controller('buyCoinbaseController', - function($scope, $log, $ionicModal, $timeout, lodash, profileService, coinbaseService, addressService, ongoingProcess) { - var self = this; +angular.module('copayApp.controllers').controller('buyCoinbaseController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicScrollDelegate, lodash, coinbaseService, popupService, profileService, ongoingProcess, walletService) { - this.init = function(testnet) { - self.allWallets = profileService.getWallets(testnet ? 'testnet' : 'livenet'); + var amount; + var currency; - var client = profileService.focusedClient; - if (client) { - $timeout(function() { - self.selectedWalletId = client.credentials.walletId; - self.selectedWalletName = client.credentials.walletName; - $scope.$apply(); - }, 100); + var showErrorAndBack = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err, function() { + $ionicHistory.goBack(); + }); + }; + + var showError = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err); + }; + + var statusChangeHandler = function (processName, showName, isOn) { + $log.debug('statusChangeHandler: ', processName, showName, isOn); + if ( processName == 'buyingBitcoin' && !isOn) { + $scope.sendStatus = 'success'; + $timeout(function() { + $scope.$digest(); + }, 100); + } else if (showName) { + $scope.sendStatus = showName; + } + }; + + $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, + data.stateParams.currency); + + $scope.network = coinbaseService.getNetwork(); + $scope.wallets = profileService.getWallets({ + onlyComplete: true, + network: $scope.network + }); + $scope.wallet = $scope.wallets[0]; // Default first wallet + + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; } - }; + var accessToken = res.accessToken; - this.getPaymentMethods = function(token) { - coinbaseService.getPaymentMethods(token, function(err, p) { + coinbaseService.buyPrice(accessToken, coinbaseService.getAvailableCurrency(), function(err, b) { + $scope.buyPrice = b.data || null; + }); + + $scope.paymentMethods = []; + $scope.selectedPaymentMethodId = { value : null }; + coinbaseService.getPaymentMethods(accessToken, function(err, p) { if (err) { - self.error = err; + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); return; } - self.paymentMethods = []; - lodash.each(p.data, function(pm) { + + var hasPrimary; + var pm; + for(var i = 0; i < p.data.length; i++) { + pm = p.data[i]; if (pm.allow_buy) { - self.paymentMethods.push(pm); + $scope.paymentMethods.push(pm); } if (pm.allow_buy && pm.primary_buy) { - $scope.selectedPaymentMethod = pm; + hasPrimary = true; + $scope.selectedPaymentMethodId.value = pm.id; } - }); + } + if (lodash.isEmpty($scope.paymentMethods)) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack('No payment method available to buy'); + return; + } + if (!hasPrimary) $scope.selectedPaymentMethodId.value = $scope.paymentMethods[0].id; + $scope.buyRequest(); }); - }; + }); + }); - this.getPrice = function(token) { - var currency = 'USD'; - coinbaseService.buyPrice(token, currency, function(err, b) { - if (err) return; - self.buyPrice = b.data || null; - }); - }; - - $scope.openWalletsModal = function(wallets) { - self.error = null; - - $scope.type = 'BUY'; - $scope.wallets = wallets; - $scope.noColor = true; - $scope.self = self; - - $ionicModal.fromTemplateUrl('views/modals/wallets.html', { - scope: $scope, - animation: 'slide-in-up' - }).then(function(modal) { - $scope.walletsModal = modal; - $scope.walletsModal.show(); - }); - - $scope.$on('walletSelected', function(ev, walletId) { - $timeout(function() { - var client = profileService.getClient(walletId); - self.selectedWalletId = walletId; - self.selectedWalletName = client.credentials.walletName; - $scope.$apply(); - }, 100); - $scope.walletsModal.hide(); - }); - }; - - this.buyRequest = function(token, account) { - self.error = null; - var accountId = account.id; - var amount = $scope.amount ? $scope.amount : $scope.fiat; - var currency = $scope.amount ? 'BTC' : 'USD'; - if (!amount) return; + $scope.buyRequest = function() { + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; var dataSrc = { amount: amount, currency: currency, - payment_method: $scope.selectedPaymentMethod.id || null + payment_method: $scope.selectedPaymentMethodId.value, + quote: true }; - ongoingProcess.set('Sending request...', true); - coinbaseService.buyRequest(token, accountId, dataSrc, function(err, data) { - ongoingProcess.set('Sending request...', false); + coinbaseService.buyRequest(accessToken, accountId, dataSrc, function(err, data) { + ongoingProcess.set('connectingCoinbase', false); if (err) { - self.error = err; + showErrorAndBack(err); return; } - self.buyInfo = data.data; + $scope.buyRequestInfo = data.data; + $timeout(function() { + $scope.$apply(); + }, 100); }); - }; + }); + }; - this.confirmBuy = function(token, account, buy) { - self.error = null; - var accountId = account.id; - var buyId = buy.id; - ongoingProcess.set('Buying Bitcoin...', true); - coinbaseService.buyCommit(token, accountId, buyId, function(err, b) { - ongoingProcess.set('Buying Bitcoin...', false); + $scope.buyConfirm = function() { + var message = 'Buy bitcoin for ' + amount + ' ' + currency; + var okText = 'Confirm'; + var cancelText = 'Cancel'; + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + if (!ok) return; + + ongoingProcess.set('buyingBitcoin', true, statusChangeHandler); + coinbaseService.init(function(err, res) { if (err) { - self.error = err; + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + showError(err); return; - } else { - var tx = b.data.transaction; - if (!tx) return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + var dataSrc = { + amount: amount, + currency: currency, + payment_method: $scope.selectedPaymentMethodId.value, + commit: true + }; + coinbaseService.buyRequest(accessToken, accountId, dataSrc, function(err, b) { + if (err) { + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + var tx = b.data ? b.data.transaction : null; + if (!tx) { + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + showError('Transaction not found'); + return; + } - ongoingProcess.set('Fetching transaction...', true); - coinbaseService.getTransaction(token, accountId, tx.id, function(err, updatedTx) { - ongoingProcess.set('Fetching transaction...', false); - if (err) $log.debug(err); - addressService.getAddress(self.selectedWalletId, false, function(err, addr) { + $timeout(function() { + coinbaseService.getTransaction(accessToken, accountId, tx.id, function(err, updatedTx) { if (err) { - self.error = { - errors: [{ - message: 'Could not create address' - }] - }; + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + showError(err); return; } - updatedTx.data['toAddr'] = addr; - coinbaseService.savePendingTransaction(updatedTx.data, {}, function(err) { - if (err) $log.debug(err); - if (updatedTx.data.status == 'completed') { - self.sendToCopay(token, account, updatedTx.data); - } else { - self.success = updatedTx.data; - $timeout(function() { - $scope.$emit('Local/CoinbaseTx'); - }, 1000); + walletService.getAddress($scope.wallet, false, function(err, walletAddr) { + if (err) { + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + showError(err); + return; } + updatedTx.data['toAddr'] = walletAddr; + updatedTx.data['status'] = 'pending'; // Forcing "pending" status to process later + + $log.debug('Saving transaction to process later...'); + coinbaseService.savePendingTransaction(updatedTx.data, {}, function(err) { + ongoingProcess.set('buyingBitcoin', false, statusChangeHandler); + if (err) $log.debug(err); + }); }); }); - }); - } + }, 8000); + }); }); - }; + }); + }; - this.sendToCopay = function(token, account, tx) { - self.error = null; - var accountId = account.id; + $scope.showWalletSelector = function() { + $scope.walletSelectorTitle = 'Receive in'; + $scope.showWallets = true; + }; - ongoingProcess.set('Sending funds to Copay...', true); - var data = { - to: tx.toAddr, - amount: tx.amount.amount, - currency: tx.amount.currency, - description: 'Copay Wallet: ' + self.selectedWalletName - }; - coinbaseService.sendTo(token, accountId, data, function(err, res) { - ongoingProcess.set('Sending funds to Copay...', false); - if (err) { - self.error = err; - } else { - self.receiveInfo = res.data; - if (!res.data.id) return; - coinbaseService.getTransaction(token, accountId, res.data.id, function(err, sendTx) { - coinbaseService.savePendingTransaction(tx, { - remove: true - }, function(err) { - coinbaseService.savePendingTransaction(sendTx.data, {}, function(err) { - $timeout(function() { - $scope.$emit('Local/CoinbaseTx'); - }, 1000); - }); - }); - }); - } + $scope.onWalletSelect = function(wallet) { + $scope.wallet = wallet; + }; - }); - }; - - - }); + $scope.goBackHome = function() { + $scope.sendStatus = ''; + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.buyandsell.coinbase'); + }); + }; +}); diff --git a/src/js/controllers/coinbase.js b/src/js/controllers/coinbase.js index 76b93c9c7..8b23d9b28 100644 --- a/src/js/controllers/coinbase.js +++ b/src/js/controllers/coinbase.js @@ -1,78 +1,117 @@ 'use strict'; -angular.module('copayApp.controllers').controller('coinbaseController', - function($rootScope, $scope, $timeout, $ionicModal, profileService, configService, storageService, coinbaseService, lodash, platformInfo, ongoingProcess) { +angular.module('copayApp.controllers').controller('coinbaseController', function($scope, $timeout, $ionicModal, $log, coinbaseService, lodash, platformInfo, ongoingProcess, popupService, externalLinkService) { - var isNW = platformInfo.isNW; + var isNW = platformInfo.isNW; + var isCordova = platformInfo.isCordova; - if (platformInfo.isCordova && StatusBar.isVisible) { - StatusBar.backgroundColorByHexString("#4B6178"); - } - - this.openAuthenticateWindow = function() { - var oauthUrl = this.getAuthenticateUrl(); - if (!isNW) { - $rootScope.openExternalLink(oauthUrl, '_system'); - } else { - var self = this; - var gui = require('nw.gui'); - var win = gui.Window.open(oauthUrl, { - focus: true, - position: 'center' - }); - win.on('loaded', function() { - var title = win.title; - if (title.indexOf('Coinbase') == -1) { - $scope.code = title; - self.submitOauthCode(title); - win.close(); - } - }); - } - } - - this.getAuthenticateUrl = function() { - return coinbaseService.getOauthCodeUrl(); - }; - - this.submitOauthCode = function(code) { - var self = this; - var coinbaseTestnet = configService.getSync().coinbase.testnet; - var network = coinbaseTestnet ? 'testnet' : 'livenet'; - ongoingProcess.set('connectingCoinbase', true); - this.error = null; - $timeout(function() { - coinbaseService.getToken(code, function(err, data) { - ongoingProcess.set('connectingCoinbase', false); + var init = function() { + $scope.currency = coinbaseService.getAvailableCurrency(); + coinbaseService.getStoredToken(function(at) { + $scope.accessToken = at; + + // Update Access Token if necessary + $scope.loading = true; + coinbaseService.init(function(err, data) { + $scope.loading = false; + if (err || lodash.isEmpty(data)) { if (err) { - self.error = err; - $timeout(function() { - $scope.$apply(); - }, 100); - } else if (data && data.access_token && data.refresh_token) { - storageService.setCoinbaseToken(network, data.access_token, function() { - storageService.setCoinbaseRefreshToken(network, data.refresh_token, function() { - $scope.$emit('Local/CoinbaseUpdated', data.access_token); - $timeout(function() { - $scope.$apply(); - }, 100); - }); - }); + popupService.showAlert('Error', err); } + return; + } + + // Show rates + coinbaseService.buyPrice(data.accessToken, $scope.currency, function(err, b) { + $scope.buyPrice = b.data || null; + }); + coinbaseService.sellPrice(data.accessToken, $scope.currency, function(err, s) { + $scope.sellPrice = s.data || null; }); - }, 100); - }; - this.openTxModal = function(tx) { - $scope.tx = tx; - - $ionicModal.fromTemplateUrl('views/modals/coinbase-tx-details.html', { - scope: $scope, - animation: 'slide-in-up' - }).then(function(modal) { - $scope.coinbaseTxDetailsModal = modal; - $scope.coinbaseTxDetailsModal.show(); + // Updating accessToken and accountId + $timeout(function() { + $scope.accessToken = data.accessToken; + $scope.accountId = data.accountId; + $scope.updateTransactions(); + $scope.$apply(); + }, 100); }); - }; + }); + }; + $scope.updateTransactions = function() { + $log.debug('Getting transactions...'); + $scope.pendingTransactions = { data: {} }; + coinbaseService.getPendingTransactions($scope.pendingTransactions); + }; + + this.openAuthenticateWindow = function() { + var oauthUrl = this.getAuthenticateUrl(); + if (!isNW) { + externalLinkService.open(oauthUrl); + } else { + var self = this; + var gui = require('nw.gui'); + gui.Window.open(oauthUrl, { + focus: true, + position: 'center' + }, function(new_win) { + new_win.on('loaded', function() { + var title = new_win.window.document.title; + $timeout(function() { + if (title.indexOf('Coinbase') == -1) { + $scope.code = title; + self.submitOauthCode($scope.code); + new_win.close(); + } + }, 100); + }); + }); + } + } + + this.getAuthenticateUrl = function() { + $scope.showOauthForm = isCordova || isNW ? false : true; + return coinbaseService.getOauthCodeUrl(); + }; + + this.submitOauthCode = function(code) { + var self = this; + ongoingProcess.set('connectingCoinbase', true); + $scope.error = null; + $timeout(function() { + coinbaseService.getToken(code, function(err, accessToken) { + ongoingProcess.set('connectingCoinbase', false); + if (err) { + popupService.showAlert('Error', err); + return; + } + $scope.accessToken = accessToken; + init(); + }); + }, 100); + }; + + this.openTxModal = function(tx) { + $scope.tx = tx; + + $ionicModal.fromTemplateUrl('views/modals/coinbase-tx-details.html', { + scope: $scope, + animation: 'slide-in-up' + }).then(function(modal) { + $scope.modal = modal; + $scope.modal.show(); + }); + }; + + var self = this; + $scope.$on("$ionicView.beforeEnter", function(event, data) { + coinbaseService.setCredentials(); + if (data.stateParams && data.stateParams.code) { + self.submitOauthCode(data.stateParams.code); + } else { + init(); + } }); +}); diff --git a/src/js/controllers/coinbaseUri.js b/src/js/controllers/coinbaseUri.js deleted file mode 100644 index b92c1b355..000000000 --- a/src/js/controllers/coinbaseUri.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; -angular.module('copayApp.controllers').controller('coinbaseUriController', - function($scope, $stateParams, $timeout, profileService, configService, coinbaseService, storageService, $state, ongoingProcess) { - - this.submitOauthCode = function(code) { - var self = this; - var coinbaseTestnet = configService.getSync().coinbase.testnet; - var network = coinbaseTestnet ? 'testnet' : 'livenet'; - ongoingProcess.set('connectingCoinbase', true); - this.error = null; - $timeout(function() { - coinbaseService.getToken(code, function(err, data) { - ongoingProcess.set('connectingCoinbase', false); - if (err) { - self.error = err; - $timeout(function() { - $scope.$apply(); - }, 100); - } else if (data && data.access_token && data.refresh_token) { - storageService.setCoinbaseToken(network, data.access_token, function() { - storageService.setCoinbaseRefreshToken(network, data.refresh_token, function() { - $scope.$emit('Local/CoinbaseUpdated', data.access_token); - $timeout(function() { - $state.go('coinbase'); - $scope.$apply(); - }, 100); - }); - }); - } - }); - }, 100); - }; - - this.checkCode = function() { - if ($stateParams.url) { - var match = $stateParams.url.match(/code=(.+)&/); - if (match && match[1]) { - this.code = match[1]; - return this.submitOauthCode(this.code); - } - } - $log.error('Bad state: ' + JSON.stringify($stateParams)); - } - }); diff --git a/src/js/controllers/confirm.js b/src/js/controllers/confirm.js index 9ebcf507f..9e2432e16 100644 --- a/src/js/controllers/confirm.js +++ b/src/js/controllers/confirm.js @@ -528,7 +528,7 @@ angular.module('copayApp.controllers').controller('confirmController', function( }); return; } - + ongoingProcess.set('creatingTx', true, onSendStatusChange); createTx(wallet, false, function(err, txp) { ongoingProcess.set('creatingTx', false, onSendStatusChange); @@ -577,12 +577,14 @@ angular.module('copayApp.controllers').controller('confirmController', function( $log.debug('statusChangeHandler: ', processName, showName, isOn); if ( ( - processName === 'broadcastingTx' || - ((processName === 'signingTx') && $scope.wallet.m > 1) || + processName === 'broadcastingTx' || + ((processName === 'signingTx') && $scope.wallet.m > 1) || (processName == 'sendingTx' && !$scope.wallet.canSign() && !$scope.wallet.isPrivKeyExternal()) ) && !isOn) { $scope.sendStatus = 'success'; - $scope.$digest(); + $timeout(function() { + $scope.$digest(); + }, 100); } else if (showName) { $scope.sendStatus = showName; } @@ -831,7 +833,7 @@ angular.module('copayApp.controllers').controller('confirmController', function( debounceCreate(count, dataSrc, onSendStatusChange); } }, onSendStatusChange); - } + }; var debounceCreate = lodash.throttle(function(count, dataSrc) { debounceCreateGiftCard(count, dataSrc); diff --git a/src/js/controllers/modals/coinbaseConfirmation.js b/src/js/controllers/modals/coinbaseConfirmation.js deleted file mode 100644 index 729b81945..000000000 --- a/src/js/controllers/modals/coinbaseConfirmation.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('coinbaseConfirmationController', function($scope, $timeout, coinbaseService, applicationService) { - - $scope.ok = function() { - - coinbaseService.logout($scope.network, function() { - - $timeout(function() { - applicationService.restart(); - }, 1000); - }); - $scope.cancel(); - }; - - $scope.cancel = function() { - $scope.coinbaseConfirmationModal.hide(); - }; - -}); diff --git a/src/js/controllers/modals/coinbaseTxDetails.js b/src/js/controllers/modals/coinbaseTxDetails.js index feebfd47d..bb9cd3020 100644 --- a/src/js/controllers/modals/coinbaseTxDetails.js +++ b/src/js/controllers/modals/coinbaseTxDetails.js @@ -1,18 +1,28 @@ 'use strict'; -angular.module('copayApp.controllers').controller('coinbaseTxDetailsController', function($scope, $rootScope, coinbaseService) { +angular.module('copayApp.controllers').controller('coinbaseTxDetailsController', function($scope, coinbaseService, popupService) { $scope.remove = function() { - coinbaseService.savePendingTransaction($scope.tx, { - remove: true - }, function(err) { - $rootScope.$emit('Local/CoinbaseTx'); - $scope.cancel(); + coinbaseService.setCredentials(); + $scope.updateRequired = false; + var message = 'Are you sure you want to remove this transaction?'; + popupService.showConfirm(null, message, null, null, function(ok) { + if (!ok) { + return; + } + coinbaseService.savePendingTransaction($scope.tx, { + remove: true + }, function(err) { + $scope.updateRequired = true; + $scope.close(); + }); }); }; - $scope.cancel = function() { - $scope.coinbaseTxDetailsModal.hide(); + $scope.close = function() { + $scope.modal.hide().then(function() { + if ($scope.updateRequired) $scope.updateTransactions(); + }); }; }); diff --git a/src/js/controllers/preferencesCoinbase.js b/src/js/controllers/preferencesCoinbase.js index 6bca19de6..c55e996ef 100644 --- a/src/js/controllers/preferencesCoinbase.js +++ b/src/js/controllers/preferencesCoinbase.js @@ -1,18 +1,41 @@ 'use strict'; -angular.module('copayApp.controllers').controller('preferencesCoinbaseController', - function($scope, $timeout, $ionicModal, applicationService, coinbaseService) { +angular.module('copayApp.controllers').controller('preferencesCoinbaseController', function($scope, $timeout, $state, $ionicHistory, lodash, ongoingProcess, popupService, coinbaseService) { - this.revokeToken = function(testnet) { - $scope.network = testnet ? 'testnet' : 'livenet'; + $scope.revokeToken = function() { + popupService.showConfirm('Coinbase', 'Are you sure you would like to log out of your Coinbase account?', null, null, function(res) { + if (res) { + coinbaseService.logout(function() { + $ionicHistory.clearHistory(); + $timeout(function() { + $state.go('tabs.home'); + }, 100); + }); + } + }); + }; - $ionicModal.fromTemplateUrl('views/modals/coinbase-confirmation.html', { - scope: $scope, - animation: 'slide-in-up' - }).then(function(modal) { - $scope.coinbaseConfirmationModal = modal; - $scope.coinbaseConfirmationModal.show(); + $scope.$on("$ionicView.enter", function(event, data){ + coinbaseService.setCredentials(); + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, data) { + if (err || lodash.isEmpty(data)) { + ongoingProcess.set('connectingCoinbase', false); + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err); + } + return; + } + var accessToken = data.accessToken; + var accountId = data.accountId; + coinbaseService.getAccount(accessToken, accountId, function(err, account) { + ongoingProcess.set('connectingCoinbase', false); + $scope.coinbaseAccount = account.data; }); - }; - + coinbaseService.getCurrentUser(accessToken, function(err, user) { + $scope.coinbaseUser = user.data; + }); + }); }); + +}); diff --git a/src/js/controllers/sellCoinbase.js b/src/js/controllers/sellCoinbase.js index 6ce9b28b3..d7b385e72 100644 --- a/src/js/controllers/sellCoinbase.js +++ b/src/js/controllers/sellCoinbase.js @@ -1,187 +1,258 @@ 'use strict'; -angular.module('copayApp.controllers').controller('sellCoinbaseController', - function($rootScope, $scope, $log, $timeout, $ionicModal, lodash, profileService, coinbaseService, configService, walletService, fingerprintService, ongoingProcess, go) { +angular.module('copayApp.controllers').controller('sellCoinbaseController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicScrollDelegate, lodash, coinbaseService, popupService, profileService, ongoingProcess, walletService, appConfigService, configService) { - var self = this; - var client; + var amount; + var currency; - $scope.priceSensitivity = [ - { - value: 0.5, - name: '0.5%' - }, - { - value: 1, - name: '1%' - }, - { - value: 2, - name: '2%' - }, - { - value: 5, - name: '5%' - }, - { - value: 10, - name: '10%' - } - ]; - $scope.selectedPriceSensitivity = $scope.priceSensitivity[1]; + var showErrorAndBack = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err, function() { + $ionicHistory.goBack(); + }); + }; - this.init = function(testnet) { - self.allWallets = profileService.getWallets(testnet ? 'testnet' : 'livenet', 1); + var showError = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err); + }; - client = profileService.focusedClient; - if (client && client.credentials.m == 1) { - $timeout(function() { - self.selectedWalletId = client.credentials.walletId; - self.selectedWalletName = client.credentials.walletName; - $scope.$apply(); - }, 100); - } - }; + var publishAndSign = function (wallet, txp, onSendStatusChange, cb) { + if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { + var err = 'No signing proposal: No private key'; + $log.info(err); + return cb(err); + } - this.getPaymentMethods = function(token) { - coinbaseService.getPaymentMethods(token, function(err, p) { - if (err) { - self.error = err; - return; - } - self.paymentMethods = []; - lodash.each(p.data, function(pm) { - if (pm.allow_sell) { - self.paymentMethods.push(pm); - } - if (pm.allow_sell && pm.primary_sell) { - $scope.selectedPaymentMethod = pm; - } - }); - }); - }; + walletService.publishAndSign(wallet, txp, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }, onSendStatusChange); + }; - this.getPrice = function(token) { - var currency = 'USD'; - coinbaseService.sellPrice(token, currency, function(err, s) { - if (err) return; - self.sellPrice = s.data || null; - }); - }; - - $scope.openWalletsModal = function(wallets) { - self.error = null; - - $scope.type = 'SELL'; - $scope.wallets = wallets; - $scope.noColor = true; - $scope.self = self; - - $ionicModal.fromTemplateUrl('views/modals/wallets.html', { - scope: $scope, - animation: 'slide-in-up' - }).then(function(modal) { - $scope.walletsModal = modal; - $scope.walletsModal.show(); - }); - - $scope.$on('walletSelected', function(ev, walletId) { - $timeout(function() { - client = profileService.getClient(walletId); - self.selectedWalletId = walletId; - self.selectedWalletName = client.credentials.walletName; - $scope.$apply(); - }, 100); - $scope.walletsModal.hide(); - }); - }; - - this.depositFunds = function(token, account) { - self.error = null; - if ($scope.amount) { - this.createTx(token, account, $scope.amount) - } else if ($scope.fiat) { - var btcValue = ($scope.fiat / self.sellPrice.amount).toFixed(8); - this.createTx(token, account, btcValue); - } - }; - - this.sellRequest = function(token, account, ctx) { - self.error = null; - if (!ctx.amount) return; - var accountId = account.id; - var data = ctx.amount; - data['payment_method'] = $scope.selectedPaymentMethod.id || null; - ongoingProcess.set('Sending request...', true); - coinbaseService.sellRequest(token, accountId, data, function(err, sell) { - ongoingProcess.set('Sending request...', false); - if (err) { - self.error = err; - return; - } - self.sellInfo = sell.data; - }); - }; - - this.confirmSell = function(token, account, sell) { - self.error = null; - var accountId = account.id; - var sellId = sell.id; - ongoingProcess.set('Selling Bitcoin...', true); - coinbaseService.sellCommit(token, accountId, sellId, function(err, data) { - ongoingProcess.set('Selling Bitcoin...', false); - if (err) { - self.error = err; - return; - } - self.success = data.data; - $scope.$emit('Local/CoinbaseTx'); - }); - }; - - this.createTx = function(token, account, amount) { - self.error = null; - - if (!client) { - self.error = 'No wallet selected'; + var checkTransaction = lodash.throttle(function(count, txp) { + $log.warn('Check if transaction has been received by Coinbase. Try ' + count + '/5'); + // TX amount in BTC + var satToBtc = 1 / 100000000; + var amountBTC = (txp.amount * satToBtc).toFixed(8); + coinbaseService.init(function(err, res) { + if (err) { + $log.error(err); + checkTransaction(count, txp); return; } + var accessToken = res.accessToken; + var accountId = res.accountId; + var sellPrice = null; + + coinbaseService.sellPrice(accessToken, coinbaseService.getAvailableCurrency(), function(err, sell) { + if (err) { + $log.debug(err); + checkTransaction(count, txp); + return; + } + sellPrice = sell.data; - var accountId = account.id; - var dataSrc = { - name: 'Received from Copay: ' + self.selectedWalletName - }; - var outputs = []; - var config = configService.getSync(); - var configWallet = config.wallet; - var walletSettings = configWallet.settings; - - - ongoingProcess.set('Creating Transaction...', true); - $timeout(function() { - - coinbaseService.createAddress(token, accountId, dataSrc, function(err, data) { + coinbaseService.getTransactions(accessToken, accountId, function(err, ctxs) { if (err) { - ongoingProcess.set('Creating Transaction...', false); - self.error = err; + $log.debug(err); + checkTransaction(count, txp); return; } - var address, comment; + var coinbaseTransactions = ctxs.data; + var txFound = false; + var ctx; + for(var i = 0; i < coinbaseTransactions.length; i++) { + ctx = coinbaseTransactions[i]; + if (ctx.type == 'send' && ctx.from && ctx.amount.amount == amountBTC ) { + $log.warn('Transaction found!', ctx); + txFound = true; + $log.debug('Saving transaction to process later...'); + ctx['payment_method'] = $scope.selectedPaymentMethodId.value; + ctx['status'] = 'pending'; // Forcing "pending" status to process later + ctx['price_sensitivity'] = $scope.selectedPriceSensitivity.data; + ctx['sell_price_amount'] = sellPrice ? sellPrice.amount : ''; + ctx['sell_price_currency'] = sellPrice ? sellPrice.currency : 'USD'; + ctx['description'] = appConfigService.nameCase + ' Wallet: ' + $scope.wallet.name; + coinbaseService.savePendingTransaction(ctx, null, function(err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + if (err) $log.debug(err); + }); + return; + } + } + if (!txFound) { + // Transaction sent, but could not be verified by Coinbase.com + $log.warn('Transaction not found in Coinbase.'); + if (count < 5) { + checkTransaction(count + 1, txp); + } else { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError('No transaction found'); + return; + } + } + }); + }); + }); + }, 8000, { + 'leading': true + }); - address = data.data.address; - amount = parseInt((amount * 100000000).toFixed(0)); - comment = 'Send funds to Coinbase Account: ' + account.name; + var statusChangeHandler = function (processName, showName, isOn) { + $log.debug('statusChangeHandler: ', processName, showName, isOn); + if ( processName == 'sellingBitcoin' && !isOn) { + $scope.sendStatus = 'success'; + $timeout(function() { + $scope.$digest(); + }, 100); + } else if (showName) { + $scope.sendStatus = showName; + } + }; + + $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, + data.stateParams.currency); + + $scope.priceSensitivity = coinbaseService.priceSensitivity; + $scope.selectedPriceSensitivity = { data: coinbaseService.selectedPriceSensitivity }; + + $scope.network = coinbaseService.getNetwork(); + $scope.wallets = profileService.getWallets({ + m: 1, // Only 1-signature wallet + onlyComplete: true, + network: $scope.network + }); + $scope.wallet = $scope.wallets[0]; // Default first wallet + + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var accessToken = res.accessToken; + + coinbaseService.sellPrice(accessToken, coinbaseService.getAvailableCurrency(), function(err, s) { + $scope.sellPrice = s.data || null; + }); + + $scope.paymentMethods = []; + $scope.selectedPaymentMethodId = { value : null }; + coinbaseService.getPaymentMethods(accessToken, function(err, p) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var hasPrimary; + var pm; + for(var i = 0; i < p.data.length; i++) { + pm = p.data[i]; + if (pm.allow_sell) { + $scope.paymentMethods.push(pm); + } + if (pm.allow_sell && pm.primary_sell) { + hasPrimary = true; + $scope.selectedPaymentMethodId.value = pm.id; + } + } + if (lodash.isEmpty($scope.paymentMethods)) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack('No payment method available to sell'); + return; + } + if (!hasPrimary) $scope.selectedPaymentMethodId.value = $scope.paymentMethods[0].id; + $scope.sellRequest(); + }); + }); + }); + + $scope.sellRequest = function() { + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + var dataSrc = { + amount: amount, + currency: currency, + payment_method: $scope.selectedPaymentMethodId.value, + quote: true + }; + coinbaseService.sellRequest(accessToken, accountId, dataSrc, function(err, data) { + ongoingProcess.set('connectingCoinbase', false); + if (err) { + showErrorAndBack(err); + return; + } + $scope.sellRequestInfo = data.data; + $timeout(function() { + $scope.$apply(); + }, 100); + }); + }); + }; + + $scope.sellConfirm = function() { + var config = configService.getSync(); + var configWallet = config.wallet; + var walletSettings = configWallet.settings; + + var message = 'Selling bitcoin for ' + amount + ' ' + currency; + var okText = 'Confirm'; + var cancelText = 'Cancel'; + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + if (!ok) return; + + ongoingProcess.set('sellingBitcoin', true, statusChangeHandler); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + + var dataSrc = { + name: 'Received from ' + appConfigService.nameCase + }; + coinbaseService.createAddress(accessToken, accountId, dataSrc, function(err, data) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + var outputs = []; + var toAddress = data.data.address; + var amountSat = parseInt(($scope.sellRequestInfo.amount.amount * 100000000).toFixed(0)); + var comment = 'Sell bitcoin (Coinbase)'; outputs.push({ - 'toAddress': address, - 'amount': amount, + 'toAddress': toAddress, + 'amount': amountSat, 'message': comment }); var txp = { - toAddress: address, - amount: amount, + toAddress: toAddress, + amount: amountSat, outputs: outputs, message: comment, payProUrl: null, @@ -189,73 +260,47 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', feeLevel: walletSettings.feeLevel || 'normal' }; - walletService.createTx(client, txp, function(err, createdTxp) { + walletService.createTx($scope.wallet, txp, function(err, ctxp) { if (err) { - $log.debug(err); - ongoingProcess.set('Creating Transaction...', false); - self.error = { - errors: [{ - message: 'Could not create transaction: ' + err.message - }] - }; - $scope.$apply(); + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); return; } - ongoingProcess.set('Creating Transaction...', false); - $scope.$emit('Local/NeedsConfirmation', createdTxp, function(accept) { - if (accept) { - self.confirmTx(createdTxp, function(err, tx) { - ongoingProcess.clear(); - if (err) { - self.error = { - errors: [{ - message: 'Could not create transaction: ' + err.message - }] - }; - return; - } - ongoingProcess.set('Checking Transaction...', false); - coinbaseService.getTransactions(token, accountId, function(err, ctxs) { - if (err) { - $log.debug(err); - return; - } - lodash.each(ctxs.data, function(ctx) { - if (ctx.type == 'send' && ctx.from) { - ongoingProcess.clear(); - if (ctx.status == 'completed') { - self.sellRequest(token, account, ctx); - } else { - // Save to localstorage - ctx['price_sensitivity'] = $scope.selectedPriceSensitivity; - ctx['sell_price_amount'] = self.sellPrice ? self.sellPrice.amount : ''; - ctx['sell_price_currency'] = self.sellPrice ? self.sellPrice.currency : 'USD'; - ctx['description'] = 'Copay Wallet: ' + client.credentials.walletName; - coinbaseService.savePendingTransaction(ctx, null, function(err) { - if (err) $log.debug(err); - self.sendInfo = ctx; - $timeout(function() { - $scope.$emit('Local/CoinbaseTx'); - }, 1000); - }); - } - return false; - } - }); - }); - }); - } else { - go.path('coinbase'); + $log.debug('Transaction created.'); + publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; } + $log.debug('Transaction broadcasted. Wait for Coinbase confirmation...'); + checkTransaction(1, txSent); }); }); - }); - }, 100); - }; + }); + }); + }); + }; - this.confirmTx = function(txp, cb) { + $scope.showWalletSelector = function() { + $scope.walletSelectorTitle = 'Sell From'; + $scope.showWallets = true; + }; - // TODO see walletService createAndPublish - }; + $scope.onWalletSelect = function(wallet) { + $scope.wallet = wallet; + }; - }); + $scope.goBackHome = function() { + $scope.sendStatus = ''; + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.buyandsell.coinbase'); + }); + }; + +}); diff --git a/src/js/controllers/tab-home.js b/src/js/controllers/tab-home.js index 9ef5ce4a7..ac2a6b471 100644 --- a/src/js/controllers/tab-home.js +++ b/src/js/controllers/tab-home.js @@ -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) { + 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) { var wallet; var listeners = []; var notifications = []; @@ -83,6 +83,10 @@ angular.module('copayApp.controllers').controller('tabHomeController', var wallet = profileService.getWallet(walletId); updateWallet(wallet); if ($scope.recentTransactionsEnabled) getNotifications(); + if (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); diff --git a/src/js/controllers/tab-settings.js b/src/js/controllers/tab-settings.js index b5573155e..a92d8cb6c 100644 --- a/src/js/controllers/tab-settings.js +++ b/src/js/controllers/tab-settings.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $ionicModal, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayCardService, storageService, glideraService, gettextCatalog) { +angular.module('copayApp.controllers').controller('tabSettingsController', function($scope, appConfigService, $log, lodash, uxLanguage, platformInfo, profileService, feeService, configService, externalLinkService, bitpayCardService, storageService, glideraService, coinbaseService, gettextCatalog) { var updateConfig = function() { var isCordova = platformInfo.isCordova; @@ -26,6 +26,7 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct $scope.bitpayCardEnabled = config.bitpayCard.enabled; $scope.glideraEnabled = config.glidera.enabled && !isWindowsPhoneApp; + $scope.coinbaseEnabled = config.coinbase.enabled && !isWindowsPhoneApp; if ($scope.bitpayCardEnabled) { bitpayCardService.getBitpayDebitCards(function(err, cards) { @@ -41,6 +42,13 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct }); } + if ($scope.coinbaseEnabled) { + coinbaseService.setCredentials(); + coinbaseService.getStoredToken(function(at) { + $scope.coinbaseToken = at; + }); + } + }); }; diff --git a/src/js/routes.js b/src/js/routes.js index f9a02560b..e88c15275 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -142,10 +142,6 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr controller: 'glideraUriController', templateUrl: 'views/glideraUri.html' }) - .state('uricoinbase', { - url: '/uri-coinbase/:url', - templateUrl: 'views/coinbaseUri.html' - }) /* * @@ -926,22 +922,52 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr * */ - .state('coinbase', { - url: '/coinbase', - templateUrl: 'views/coinbase.html' - }) - .state('preferencesCoinbase', { - url: '/preferencesCoinbase', - templateUrl: 'views/preferencesCoinbase.html' - }) - .state('buyCoinbase', { - url: '/buycoinbase', - templateUrl: 'views/buyCoinbase.html' - }) - .state('sellCoinbase', { - url: '/sellcoinbase', - templateUrl: 'views/sellCoinbase.html' - }) + .state('tabs.buyandsell.coinbase', { + url: '/coinbase/:code', + views: { + 'tab-home@tabs': { + controller: 'coinbaseController', + controllerAs: 'coinbase', + templateUrl: 'views/coinbase.html' + } + } + }) + .state('tabs.preferences.coinbase', { + url: '/coinbase', + views: { + 'tab-settings@tabs': { + controller: 'preferencesCoinbaseController', + templateUrl: 'views/preferencesCoinbase.html' + } + } + }) + .state('tabs.buyandsell.coinbase.amount', { + url: '/amount/:nextStep/:currency', + views: { + 'tab-home@tabs': { + controller: 'amountController', + templateUrl: 'views/amount.html' + } + } + }) + .state('tabs.buyandsell.coinbase.buy', { + url: '/buy/:amount/:currency', + views: { + 'tab-home@tabs': { + controller: 'buyCoinbaseController', + templateUrl: 'views/buyCoinbase.html' + } + } + }) + .state('tabs.buyandsell.coinbase.sell', { + url: '/sell/:amount/:currency', + views: { + 'tab-home@tabs': { + controller: 'sellCoinbaseController', + templateUrl: 'views/sellCoinbase.html' + } + } + }) /* * diff --git a/src/js/services/coinbaseService.js b/src/js/services/coinbaseService.js index ce5b984cd..307638c40 100644 --- a/src/js/services/coinbaseService.js +++ b/src/js/services/coinbaseService.js @@ -1,11 +1,51 @@ 'use strict'; -angular.module('copayApp.services').factory('coinbaseService', function($http, $log, platformInfo, lodash, storageService, configService) { +angular.module('copayApp.services').factory('coinbaseService', function($http, $log, $window, $filter, platformInfo, lodash, storageService, configService, appConfigService, txFormatService) { var root = {}; var credentials = {}; var isCordova = platformInfo.isCordova; + var isNW = platformInfo.isNW; - root.setCredentials = function(network) { + 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() { + + if (!$window.externalServices || !$window.externalServices.coinbase) { + return; + } + + var coinbase = $window.externalServices.coinbase; + + /* + * Development: 'testnet' + * Production: 'livenet' + */ + credentials.NETWORK = 'livenet'; + + // Coinbase permissions credentials.SCOPE = '' + 'wallet:accounts:read,' + 'wallet:addresses:read,' @@ -20,26 +60,78 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ + 'wallet:transactions:send,' + 'wallet:payment-methods:read'; + // NW has a bug with Window Object if (isCordova) { - credentials.REDIRECT_URI = 'copay://coinbase'; + credentials.REDIRECT_URI = coinbase.redirect_uri.mobile; } else { - credentials.REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'; + credentials.REDIRECT_URI = coinbase.redirect_uri.desktop; } - if (network == 'testnet') { - credentials.HOST = 'https://sandbox.coinbase.com'; - credentials.API = 'https://api.sandbox.coinbase.com'; - credentials.CLIENT_ID = '6cdcc82d5d46654c46880e93ab3d2a43c639776347dd88022904bd78cd067841'; - credentials.CLIENT_SECRET = '228cb6308951f4b6f41ba010c7d7981b2721a493c40c50fd2425132dcaccce59'; + if (credentials.NETWORK == 'testnet') { + credentials.HOST = coinbase.sandbox.host; + credentials.API = coinbase.sandbox.api; + credentials.CLIENT_ID = coinbase.sandbox.client_id; + credentials.CLIENT_SECRET = coinbase.sandbox.client_secret; } else { - credentials.HOST = 'https://coinbase.com'; - credentials.API = 'https://api.coinbase.com'; - credentials.CLIENT_ID = window.coinbase_client_id; - credentials.CLIENT_SECRET = window.coinbase_client_secret; + credentials.HOST = coinbase.production.host; + credentials.API = coinbase.production.api; + credentials.CLIENT_ID = coinbase.production.client_id; + credentials.CLIENT_SECRET = coinbase.production.client_secret; }; }; + var _afterTokenReceived = function(data, cb) { + if (data && data.access_token && data.refresh_token) { + storageService.setCoinbaseToken(credentials.NETWORK, data.access_token, function() { + storageService.setCoinbaseRefreshToken(credentials.NETWORK, data.refresh_token, function() { + return cb(null, data.access_token); + }); + }); + } else { + return cb('Could not get the access token'); + } + }; + + root.getNetwork = function() { + return credentials.NETWORK; + }; + + root.getStoredToken = function(cb) { + storageService.getCoinbaseToken(credentials.NETWORK, function(err, accessToken) { + if (err || !accessToken) return cb(); + return cb(accessToken); + }); + }; + + root.getAvailableCurrency = function() { + var config = configService.getSync().wallet.settings; + // ONLY "USD" + switch(config.alternativeIsoCode) { + default : return 'USD' + }; + }; + + root.parseAmount = function(amount, currency) { + var config = configService.getSync().wallet.settings; + var satToBtc = 1 / 100000000; + var unitToSatoshi = config.unitToSatoshi; + var amountUnitStr; + + // IF 'USD' + if (currency) { + amountUnitStr = $filter('formatFiatAmount')(amount) + ' ' + currency; + } else { + var amountSat = parseInt((amount * unitToSatoshi).toFixed(0)); + amountUnitStr = txFormatService.formatAmountStr(amountSat); + // convert unit to BTC + amount = (amountSat * satToBtc).toFixed(8); + currency = 'BTC'; + } + + return [amount, currency, amountUnitStr]; + }; + root.getOauthCodeUrl = function() { return credentials.HOST + '/oauth/authorize?response_type=code&client_id=' @@ -54,7 +146,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ root.getToken = function(code, cb) { var req = { method: 'POST', - url: credentials.API + '/oauth/token', + url: credentials.HOST + '/oauth/token', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' @@ -71,18 +163,18 @@ 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) {}); - return cb(null, data.data); + storageService.setNextStep('BuyAndSell', 'true', function(err) {}); + _afterTokenReceived(data.data, cb); }, function(data) { $log.error('Coinbase Authorization Access Token: ERROR ' + data.statusText); - return cb(data.data); + return cb(data.data || 'Could not get the access token'); }); }; - root.refreshToken = function(refreshToken, cb) { + var _refreshToken = function(refreshToken, cb) { var req = { method: 'POST', - url: credentials.API + '/oauth/token', + url: credentials.HOST + '/oauth/token', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' @@ -98,10 +190,58 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ $http(req).then(function(data) { $log.info('Coinbase Refresh Access Token: SUCCESS'); - return cb(null, data.data); + _afterTokenReceived(data.data, cb); }, function(data) { $log.error('Coinbase Refresh Access Token: ERROR ' + data.statusText); - return cb(data.data); + return cb(data.data || 'Could not get the access token'); + }); + }; + + var _getMainAccountId = function(accessToken, cb) { + root.getAccounts(accessToken, function(err, a) { + if (err) return cb(err); + var data = a.data; + for (var i = 0; i < data.length; i++) { + if (data[i].primary && data[i].type == 'wallet') { + return cb(null, data[i].id); + } + } + root.logout(function() {}); + return cb('Your primary account should be a WALLET. Set your wallet account as primary and try again'); + }); + }; + + root.init = function(cb) { + if (lodash.isEmpty(credentials.CLIENT_ID)) { + return cb('Coinbase is Disabled'); + } + $log.debug('Trying to initialise Coinbase...'); + + storageService.getCoinbaseToken(credentials.NETWORK, function(err, accessToken) { + if (err || !accessToken) return cb(); + else { + _getMainAccountId(accessToken, function(err, accountId) { + if (err) { + if (err.errors && err.errors[0] && err.errors[0].id == 'expired_token') { + $log.debug('Refresh token'); + storageService.getCoinbaseRefreshToken(credentials.NETWORK, function(err, refreshToken) { + if (err) return cb(err); + _refreshToken(refreshToken, function(err, newToken) { + if (err) return cb(err); + _getMainAccountId(newToken, function(err, accountId) { + if (err) return cb(err); + return cb(null, {accessToken: newToken, accountId: accountId}); + }); + }); + }); + } else { + return cb(err); + } + } else { + return cb(null, {accessToken: accessToken, accountId: accountId}); + } + }); + } }); }; @@ -124,7 +264,7 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ return cb(null, data.data); }, function(data) { $log.error('Coinbase Get Accounts: ERROR ' + data.statusText); - return cb(data.data); + return cb(data.data || 'Could not get the accounts'); }); }; @@ -172,6 +312,17 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ }); }; + root.getAddressTransactions = function(token, accountId, addressId, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/accounts/' + accountId + '/addresses/' + addressId + '/transactions', token)).then(function(data) { + $log.info('Coinbase Address s Transactions: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Address s Transactions: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + root.getTransactions = function(token, accountId, cb) { if (!token) return cb('Invalid Token'); $http(_get('/accounts/' + accountId + '/transactions', token)).then(function(data) { @@ -252,7 +403,8 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ amount: data.amount, currency: data.currency, payment_method: data.payment_method || null, - commit: data.commit || false + commit: data.commit || false, + quote: data.quote || false }; $http(_post('/accounts/' + accountId + '/sells', token, data)).then(function(data) { $log.info('Coinbase Sell Request: SUCCESS'); @@ -278,7 +430,8 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ amount: data.amount, currency: data.currency, payment_method: data.payment_method || null, - commit: false + commit: data.commit || false, + quote: data.quote || false }; $http(_post('/accounts/' + accountId + '/buys', token, data)).then(function(data) { $log.info('Coinbase Buy Request: SUCCESS'); @@ -330,10 +483,13 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ }; // Pending transactions - + root.savePendingTransaction = function(ctx, opts, cb) { - var network = configService.getSync().coinbase.testnet ? 'testnet' : 'livenet'; - storageService.getCoinbaseTxs(network, function(err, oldTxs) { + _savePendingTransaction(ctx, opts, cb); + }; + + var _savePendingTransaction = function(ctx, opts, cb) { + storageService.getCoinbaseTxs(credentials.NETWORK, function(err, oldTxs) { if (lodash.isString(oldTxs)) { oldTxs = JSON.parse(oldTxs); } @@ -350,23 +506,200 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ } tx = JSON.stringify(tx); - storageService.setCoinbaseTxs(network, tx, function(err) { + storageService.setCoinbaseTxs(credentials.NETWORK, tx, function(err) { return cb(err); }); }); }; - root.getPendingTransactions = function(cb) { - var network = configService.getSync().coinbase.testnet ? 'testnet' : 'livenet'; - storageService.getCoinbaseTxs(network, function(err, txs) { - var _txs = txs ? JSON.parse(txs) : {}; - return cb(err, _txs); + root.getPendingTransactions = function(coinbasePendingTransactions) { + storageService.getCoinbaseTxs(credentials.NETWORK, function(err, txs) { + txs = txs ? JSON.parse(txs) : {}; + coinbasePendingTransactions.data = lodash.isEmpty(txs) ? null : txs; + + root.init(function(err, data) { + if (err || lodash.isEmpty(data)) { + if (err) $log.error(err); + return; + } + var accessToken = data.accessToken; + var accountId = data.accountId; + + lodash.forEach(coinbasePendingTransactions.data, function(dataFromStorage, txId) { + if ((dataFromStorage.type == 'sell' && dataFromStorage.status == 'completed') || + (dataFromStorage.type == 'buy' && dataFromStorage.status == 'completed') || + dataFromStorage.status == 'error' || + (dataFromStorage.type == 'send' && dataFromStorage.status == 'completed')) + return; + root.getTransaction(accessToken, accountId, txId, function(err, tx) { + if (err || lodash.isEmpty(tx) || (tx.data && tx.data.error)) { + _savePendingTransaction(dataFromStorage, { + status: 'error', + error: (tx.data && tx.data.error) ? tx.data.error : err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + return; + } + _updateCoinbasePendingTransactions(dataFromStorage, tx.data); + coinbasePendingTransactions.data[txId] = dataFromStorage; + if (tx.data.type == 'send' && tx.data.status == 'completed' && tx.data.from) { + root.sellPrice(accessToken, dataFromStorage.sell_price_currency, function(err, s) { + if (err) { + _savePendingTransaction(dataFromStorage, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + return; + } + var newSellPrice = s.data.amount; + var variance = Math.abs((newSellPrice - dataFromStorage.sell_price_amount) / dataFromStorage.sell_price_amount * 100); + if (variance < dataFromStorage.price_sensitivity.value) { + _sellPending(dataFromStorage, accessToken, accountId, coinbasePendingTransactions); + } else { + var error = { + errors: [{ + message: 'Price falls over the selected percentage' + }] + }; + _savePendingTransaction(dataFromStorage, { + status: 'error', + error: error + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + } + }); + } else if (tx.data.type == 'buy' && tx.data.status == 'completed' && tx.data.buy) { + _sendToWallet(dataFromStorage, accessToken, accountId, coinbasePendingTransactions); + } else { + _savePendingTransaction(dataFromStorage, {}, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + } + }); + }); + }); }); }; - root.logout = function(network, cb) { - storageService.removeCoinbaseToken(network, function() { - storageService.removeCoinbaseRefreshToken(network, function() { + root.updatePendingTransactions = lodash.throttle(function() { + $log.debug('Updating pending transactions...'); + root.setCredentials(); + var pendingTransactions = { data: {} }; + root.getPendingTransactions(pendingTransactions); + }, 20000); + + var _updateTxs = function(coinbasePendingTransactions) { + storageService.getCoinbaseTxs(credentials.NETWORK, function(err, txs) { + txs = txs ? JSON.parse(txs) : {}; + coinbasePendingTransactions.data = lodash.isEmpty(txs) ? null : txs; + }); + }; + + var _sellPending = function(tx, accessToken, accountId, coinbasePendingTransactions) { + var data = tx.amount; + data['payment_method'] = tx.payment_method || null; + data['commit'] = true; + root.sellRequest(accessToken, accountId, data, function(err, res) { + if (err) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + } else { + if (res.data && !res.data.transaction) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + return; + } + _savePendingTransaction(tx, { + remove: true + }, function(err) { + root.getTransaction(accessToken, accountId, res.data.transaction.id, function(err, updatedTx) { + _savePendingTransaction(updatedTx.data, {}, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + }); + }); + } + }); + }; + + var _sendToWallet = function(tx, accessToken, accountId, coinbasePendingTransactions) { + if (!tx) return; + var desc = appConfigService.nameCase + ' Wallet'; + var data = { + to: tx.toAddr, + amount: tx.amount.amount, + currency: tx.amount.currency, + description: desc + }; + root.sendTo(accessToken, accountId, data, function(err, res) { + if (err) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + } else { + if (res.data && !res.data.id) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + return; + } + root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) { + _savePendingTransaction(tx, { + remove: true + }, function(err) { + _savePendingTransaction(sendTx.data, {}, function(err) { + if (err) $log.debug(err); + _updateTxs(coinbasePendingTransactions); + }); + }); + }); + } + }); + }; + + var _updateCoinbasePendingTransactions = function(obj /*, …*/ ) { + for (var i = 1; i < arguments.length; i++) { + for (var prop in arguments[i]) { + var val = arguments[i][prop]; + if (typeof val == "object") + _updateCoinbasePendingTransactions(obj[prop], val); + else + obj[prop] = val ? val : obj[prop]; + } + } + return obj; + }; + + root.logout = function(cb) { + storageService.removeCoinbaseToken(credentials.NETWORK, function() { + storageService.removeCoinbaseRefreshToken(credentials.NETWORK, function() { return cb(); }); }); diff --git a/src/js/services/configService.js b/src/js/services/configService.js index b3396a3ef..88bd1e3bb 100644 --- a/src/js/services/configService.js +++ b/src/js/services/configService.js @@ -49,7 +49,7 @@ angular.module('copayApp.services').factory('configService', function(storageSer }, coinbase: { - enabled: false, //disable coinbase for this release + enabled: true, testnet: false }, @@ -222,10 +222,6 @@ angular.module('copayApp.services').factory('configService', function(storageSer configCache.aliasFor = configCache.aliasFor || {}; configCache.emailFor = configCache.emailFor || {}; - // Coinbase - // Disabled for testnet - configCache.coinbase.testnet = false; - $log.debug('Preferences read:', configCache) lodash.each(root._queue, function(x) { diff --git a/src/js/services/incomingData.js b/src/js/services/incomingData.js index 4b7d4ea34..78e856af1 100644 --- a/src/js/services/incomingData.js +++ b/src/js/services/incomingData.js @@ -126,9 +126,16 @@ angular.module('copayApp.services').factory('incomingData', function($log, $stat url: data }); } else if (data && data.indexOf(appConfigService.name + '://coinbase') === 0) { - return $state.go('uricoinbase', { - url: data + var code = getParameterByName('code', data); + $state.go('tabs.home', {}, { + 'reload': true, + 'notify': $state.current.name == 'tabs.home' ? false : true + }).then(function() { + $state.transitionTo('tabs.buyandsell.coinbase', { + code: code + }); }); + return true; // BitPayCard Authentication } else if (data && data.indexOf(appConfigService.name + '://') === 0) { diff --git a/src/sass/views/amount.scss b/src/sass/views/amount.scss index cc4bf24e3..175e230f4 100644 --- a/src/sass/views/amount.scss +++ b/src/sass/views/amount.scss @@ -80,7 +80,6 @@ padding: 24px 0; font-size: 18px; .title { - float: left; padding-top: 10px; color: $dark-gray; font-weight: bold; @@ -89,6 +88,9 @@ color: $light-gray; font-size: 12px; } + .select { + margin: 10px 1px; + } } } .amount { diff --git a/src/sass/views/integrations/coinbase.scss b/src/sass/views/integrations/coinbase.scss index 0896abffa..2221463f3 100644 --- a/src/sass/views/integrations/coinbase.scss +++ b/src/sass/views/integrations/coinbase.scss @@ -1,21 +1,129 @@ -.coinbase-preferences { - ul { - font-size: 14px; - background: white; - li { - padding: 16px 10px 16px 16px; - border-bottom: 1px solid #E9E9EC; +#coinbase { + $item-lateral-padding: 20px; + $item-vertical-padding: 10px; + $item-border-color: #EFEFEF; + $item-label-color: #6C6C6E; + @extend .deflash-blue; + + .add-bottom-for-cta { + bottom: 92px; + } + .head { + padding: 30px $item-lateral-padding 4rem; + border-top: 0; + + .sending-label { + display: flex; + font-size: 18px; + align-items: center; + margin-bottom: 1.5rem; + + img { + margin-right: 1rem; + height: 35px; + width: 35px; + } + + span { + text-transform: capitalize; + } + } + .amount-label{ + line-height: 30px; + .amount{ + font-size: 38px; + margin-bottom: .5rem; + + > .unit { + font-family: "Roboto-Light"; + } + } + .alternative { + font-size: 16px; + font-family: "Roboto-Light"; + color: #9B9B9B; + } + } + } + .item { + border-color: $item-border-color; + } + .info { + .badge { + border-radius: 0; + padding: .5rem; + } + .item { + color: #4A4A4A; + padding-top: $item-vertical-padding; + padding-bottom: $item-vertical-padding; + padding-left: $item-lateral-padding; + + &:not(.item-icon-right) { + padding-right: $item-lateral-padding; + } + + .label { + font-size: 14px; + color: $item-label-color; + margin-bottom: 8px; + } + + .capitalized { + text-transform: capitalize; + } + + .wallet .big-icon-svg > .bg { + height: 24px; + width: 24px; + padding: 2px; + box-shadow: none; + vertical-align: middle; + } + + .total-amount { + font-weight: bold; + } + + &.single-line { + display: flex; + align-items: center; + padding-top: 17px; + padding-bottom: 17px; + + .label { + margin: 0; + flex-grow: 1; + } + } + } + .item-divider { + padding-top: 1.2rem; + color: $item-label-color; + font-size: 15px; + } + .wallet { + display: flex; + align-items: center; + padding: .2rem 0; + margin-bottom: 5px; + + ~ .bp-arrow-right { + top: 14px; + } + + > i { + padding: 0; + position: static; + + > img { + height: 24px; + width: 24px; + padding: 2px; + margin-right: .7rem; + box-shadow: none; + } + } } } } - -.coinbase-last-transactions-content { - background: #fff; - padding: 0.8rem 1rem; - cursor: pointer; - border-bottom: 1px solid #E4E8EC; -} - -.coinbase-pointer { - cursor: pointer; -} diff --git a/www/views/advancedSettings.html b/www/views/advancedSettings.html index 72651a97e..afbbf764b 100644 --- a/www/views/advancedSettings.html +++ b/www/views/advancedSettings.html @@ -21,12 +21,9 @@ Enable Glidera Service - - - - +