diff --git a/Gruntfile.js b/Gruntfile.js index 188cb81b8..3fd51b784 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,7 +52,7 @@ module.exports = function(grunt) { }, }, css: { - files: ['src/css/*.css'], + files: ['src/sass/*.css', 'src/css/*.css'], tasks: ['concat:css'] }, sass: { diff --git a/public/img/amazon-card.png b/public/img/amazon-card.png new file mode 100644 index 000000000..6c65982ed Binary files /dev/null and b/public/img/amazon-card.png differ diff --git a/public/img/amazon-gift-card.png b/public/img/amazon-gift-card.png new file mode 100644 index 000000000..44d426160 Binary files /dev/null and b/public/img/amazon-gift-card.png differ diff --git a/public/img/amazon-item.png b/public/img/amazon-item.png new file mode 100644 index 000000000..d6fd5be95 Binary files /dev/null and b/public/img/amazon-item.png differ diff --git a/public/views/amazon.html b/public/views/amazon.html new file mode 100644 index 000000000..d8ba2f0d9 --- /dev/null +++ b/public/views/amazon.html @@ -0,0 +1,75 @@ + +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ {{amazon.loading}} +
+
+ +
+ +
+ Amazon Gift Card +
+ +
+ No gift card in your wallet. Buy one now! +
+ +
+

Cards

+
+
+ {{id}} +
+
+ {{item.cardInfo.value.amount | currency : item.cardInfo.value.currencyCode + ' ' : 2}} +
+
+ Error + {{item.date * 1000 | amCalendar}} +
+
+ +
+
+
+ +
+ +
+
diff --git a/public/views/buyAmazon.html b/public/views/buyAmazon.html new file mode 100644 index 000000000..57dfd0323 --- /dev/null +++ b/public/views/buyAmazon.html @@ -0,0 +1,119 @@ +
+
+ + +
+ +
+
+
+
+
+
+
+
+
+ {{buy.loading}} +
+
+ +
+
+ +
+ + {{buy.error}} + +
+ +
+ Amazon Gift Card +
Redeem online
+
+
+ +
+ - + ${{fiat}} + + +
+ +
+ +
+ + + + +
+
+ +
+ Use your Amazon.com Gift Card to shop from a huge selection of books, electronics, music, movies, software, apparel, toys, and more. +
+ +
+ +
+
+
+
+ +
+
+

Gift card ready to redeem!

+ +
+ + {{buy.giftCard.cardInfo.value.amount | currency : buy.giftCard.cardInfo.value.currencyCode + ' ' : 2 }} + +
+ +
+
Claim code
+
{{buy.giftCard.gcClaimCode}}
+
+ +
+ +
+ +
+ To redeem your gift card, follow these steps: +
    +
  1. Visit www.amazon.com/gc.
  2. +
  3. Click Apply to Account and enter the claim code when prompted.
  4. +
  5. Gift card funds will be applied automatically to eligible orders during the checkout process.
  6. +
  7. You must pay for any remaining balance on your order with another payment method.
  8. +
+
+ +
+
BitPay invoice
+
{{buy.giftCard.bitpayInvoiceId}}
+ +
+
+
+ +
+
diff --git a/public/views/includes/sidebar.html b/public/views/includes/sidebar.html index 4b67781da..98e5db930 100644 --- a/public/views/includes/sidebar.html +++ b/public/views/includes/sidebar.html @@ -30,6 +30,13 @@ Buy and Sell +
  • + + +
    + Gift Card +
    +
  • diff --git a/public/views/modals/amazon-card-details.html b/public/views/modals/amazon-card-details.html new file mode 100644 index 000000000..4bb94b945 --- /dev/null +++ b/public/views/modals/amazon-card-details.html @@ -0,0 +1,45 @@ + + + diff --git a/src/js/controllers/amazon.js b/src/js/controllers/amazon.js new file mode 100644 index 000000000..0149c11aa --- /dev/null +++ b/src/js/controllers/amazon.js @@ -0,0 +1,53 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('amazonController', + function($rootScope, $scope, $timeout, $modal, profileService, configService, storageService, amazonService, isChromeApp, animationService, lodash, nodeWebkit) { + + window.ignoreMobilePause = true; + + this.init = function() { + var self = this; + var network = configService.getSync().amazon.testnet ? 'testnet' : 'livenet'; + amazonService.setCredentials(network); + amazonService.getGiftCards(function(err, gcds) { + if (err) { + self.error = err; + return; + } + self.giftCards = gcds; + + }); + }; + + this.openCardModal = function(card) { + $rootScope.modalOpened = true; + var self = this; + var fc = profileService.focusedClient; + var ModalInstanceCtrl = function($scope, $modalInstance) { + $scope.card = card; + + $scope.cancel = lodash.debounce(function() { + $modalInstance.dismiss('cancel'); + }, 0, 1000); + + }; + + var modalInstance = $modal.open({ + templateUrl: 'views/modals/amazon-card-details.html', + windowClass: animationService.modalAnimated.slideRight, + controller: ModalInstanceCtrl, + }); + + var disableCloseModal = $rootScope.$on('closeModal', function() { + modalInstance.dismiss('cancel'); + }); + + modalInstance.result.finally(function() { + $rootScope.modalOpened = false; + disableCloseModal(); + var m = angular.element(document.getElementsByClassName('reveal-modal')); + m.addClass(animationService.modalAnimated.slideOutRight); + }); + }; + + }); diff --git a/src/js/controllers/buyAmazon.js b/src/js/controllers/buyAmazon.js new file mode 100644 index 000000000..6d49eb5d1 --- /dev/null +++ b/src/js/controllers/buyAmazon.js @@ -0,0 +1,239 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('buyAmazonController', + function($rootScope, $scope, $modal, $log, $timeout, lodash, profileService, animationService, bwsError, configService, walletService, fingerprintService, amazonService) { + + window.ignoreMobilePause = true; + var self = this; + var fc; + var minimumAmount = 1; + var stepAmount = 1; + var multiplierAmount = 2; + var maximumAmount = 10; + + var otherWallets = function(network) { + return lodash.filter(profileService.getWallets(network), function(w) { + return w.network == network && w.m == 1; + }); + }; + + var handleEncryptedWallet = function(client, cb) { + if (!walletService.isEncrypted(client)) return cb(); + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); + }; + + this.init = function() { + $scope.fiat = minimumAmount * multiplierAmount; + var network = configService.getSync().amazon.testnet ? 'testnet' : 'livenet'; + amazonService.setCredentials(network); + self.otherWallets = otherWallets(network); + // Choose focused wallet + try { + var currentWalletId = profileService.focusedClient.credentials.walletId; + lodash.find(self.otherWallets, function(w) { + if (w.id == currentWalletId) { + $timeout(function() { + self.selectedWalletId = w.id; + self.selectedWalletName = w.name; + fc = profileService.getClient(w.id); + $scope.$apply(); + }, 100); + } + }); + } catch (e) { + $log.debug(e); + }; + }; + + $scope.openWalletsModal = function(wallets) { + self.error = null; + var ModalInstanceCtrl = function($scope, $modalInstance) { + $scope.type = 'SELL'; + $scope.wallets = wallets; + $scope.noColor = true; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + + $scope.selectWallet = function(walletId, walletName) { + if (!profileService.getClient(walletId).isComplete()) { + self.error = bwsError.msg('WALLET_NOT_COMPLETE'); + $modalInstance.dismiss('cancel'); + return; + } + $modalInstance.close({ + 'walletId': walletId, + 'walletName': walletName, + }); + }; + }; + + var modalInstance = $modal.open({ + templateUrl: 'views/modals/wallets.html', + windowClass: animationService.modalAnimated.slideUp, + controller: ModalInstanceCtrl, + }); + + modalInstance.result.finally(function() { + var m = angular.element(document.getElementsByClassName('reveal-modal')); + m.addClass(animationService.modalAnimated.slideOutDown); + }); + + modalInstance.result.then(function(obj) { + $timeout(function() { + self.selectedWalletId = obj.walletId; + self.selectedWalletName = obj.walletName; + fc = profileService.getClient(obj.walletId); + $scope.$apply(); + }, 100); + }); + }; + + this.setAmount = function(plus) { + if (plus && $scope.fiat < maximumAmount ) { + stepAmount = stepAmount + 1; + $scope.fiat = stepAmount * multiplierAmount; + } else if (!plus && $scope.fiat > minimumAmount * multiplierAmount) { + stepAmount = stepAmount - 1; + $scope.fiat = stepAmount * multiplierAmount; + } + }; + + this.createTx = function() { + self.error = null; + + var dataSrc = { + price: $scope.fiat, + currency: 'USD' + }; + var outputs = []; + var config = configService.getSync(); + var configWallet = config.wallet; + var walletSettings = configWallet.settings; + + + self.loading = 'Creating invoice...'; + $timeout(function() { + + amazonService.createBitPayInvoice(dataSrc, function(err, data) { + if (err) { + self.loading = null; + self.error = err; + return; + } + + var address, comment, amount; + + address = data.data.bitcoinAddress; + amount = parseInt((data.data.btcPrice * 100000000).toFixed(0)); + comment = 'Buy Amazon Gift Card'; + + outputs.push({ + 'toAddress': address, + 'amount': amount, + 'message': comment + }); + + var txp = { + toAddress: address, + amount: amount, + outputs: outputs, + message: comment, + payProUrl: null, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: walletSettings.feeLevel || 'normal' + }; + + self.loading = 'Creating transaction...'; + walletService.createTx(fc, txp, function(err, createdTxp) { + self.loading = null; + if (err) { + self.loading = null; + $log.debug(err); + self.error = bwsError.msg(err); + $scope.$apply(); + return; + } + $scope.$emit('Local/NeedsConfirmation', createdTxp, function(accept) { + if (accept) { + self.loading = 'Sending bitcoin...'; + self.confirmTx(createdTxp, function(err, tx) { + if (err) { + self.loading = null; + self.error = err; + return; + } + var gift = { + amount: dataSrc.price, + currencyCode: dataSrc.currency, + bitpayInvoiceId: data.data.id + }; + self.loading = 'Buying gift card...'; + amazonService.buyGiftCard(gift, function(err, giftCard) { + self.loading = null; + if (err) { + self.error = err; + return; + } + self.giftCard = giftCard; + }); + }); + } + }); + }); + }); + }, 100); + }; + + this.confirmTx = function(txp, cb) { + + fingerprintService.check(fc, function(err) { + if (err) { + $log.debug(err); + return cb(err); + } + + handleEncryptedWallet(fc, function(err) { + if (err) { + $log.debug(err); + return bwsError.cb(err, null, cb); + } + + walletService.publishTx(fc, txp, function(err, publishedTxp) { + if (err) { + $log.debug(err); + return bwsError.cb(err, null, cb); + } + + walletService.signTx(fc, publishedTxp, function(err, signedTxp) { + walletService.lock(fc); + if (err) { + $log.debug(err); + walletService.removeTx(fc, signedTxp, function(err) { + if (err) $log.debug(err); + }); + return bwsError.cb(err, null, cb); + } + + walletService.broadcastTx(fc, signedTxp, function(err, broadcastedTxp) { + if (err) { + $log.debug(err); + walletService.removeTx(fc, broadcastedTxp, function(err) { + if (err) $log.debug(err); + }); + return bwsError.cb(err, null, cb); + } + $timeout(function() { + return cb(null, broadcastedTxp); + }, 5000); + }); + }); + }); + }); + }); + }; + + }); diff --git a/src/js/controllers/index.js b/src/js/controllers/index.js index 72de95860..a8bf7b0b1 100644 --- a/src/js/controllers/index.js +++ b/src/js/controllers/index.js @@ -160,6 +160,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.initGlidera(); self.initCoinbase(); + self.initAmazon(); self.hideBalance(); @@ -1040,6 +1041,10 @@ angular.module('copayApp.controllers').controller('indexController', function($r }); }; + self.initAmazon = function() { + self.amazonEnabled = configService.getSync().amazon.enabled; + }; + self.initGlidera = function(accessToken) { self.glideraEnabled = configService.getSync().glidera.enabled; self.glideraTestnet = configService.getSync().glidera.testnet; diff --git a/src/js/routes.js b/src/js/routes.js index c2e97747f..61db44513 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -315,6 +315,26 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr }, } }) + .state('amazon', { + url: '/amazon', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/amazon.html' + }, + } + }) + .state('buyAmazon', { + url: '/buyamazon', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/buyAmazon.html' + }, + } + }) .state('preferencesAdvanced', { url: '/preferencesAdvanced', templateUrl: 'views/preferencesAdvanced.html', diff --git a/src/js/services/amazonService.js b/src/js/services/amazonService.js new file mode 100644 index 000000000..c7dc3ef6f --- /dev/null +++ b/src/js/services/amazonService.js @@ -0,0 +1,123 @@ +'use strict'; + +angular.module('copayApp.services').factory('amazonService', function($http, $log, isCordova, lodash, storageService, configService) { + var root = {}; + var credentials = {}; + + var fakeData = { + "cardInfo": { + "cardNumber":null, + "cardStatus":"RefundedToPurchaser", + "expirationDate":null, + "value":{ + "amount":1.0, + "currencyCode":"USD" + } + }, + "creationRequestId":"AwssbTSpecTest001", + "gcClaimCode":"Z7NV-LBBG39-75MU", + "gcExpirationDate":null, + "gcId":"A2GCN9BRX5QS76", + "status":"SUCCESS", + "bitpayInvoiceId":"NJtevvEponHbQVmYoL7FYp" + }; + + root.setCredentials = function(network) { + + if (network == 'testnet') { + credentials.BITPAY_API = 'https://test.bitpay.com'; + credentials.BITPAY_API_TOKEN = 'GDtYwBqbMZvjz5JrYZ1d2ba96StV92U4Yg4AGhT3C4He'; + credentials.AMAZON_HOST = 'https://agcod-v2-gamma.amazon.com'; + } + else { + credentials.BITPAY_API = 'https://bitpay.com'; + credentials.BITPAY_API_TOKEN = window.bitpay_token; + credentials.AMAZON_HOST = 'https://agcod-v2.amazon.com'; + }; + }; + + var _getBitPay = function(endpoint, token) { + return { + method: 'GET', + url: credentials.BITPAY_API + endpoint, + headers: { + 'content-type': 'application/json' + } + }; + }; + + var _postBitPay = function(endpoint, data) { + data.token = credentials.BITPAY_API_TOKEN; + return { + method: 'POST', + url: credentials.BITPAY_API + endpoint, + headers: { + 'content-type': 'application/json' + }, + data: data + }; + }; + + root.createBitPayInvoice = function(data, cb) { + var data = { + price: data.price, + currency: data.currency + }; + $http(_postBitPay('/invoices', data)).then(function(data) { + $log.info('BitPay Create Invoice: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('BitPay Create Invoice: ERROR ' + data.statusText); + return cb(data); + }); + }; + + root.saveGiftCard = function(gc, opts, cb) { + var network = configService.getSync().amazon.testnet ? 'testnet' : 'livenet'; + storageService.getAmazonGiftCards(network, function(err, oldGiftCards) { + if (lodash.isString(oldGiftCards)) { + oldGiftCards = JSON.parse(oldGiftCards); + } + if (lodash.isString(gc)) { + gc = JSON.parse(gc); + } + var inv = oldGiftCards || {}; + inv[gc.gcId] = gc; + if (opts && (opts.error || opts.status)) { + inv[gc.gcId] = lodash.assign(inv[gc.gcId], opts); + } + if (opts && opts.remove) { + delete(inv[gc.gcId]); + } + inv = JSON.stringify(inv); + + storageService.setAmazonGiftCards(network, inv, function(err) { + return cb(err); + }); + }); + }; + + root.getGiftCards = function(cb) { + var network = configService.getSync().amazon.testnet ? 'testnet' : 'livenet'; + storageService.getAmazonGiftCards(network, function(err, giftCards) { + var _gcds = giftCards ? JSON.parse(giftCards) : null; + return cb(err, _gcds); + }); + }; + + root.buyGiftCard = function(gift, cb) { + var newId = Math.floor(Date.now() / 1000); + var saveData = fakeData; + saveData.gcId = saveData.gcId + '' + newId; + saveData.cardInfo.value.amount = gift.amount; + saveData.cardInfo.value.currencyCode = gift.currencyCode; + saveData['bitpayInvoiceId'] = gift.bitpayInvoiceId; + saveData['date'] = newId; + root.saveGiftCard(saveData, null, function(err) { + return cb(null, fakeData); + }); + }; + + return root; + +}); diff --git a/src/js/services/configService.js b/src/js/services/configService.js index a552b58fe..c956d2945 100644 --- a/src/js/services/configService.js +++ b/src/js/services/configService.js @@ -43,6 +43,11 @@ angular.module('copayApp.services').factory('configService', function(storageSer testnet: false }, + amazon: { + enabled: true, + testnet: false + }, + rates: { url: 'https://insight.bitpay.com:443/api/rates', }, @@ -101,6 +106,9 @@ angular.module('copayApp.services').factory('configService', function(storageSer if (!configCache.coinbase) { configCache.coinbase = defaultConfig.coinbase; } + if (!configCache.amazon) { + configCache.amazon = defaultConfig.amazon; + } if (!configCache.pushNotifications) { configCache.pushNotifications = defaultConfig.pushNotifications; } @@ -117,6 +125,10 @@ angular.module('copayApp.services').factory('configService', function(storageSer // Disabled for testnet configCache.coinbase.testnet = false; + // Amazon + // Disabled for testnet + configCache.amazon.testnet = true; + $log.debug('Preferences read:', configCache) return cb(err, configCache); }); diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 33eaa819c..2f67fd282 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -317,6 +317,18 @@ angular.module('copayApp.services') }); }); }; + + root.setAmazonGiftCards = function(network, gcs, cb) { + storage.set('amazonGiftCards-' + network, gcs, cb); + }; + + root.getAmazonGiftCards = function(network, cb) { + storage.get('amazonGiftCards-' + network, cb); + }; + + root.removeAmazonGiftCards = function(network, cb) { + storage.remove('amazonGiftCards-' + network, cb); + }; return root; }); diff --git a/src/sass/amazon.scss b/src/sass/amazon.scss new file mode 100644 index 000000000..bcbae297c --- /dev/null +++ b/src/sass/amazon.scss @@ -0,0 +1,22 @@ + +.amazon-select-amount { + display: inline-block; + padding: 3px 15px; + background-color: #4B6178; + color: #ffffff; + border-radius: 5px; + margin: 0 5px; + &:hover { + color: #ffffff; + } +} + +.amazon-amount { + display: inline-block; + background-color: #E4E8EC; + padding: 3px 10px; + width: 60px; + text-align: center; + border: 1px solid #ccc; + border-radius: 5px; +} diff --git a/src/sass/main.scss b/src/sass/main.scss index 985a219c3..02bcdbd34 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -99,7 +99,7 @@ h4.title a { } } -.modal-content h4, .glidera h4, .coinbase h4 { +.modal-content h4, .glidera h4, .coinbase h4, .amazon h4 { background: #F6F7F9; padding: 25px 0px 5px 10px; text-transform: uppercase;