From af932b3e59f5a33414a92e6af1cb4e2bbe8997d3 Mon Sep 17 00:00:00 2001 From: Gustavo Maximiliano Cortez Date: Thu, 8 Dec 2016 11:04:07 -0300 Subject: [PATCH] Coinbase: first step integration, connect account and main view --- src/js/controllers/coinbase.js | 148 ++++++++------ src/js/routes.js | 40 ++-- src/js/services/coinbaseService.js | 300 +++++++++++++++++++++++++---- src/js/services/configService.js | 6 +- www/views/advancedSettings.html | 7 +- www/views/buyandsell.html | 4 + www/views/coinbase.html | 225 +++++++++------------- www/views/tab-home.html | 10 +- 8 files changed, 481 insertions(+), 259 deletions(-) diff --git a/src/js/controllers/coinbase.js b/src/js/controllers/coinbase.js index 76b93c9c7..ef2ef3749 100644 --- a/src/js/controllers/coinbase.js +++ b/src/js/controllers/coinbase.js @@ -1,25 +1,53 @@ '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($rootScope, $scope, $timeout, $ionicModal, $log, profileService, configService, storageService, coinbaseService, lodash, platformInfo, ongoingProcess, popupService, gettextCatalog, externalLinkService) { - var isNW = platformInfo.isNW; + var isNW = platformInfo.isNW; - if (platformInfo.isCordova && StatusBar.isVisible) { - StatusBar.backgroundColorByHexString("#4B6178"); - } + var init = function() { + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init($scope.accessToken, function(err, data) { +console.log('[coinbase.js:9]',data); //TODO) + ongoingProcess.set('connectingCoinbase', false); + if (err || lodash.isEmpty(data)) { + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err); + } + return; + } + // Updating accessToken and accountId + $timeout(function() { + $scope.accessToken = data.accessToken; + $scope.accountId = data.accountId; + $scope.updateTransactions(); + $scope.$apply(); + }, 100); + }); + }; - 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' - }); + $scope.updateTransactions = function() { + $log.debug('Checking for transactions...'); + coinbaseService.getPendingTransactions($scope.accessToken, $scope.accountId, function(err, txs) { +console.log('[coinbase.js:43]',txs); //TODO) + $scope.pendingTransactions = txs; + }); + + }; + + this.openAuthenticateWindow = function() { + var oauthUrl = this.getAuthenticateUrl(); + externalLinkService.open(oauthUrl); + /* + * Not working (NW bug) + if (!isNW) { + externalLinkService.open(oauthUrl); + } else { + var self = this; + var gui = require('nw.gui'); + gui.Window.open(oauthUrl, { + focus: true, + position: 'center' + }, function(win) { win.on('loaded', function() { var title = win.title; if (title.indexOf('Coinbase') == -1) { @@ -28,51 +56,47 @@ angular.module('copayApp.controllers').controller('coinbaseController', 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); - 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); - }); - }); - } - }); - }, 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(); }); - }; + } + */ + } + this.getAuthenticateUrl = function() { + 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(gettextCatalog.getString('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.coinbaseTxDetailsModal = modal; + $scope.coinbaseTxDetailsModal.show(); + }); + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + coinbaseService.setCredentials(); + $scope.network = coinbaseService.getEnvironment(); + init(); }); +}); diff --git a/src/js/routes.js b/src/js/routes.js index f9a02560b..786d50f17 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -926,21 +926,39 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr * */ - .state('coinbase', { + .state('tabs.buyandsell.coinbase', { url: '/coinbase', - templateUrl: 'views/coinbase.html' + views: { + 'tab-home@tabs': { + controller: 'coinbaseController', + controllerAs: 'coinbase', + templateUrl: 'views/coinbase.html' + } + } }) - .state('preferencesCoinbase', { - url: '/preferencesCoinbase', - templateUrl: 'views/preferencesCoinbase.html' + .state('tabs.buyandsell.coinbase.preferences', { + url: '/preferences', + 'tab-home@tabs': { + controller: 'preferencesCoinbaseController', + controllerAs: 'coinbase', + templateUrl: 'views/preferencesCoinbase.html' + } }) - .state('buyCoinbase', { - url: '/buycoinbase', - templateUrl: 'views/buyCoinbase.html' + .state('tabs.buyandsell.coinbase.buy', { + url: '/buy', + 'tab-home@tabs': { + controller: 'buyCoinbaseController', + controllerAs: 'buy', + templateUrl: 'views/buyCoinbase.html' + } }) - .state('sellCoinbase', { - url: '/sellcoinbase', - templateUrl: 'views/sellCoinbase.html' + .state('tabs.buyandsell.coinbase.sell', { + url: '/sell', + 'tab-home@tabs': { + controller: 'sellCoinbaseController', + controllerAs: 'sell', + templateUrl: 'views/sellCoinbase.html' + } }) /* diff --git a/src/js/services/coinbaseService.js b/src/js/services/coinbaseService.js index ce5b984cd..4acb37c60 100644 --- a/src/js/services/coinbaseService.js +++ b/src/js/services/coinbaseService.js @@ -1,11 +1,26 @@ '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, platformInfo, lodash, storageService, configService) { var root = {}; var credentials = {}; var isCordova = platformInfo.isCordova; + var isNW = platformInfo.isNW; - root.setCredentials = function(network) { + 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,27 +35,45 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ + 'wallet:transactions:send,' + 'wallet:payment-methods:read'; - if (isCordova) { - credentials.REDIRECT_URI = 'copay://coinbase'; + // NW has a bug with Window Object + if (isCordova && isNW) { + 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.getEnvironment = function() { + return credentials.NETWORK; + }; + root.getOauthCodeUrl = function() { + // TODO CHANGE LIMIT BACK TO 1000 ************************************************* return credentials.HOST + '/oauth/authorize?response_type=code&client_id=' + credentials.CLIENT_ID @@ -48,13 +81,13 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ + credentials.REDIRECT_URI + '&state=SECURE_RANDOM&scope=' + credentials.SCOPE - + '&meta[send_limit_amount]=1000&meta[send_limit_currency]=USD&meta[send_limit_period]=day'; + + '&meta[send_limit_amount]=1&meta[send_limit_currency]=USD&meta[send_limit_period]=day'; }; 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 +104,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 +131,63 @@ 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); + } + } + coinbaseService.logout(function() {}); + return cb('Your primary account should be a WALLET. Set your wallet account as primary and try again'); + }); + }; + + root.init = function(accessToken, cb) { + if (lodash.isEmpty(credentials.CLIENT_ID)) { + return cb('Coinbase is Disabled'); + } + $log.debug('Init Token...'); + + var getToken = function(cb) { + if (accessToken) { + cb(null, accessToken); + } else { + storageService.getCoinbaseToken(credentials.NETWORK, cb); + } + }; + + getToken(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); + return cb(null, {accessToken: newToken, accountId: accountId}); + }); + }); + } else { + return cb(err); + } + } else { + return cb(null, {accessToken: accessToken, accountId: accountId}); + } + }); + } }); }; @@ -124,7 +210,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'); }); }; @@ -331,9 +417,8 @@ 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) { + var _savePendingTransaction = function(ctx, opts, cb) { + storageService.getCoinbaseTxs(credentials.NETWORK, function(err, oldTxs) { if (lodash.isString(oldTxs)) { oldTxs = JSON.parse(oldTxs); } @@ -350,23 +435,166 @@ 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(accessToken, accountId, cb) { + var coinbasePendingTransactions; + storageService.getCoinbaseTxs(credentials.NETWORK, function(err, txs) { + txs = txs ? JSON.parse(txs) : {}; + coinbasePendingTransactions = lodash.isEmpty(txs) ? null : txs; + lodash.forEach(txs, 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) { + _savePendingTransaction(dataFromStorage, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + _updateCoinbasePendingTransactions(dataFromStorage, tx.data); + coinbasePendingTransactions[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); + }); + return cb(); + } + 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(tx.data, accessToken, accountId); + } else { + var error = { + errors: [{ + message: 'Price falls over the selected percentage' + }] + }; + _savePendingTransaction(dataFromStorage, { + status: 'error', + error: error + }, function(err) { + if (err) $log.debug(err); + }); + } + }); + } else if (tx.data.type == 'buy' && tx.data.status == 'completed' && tx.data.buy) { + _sendToCopay(dataFromStorage, accessToken, accountId); + } else { + _savePendingTransaction(dataFromStorage, {}, function(err) { + if (err) $log.debug(err); + }); + } + return cb(null, coinbasePendingTransactions); + }); + }); }); }; - root.logout = function(network, cb) { - storageService.removeCoinbaseToken(network, function() { - storageService.removeCoinbaseRefreshToken(network, function() { + var _sellPending = function(tx, accessToken, accountId) { + if (!tx) return; + var data = tx.amount; + 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); + }); + } else { + if (!res.data.transaction) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + 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); + }); + }); + }); + } + }); + }; + + var _sendToCopay = function(tx, accessToken, accountId) { + if (!tx) return; + var data = { + to: tx.toAddr, + amount: tx.amount.amount, + currency: tx.amount.currency, + description: 'To Copay Wallet' + }; + root.sendTo(accessToken, accountId, data, function(err, res) { + if (err) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + } else { + if (!res.data.id) { + _savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + root.getTransaction(accessToken, accountId, res.data.id, function(err, sendTx) { + _savePendingTransaction(tx, { + remove: true + }, function(err) { + _savePendingTransaction(sendTx.data, {}, function(err) { + // TODO + }); + }); + }); + } + }); + }; + + 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 5bd6c3cc6..83783c98c 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 }, @@ -230,10 +230,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/www/views/advancedSettings.html b/www/views/advancedSettings.html index f6ee8f328..7623121e7 100644 --- a/www/views/advancedSettings.html +++ b/www/views/advancedSettings.html @@ -21,12 +21,9 @@ Enable Glidera Service - - - - +
Wallet Operation
diff --git a/www/views/buyandsell.html b/www/views/buyandsell.html index 4f7dd4b10..47e361365 100644 --- a/www/views/buyandsell.html +++ b/www/views/buyandsell.html @@ -6,6 +6,10 @@ + + + + diff --git a/www/views/coinbase.html b/www/views/coinbase.html index 9e4457961..c1ac86f3a 100644 --- a/www/views/coinbase.html +++ b/www/views/coinbase.html @@ -1,59 +1,30 @@ - -
- -
- -
-
-
-
    -
  • -
-
-
-
- Your primary account should be a WALLET. Set your wallet account as primary and try again. -
-
-
- -
- Or go to Preferences and log out manually. -
-
-
+ + -
-
- + + +
Testnet wallets only work with Coinbase Sandbox Accounts
-
+ +
+
+
    +
  • +
+
+
+ +
@@ -61,111 +32,95 @@

Connect your Coinbase account to get started

- Connect to Coinbase - - -
+
- - - -
- -
-
-
- -
- -
- -
- -
    -
  • - buy bitcoin - Buy Bitcoin - - - -
  • -
  • - sell bitcoin - Sell Bitcoin - - - -
  • -
- -
-

Activity

-
-
    -
  • -
-
-
-
- bought - bought - sold - sold -
- -
-
- Sold - Bought - - -{{tx.amount.amount.replace('-','')}} - {{tx.amount.currency}} - +
+
-
-
-
+ + +
+
+ +
- -
-
+ + diff --git a/www/views/tab-home.html b/www/views/tab-home.html index a0b17d7e6..04bedc98d 100644 --- a/www/views/tab-home.html +++ b/www/views/tab-home.html @@ -124,10 +124,10 @@ - - + + + +
@@ -156,7 +156,7 @@ Add BitPay Visa® Card - +