diff --git a/src/js/controllers/buyMercadoLibre.js b/src/js/controllers/buyMercadoLibre.js new file mode 100644 index 000000000..fa01a6143 --- /dev/null +++ b/src/js/controllers/buyMercadoLibre.js @@ -0,0 +1,352 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('buyMercadoLibreController', function($scope, $log, $state, $timeout, $filter, $ionicHistory, $ionicConfig, lodash, mercadoLibreService, popupService, profileService, ongoingProcess, configService, walletService, payproService, bwcError, externalLinkService, platformInfo, txFormatService, gettextCatalog) { + + var amount; + var currency; + var createdTx; + var message; + var invoiceId; + var configWallet = configService.getSync().wallet; + $scope.isCordova = platformInfo.isCordova; + + $scope.openExternalLink = function(url) { + externalLinkService.open(url); + }; + + var _resetValues = function() { + $scope.totalAmountStr = $scope.amount = $scope.invoiceFee = $scope.networkFee = $scope.totalAmount = $scope.wallet = null; + createdTx = message = invoiceId = null; + }; + + var showErrorAndBack = function(title, msg) { + title = title || gettextCatalog.getString('Error'); + $scope.sendStatus = ''; + $log.error(msg); + msg = (msg && msg.errors) ? msg.errors[0].message : msg; + popupService.showAlert(title, msg, function() { + $ionicHistory.goBack(); + }); + }; + + var showError = function(title, msg, cb) { + cb = cb || function() {}; + title = title || gettextCatalog.getString('Error'); + $scope.sendStatus = ''; + $log.error(msg); + msg = (msg && msg.errors) ? msg.errors[0].message : msg; + popupService.showAlert(title, msg, cb); + }; + + 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); + } + + walletService.publishAndSign(wallet, txp, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }, onSendStatusChange); + }; + + var statusChangeHandler = function(processName, showName, isOn) { + $log.debug('statusChangeHandler: ', processName, showName, isOn); + if (processName == 'buyingGiftCard' && !isOn) { + $scope.sendStatus = 'success'; + $timeout(function() { + $scope.$digest(); + }, 100); + } else if (showName) { + $scope.sendStatus = showName; + } + }; + + var satToFiat = function(sat, cb) { + txFormatService.toFiat(sat, $scope.currencyIsoCode, function(value) { + return cb(value); + }); + }; + + var setTotalAmount = function(amountSat, invoiceFeeSat, networkFeeSat) { + satToFiat(amountSat, function(a) { + $scope.amount = Number(a); + + satToFiat(invoiceFeeSat, function(i) { + $scope.invoiceFee = Number(i); + + satToFiat(networkFeeSat, function(n) { + $scope.networkFee = Number(n); + $scope.totalAmount = $scope.amount + $scope.invoiceFee + $scope.networkFee; + $timeout(function() { + $scope.$digest(); + }); + }); + }); + }); + }; + + var createInvoice = function(data, cb) { + mercadoLibreService.createBitPayInvoice(data, function(err, dataInvoice) { + if (err) { + var err_title = gettextCatalog.getString('Error creating the invoice'); + var err_msg; + if (err && err.message && err.message.match(/suspended/i)) { + err_title = gettextCatalog.getString('Service not available'); + err_msg = gettextCatalog.getString('Mercadolibre Gift Card Service is not available at this moment. Please try back later.'); + } else if (err && err.message) { + err_msg = err.message; + } else { + err_msg = gettextCatalog.getString('Could not access Gift Card Service'); + }; + + return cb({ + title: err_title, + message: err_msg + }); + } + + var accessKey = dataInvoice ? dataInvoice.accessKey : null; + + if (!accessKey) { + return cb({ + message: gettextCatalog.getString('No access key defined') + }); + } + + mercadoLibreService.getBitPayInvoice(dataInvoice.invoiceId, function(err, invoice) { + if (err) { + return cb({ + message: gettextCatalog.getString('Could not get the invoice') + }); + } + + return cb(null, invoice, accessKey); + }); + }); + }; + + var createTx = function(wallet, invoice, message, cb) { + var payProUrl = (invoice && invoice.paymentUrls) ? invoice.paymentUrls.BIP73 : null; + + if (!payProUrl) { + return cb({ + title: gettextCatalog.getString('Error in Payment Protocol'), + message: gettextCatalog.getString('Invalid URL') + }); + } + + var outputs = []; + var toAddress = invoice.bitcoinAddress; + var amountSat = parseInt((invoice.btcDue * 100000000).toFixed(0)); // BTC to Satoshi + + outputs.push({ + 'toAddress': toAddress, + 'amount': amountSat, + 'message': message + }); + + var txp = { + toAddress: toAddress, + amount: amountSat, + outputs: outputs, + message: message, + payProUrl: payProUrl, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: configWallet.settings.feeLevel || 'normal' + }; + + walletService.createTx(wallet, txp, function(err, ctxp) { + if (err) { + return cb({ + title: gettextCatalog.getString('Could not create transaction'), + message: bwcError.msg(err) + }); + } + return cb(null, ctxp); + }); + }; + + var checkTransaction = lodash.throttle(function(count, dataSrc) { + mercadoLibreService.createGiftCard(dataSrc, function(err, giftCard) { + $log.debug("creating gift card " + count); + if (err) { + $scope.sendStatus = ''; + ongoingProcess.set('buyingGiftCard', false, statusChangeHandler); + giftCard = {}; + giftCard.status = 'FAILURE'; + } + + if (giftCard && giftCard.cardStatus && (giftCard.cardStatus != 'active' && giftCard.cardStatus != 'inactive' && giftCard.cardStatus != 'expired')) { + $scope.sendStatus = ''; + ongoingProcess.set('buyingGiftCard', false, statusChangeHandler); + giftCard = {}; + giftCard.status = 'FAILURE'; + } + + + if (giftCard.status == 'PENDING' && count < 3) { + $log.debug("Waiting for payment confirmation"); + checkTransaction(count + 1, dataSrc); + return; + } + + var now = moment().unix() * 1000; + + var newData = giftCard; + newData['invoiceId'] = dataSrc.invoiceId; + newData['accessKey'] = dataSrc.accessKey; + newData['invoiceUrl'] = dataSrc.invoiceUrl; + newData['amount'] = dataSrc.amount; + newData['currency'] = dataSrc.currency; + newData['date'] = dataSrc.invoiceTime || now; + newData['uuid'] = dataSrc.uuid; + + mercadoLibreService.savePendingGiftCard(newData, null, function(err) { + ongoingProcess.set('buyingGiftCard', false, statusChangeHandler); + $log.debug("Saving new gift card with status: " + newData.status); + $scope.mlGiftCard = newData; + }); + }); + }, 8000, { + 'leading': true + }); + + var initialize = function(wallet) { + var parsedAmount = txFormatService.parseAmount(amount, currency); + $scope.currencyIsoCode = parsedAmount.currency; + $scope.amountUnitStr = parsedAmount.amountUnitStr; + var dataSrc = { + amount: parsedAmount.amount, + currency: parsedAmount.currency, + uuid: wallet.id + }; + ongoingProcess.set('loadingTxInfo', true); + createInvoice(dataSrc, function(err, invoice, accessKey) { + if (err) { + ongoingProcess.set('loadingTxInfo', false); + showErrorAndBack(err.title, err.message); + return; + } + // Sometimes API does not return this element; + invoice['buyerPaidBtcMinerFee'] = invoice.buyerPaidBtcMinerFee || 0; + var invoiceFeeSat = (invoice.buyerPaidBtcMinerFee * 100000000).toFixed(); + + message = gettextCatalog.getString("{{amountStr}} for Mercado Livre Brazil Gift Card", { + amountStr: $scope.amountUnitStr + }); + + createTx(wallet, invoice, message, function(err, ctxp) { + ongoingProcess.set('loadingTxInfo', false); + if (err) { + _resetValues(); + showError(err.title, err.message); + return; + } + + // Save in memory + createdTx = ctxp; + invoiceId = invoice.id; + + createdTx['giftData'] = { + currency: dataSrc.currency, + amount: dataSrc.amount, + uuid: dataSrc.uuid, + accessKey: accessKey, + invoiceId: invoice.id, + invoiceUrl: invoice.url, + invoiceTime: invoice.invoiceTime + }; + $scope.totalAmountStr = txFormatService.formatAmountStr(ctxp.amount); + setTotalAmount(parsedAmount.amountSat, invoiceFeeSat, ctxp.fee); + }); + }); + }; + + $scope.$on("$ionicView.beforeLeave", function(event, data) { + $ionicConfig.views.swipeBackEnabled(true); + }); + + $scope.$on("$ionicView.enter", function(event, data) { + $ionicConfig.views.swipeBackEnabled(false); + }); + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + amount = data.stateParams.amount; + currency = data.stateParams.currency; + + if (amount > 2000 || amount < 50) { + showErrorAndBack(null, gettextCatalog.getString('Purchase amount must be a value between 50 and 2000')); + return; + } + + $scope.network = mercadoLibreService.getNetwork(); + $scope.wallets = profileService.getWallets({ + onlyComplete: true, + network: $scope.network + }); + if (lodash.isEmpty($scope.wallets)) { + showErrorAndBack(null, gettextCatalog.getString('No wallets available')); + return; + } + $scope.onWalletSelect($scope.wallets[0]); // Default first wallet + }); + + $scope.buyConfirm = function() { + + if (!createdTx) { + showError(null, gettextCatalog.getString('Transaction has not been created')); + return; + } + + var title = gettextCatalog.getString('Confirm'); + var okText = gettextCatalog.getString('Ok'); + var cancelText = gettextCatalog.getString('Cancel'); + popupService.showConfirm(title, message, okText, cancelText, function(ok) { + if (!ok) { + $scope.sendStatus = ''; + return; + } + + ongoingProcess.set('buyingGiftCard', true, statusChangeHandler); + publishAndSign($scope.wallet, createdTx, function() {}, function(err, txSent) { + if (err) { + ongoingProcess.set('buyingGiftCard', false, statusChangeHandler); + showError(gettextCatalog.getString('Could not send transaction'), err); + return; + } + checkTransaction(1, createdTx.giftData); + }); + }); + }; + + $scope.showWalletSelector = function() { + $scope.walletSelectorTitle = 'Buy from'; + $scope.showWallets = true; + }; + + $scope.onWalletSelect = function(wallet) { + $scope.wallet = wallet; + initialize(wallet); + }; + + $scope.goBackHome = function() { + $scope.sendStatus = ''; + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.home').then(function() { + $ionicHistory.nextViewOptions({ + disableAnimate: true + }); + $state.transitionTo('tabs.giftcards.mercadoLibre').then(function() { + $state.transitionTo('tabs.giftcards.mercadoLibre.cards', { + invoiceId: invoiceId + }); + }); + }); + }; +}); diff --git a/src/js/controllers/mercadoLibre.js b/src/js/controllers/mercadoLibre.js new file mode 100644 index 000000000..dfdd81dc1 --- /dev/null +++ b/src/js/controllers/mercadoLibre.js @@ -0,0 +1,24 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('mercadoLibreController', + function($scope, $timeout, $log, mercadoLibreService, externalLinkService, popupService) { + + $scope.openExternalLink = function(url) { + externalLinkService.open(url); + }; + + var init = function() { + mercadoLibreService.getPendingGiftCards(function(err, gcds) { + if (err) $log.error(err); + $scope.giftCards = gcds; + $timeout(function() { + $scope.$digest(); + }); + }); + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + $scope.network = mercadoLibreService.getNetwork(); + init(); + }); + }); diff --git a/src/js/controllers/mercadoLibreCards.js b/src/js/controllers/mercadoLibreCards.js new file mode 100644 index 000000000..6d67a888a --- /dev/null +++ b/src/js/controllers/mercadoLibreCards.js @@ -0,0 +1,100 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('mercadoLibreCardsController', + function($scope, $timeout, $ionicModal, $log, $ionicScrollDelegate, lodash, mercadoLibreService, platformInfo, externalLinkService, popupService, ongoingProcess) { + + $scope.openExternalLink = function(url) { + externalLinkService.open(url); + }; + + var updateGiftCards = function(cb) { + mercadoLibreService.getPendingGiftCards(function(err, gcds) { + if (err) { + popupService.showAlert('Could not get gift cards', err); + if (cb) return cb(); + else return; + } + $scope.giftCards = gcds; + $timeout(function() { + $scope.$digest(); + $ionicScrollDelegate.resize(); + if (cb) return cb(); + }, 100); + }); + }; + + $scope.updatePendingGiftCards = lodash.debounce(function() { + updateGiftCards(function() { + var index = 0; + var gcds = $scope.giftCards; + lodash.forEach(gcds, function(dataFromStorage) { + if (dataFromStorage.status == 'PENDING') { + $log.debug("Creating / Updating gift card"); + + mercadoLibreService.createGiftCard(dataFromStorage, function(err, giftCard) { + + if (err) { + popupService.showAlert('Error creating gift card', err); + return; + } + + if (giftCard.status != 'PENDING') { + var newData = {}; + + if (!giftCard.status) dataFromStorage.status = null; // Fix error from server + + var cardStatus = giftCard.cardStatus; + if (cardStatus && (cardStatus != 'active' && cardStatus != 'inactive' && cardStatus != 'expired')) + giftCard.status = 'FAILURE'; + + lodash.merge(newData, dataFromStorage, giftCard); + + mercadoLibreService.savePendingGiftCard(newData, null, function(err) { + $log.debug("Saving new gift card"); + updateGiftCards(); + }); + } + }); + } + }); + }); + + }, 1000, { + 'leading': true + }); + + $scope.openCardModal = function(card) { + $scope.card = card; + + $ionicModal.fromTemplateUrl('views/modals/mercadolibre-card-details.html', { + scope: $scope + }).then(function(modal) { + $scope.mercadoLibreCardDetailsModal = modal; + $scope.mercadoLibreCardDetailsModal.show(); + }); + + $scope.$on('modal.hidden', function() { + $scope.updatePendingGiftCards(); + }); + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + $scope.invoiceId = data.stateParams.invoiceId; + updateGiftCards(function() { + if ($scope.invoiceId) { + var card = lodash.find($scope.giftCards, { + invoiceId: $scope.invoiceId + }); + if (lodash.isEmpty(card)) { + popupService.showAlert(null, 'Card not found'); + return; + } + $scope.openCardModal(card); + } + }); + }); + + $scope.$on("$ionicView.afterEnter", function(event, data) { + $scope.updatePendingGiftCards(); + }); + }); diff --git a/src/js/controllers/modals/mercadoLibreCardDetails.js b/src/js/controllers/modals/mercadoLibreCardDetails.js new file mode 100644 index 000000000..78bdf544d --- /dev/null +++ b/src/js/controllers/modals/mercadoLibreCardDetails.js @@ -0,0 +1,21 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('mercadoLibreCardDetailsController', function($scope, mercadoLibreService, externalLinkService) { + + $scope.remove = function() { + mercadoLibreService.savePendingGiftCard($scope.card, { + remove: true + }, function(err) { + $scope.close(); + }); + }; + + $scope.close = function() { + $scope.mercadoLibreCardDetailsModal.hide(); + }; + + $scope.openExternalLink = function(url) { + externalLinkService.open(url); + }; + +}); diff --git a/src/js/routes.js b/src/js/routes.js index 4904be9f6..59cfad4fa 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1026,6 +1026,57 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr abstract: true }) + /* + * + * Mercado Libre Gift Card + * + */ + + .state('tabs.giftcards.mercadoLibre', { + url: '/mercadoLibre', + views: { + 'tab-home@tabs': { + controller: 'mercadoLibreController', + templateUrl: 'views/mercadoLibre.html' + } + } + }) + .state('tabs.giftcards.mercadoLibre.cards', { + url: '/cards', + views: { + 'tab-home@tabs': { + controller: 'mercadoLibreCardsController', + templateUrl: 'views/mercadoLibreCards.html' + } + }, + params: { + invoiceId: null + } + }) + .state('tabs.giftcards.mercadoLibre.amount', { + url: '/amount', + views: { + 'tab-home@tabs': { + controller: 'amountController', + templateUrl: 'views/amount.html' + } + }, + params: { + nextStep: 'tabs.giftcards.mercadoLibre.buy', + currency: 'BRL', + forceCurrency: true + } + }) + .state('tabs.giftcards.mercadoLibre.buy', { + url: '/buy/:amount/:currency', + views: { + 'tab-home@tabs': { + controller: 'buyMercadoLibreController', + templateUrl: 'views/buyMercadoLibre.html' + } + } + }) + /* * * Amazon.com Gift Card @@ -1135,7 +1186,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }); }) - .run(function($rootScope, $state, $location, $log, $timeout, startupService, ionicToast, fingerprintService, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, configService, emailService, /* plugins START HERE => */ coinbaseService, glideraService, amazonService, bitpayCardService, applicationService) { + .run(function($rootScope, $state, $location, $log, $timeout, startupService, ionicToast, fingerprintService, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, configService, emailService, /* plugins START HERE => */ coinbaseService, glideraService, amazonService, bitpayCardService, applicationService, mercadoLibreService) { uxLanguage.init(); diff --git a/src/js/services/mercadoLibreService.js b/src/js/services/mercadoLibreService.js new file mode 100644 index 000000000..0db1549ce --- /dev/null +++ b/src/js/services/mercadoLibreService.js @@ -0,0 +1,182 @@ +'use strict'; +angular.module('copayApp.services').factory('mercadoLibreService', function($http, $log, lodash, moment, storageService, configService, platformInfo, nextStepsService, homeIntegrationsService) { + var root = {}; + var credentials = {}; + + // Not used yet + var availableCountries = [{ + 'country': 'Brazil', + 'currency': 'BRL', + 'name': 'Mercado Livre', + 'url': 'https://www.mercadolivre.com.br' + }]; + + /* + * Development: 'testnet' + * Production: 'livenet' + */ + credentials.NETWORK = 'livenet'; + //credentials.NETWORK = 'testnet'; + + if (credentials.NETWORK == 'testnet') { + credentials.BITPAY_API_URL = "https://test.bitpay.com"; + } else { + credentials.BITPAY_API_URL = "https://bitpay.com"; + }; + + var homeItem = { + name: 'mercadoLibre', + title: 'Mercado Livre Brazil Gift Cards', + icon: 'icon-ml', + sref: 'tabs.giftcards.mercadoLibre', + }; + + var nextStepItem = { + name: 'mercadoLibre', + title: 'Buy Mercado Livre Brazil Gift Cards', + icon: 'icon-ml', + sref: 'tabs.giftcards.mercadoLibre', + }; + + var _getBitPay = function(endpoint) { + return { + method: 'GET', + url: credentials.BITPAY_API_URL + endpoint, + headers: { + 'content-type': 'application/json' + } + }; + }; + + var _postBitPay = function(endpoint, data) { + return { + method: 'POST', + url: credentials.BITPAY_API_URL + endpoint, + headers: { + 'content-type': 'application/json' + }, + data: data + }; + }; + + root.getNetwork = function() { + return credentials.NETWORK; + }; + + root.savePendingGiftCard = function(gc, opts, cb) { + var network = root.getNetwork(); + storageService.getMercadoLibreGiftCards(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.invoiceId] = gc; + if (opts && (opts.error || opts.status)) { + inv[gc.invoiceId] = lodash.assign(inv[gc.invoiceId], opts); + } + if (opts && opts.remove) { + delete(inv[gc.invoiceId]); + } + + inv = JSON.stringify(inv); + + + storageService.setMercadoLibreGiftCards(network, inv, function(err) { + + homeIntegrationsService.register(homeItem); + nextStepsService.unregister(nextStepItem.name); + return cb(err); + }); + }); + }; + + root.getPendingGiftCards = function(cb) { + var network = root.getNetwork(); + storageService.getMercadoLibreGiftCards(network, function(err, giftCards) { + var _gcds = giftCards ? JSON.parse(giftCards) : null; + return cb(err, _gcds); + }); + }; + + root.createBitPayInvoice = function(data, cb) { + var dataSrc = { + currency: data.currency, + amount: data.amount, + clientId: data.uuid + }; + + $http(_postBitPay('/mercado-libre-gift/pay', dataSrc)).then(function(data) { + $log.info('BitPay Create Invoice: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('BitPay Create Invoice: ERROR', JSON.stringify(data.data)); + return cb(data.data); + }); + }; + + root.getBitPayInvoice = function(id, cb) { + $http(_getBitPay('/invoices/' + id)).then(function(data) { + $log.info('BitPay Get Invoice: SUCCESS'); + return cb(null, data.data.data); + }, function(data) { + $log.error('BitPay Get Invoice: ERROR', JSON.stringify(data.data)); + return cb(data.data); + }); + }; + + root.createGiftCard = function(data, cb) { + var dataSrc = { + "clientId": data.uuid, + "invoiceId": data.invoiceId, + "accessKey": data.accessKey + }; + + $http(_postBitPay('/mercado-libre-gift/redeem', dataSrc)).then(function(data) { + var status = data.data.status == 'new' ? 'PENDING' : (data.data.status == 'paid') ? 'PENDING' : data.data.status; + data.data.status = status; + $log.info('Mercado Libre Gift Card Create/Update: ' + status); + return cb(null, data.data); + }, function(data) { + $log.error('Mercado Libre Gift Card Create/Update: ERROR', JSON.stringify(data.data)); + return cb(data.data); + }); + }; + + /* + * Disabled for now * + */ + /* + root.cancelGiftCard = function(data, cb) { + + var dataSrc = { + "clientId": data.uuid, + "invoiceId": data.invoiceId, + "accessKey": data.accessKey + }; + + $http(_postBitPay('/mercado-libre-gift/cancel', dataSrc)).then(function(data) { + $log.info('Mercado Libre Gift Card Cancel: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Mercado Libre Gift Card Cancel: ' + data.data.message); + return cb(data.data); + }); + }; + */ + + var register = function() { + storageService.getMercadoLibreGiftCards(root.getNetwork(), function(err, giftCards) { + if (giftCards) { + homeIntegrationsService.register(homeItem); + } else { + nextStepsService.register(nextStepItem); + } + }); + }; + + register(); + return root; +}); diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 9c00cec35..9f32b137f 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -769,12 +769,14 @@ angular.module('copayApp.services') if (opts.hasFunds) { ret = lodash.filter(ret, function(w) { + if (!w.status) return; return (w.status.availableBalanceSat > 0); }); } if (opts.minAmount) { ret = lodash.filter(ret, function(w) { + if (!w.status) return; return (w.status.availableBalanceSat > opts.minAmount); }); } diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 5391569fd..4cd2af62e 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -610,5 +610,17 @@ angular.module('copayApp.services') storage.remove('txConfirmNotif-' + txid, cb); }; + root.setMercadoLibreGiftCards = function(network, gcs, cb) { + storage.set('mercadoLibreGiftCards-' + network, gcs, cb); + }; + + root.getMercadoLibreGiftCards = function(network, cb) { + storage.get('mercadoLibreGiftCards-' + network, cb); + }; + + root.removeMercadoLibreGiftCards = function(network, cb) { + storage.remove('MercadoLibreGiftCards-' + network, cb); + }; + return root; }); diff --git a/src/sass/views/integrations/integrations.scss b/src/sass/views/integrations/integrations.scss index 22f9cbc0e..9632da354 100644 --- a/src/sass/views/integrations/integrations.scss +++ b/src/sass/views/integrations/integrations.scss @@ -1,6 +1,7 @@ @import "coinbase"; @import "glidera"; @import "amazon"; +@import "mercadolibre"; #coinbase, #glidera { .button-small { diff --git a/src/sass/views/integrations/mercadolibre.scss b/src/sass/views/integrations/mercadolibre.scss new file mode 100644 index 000000000..db8e6ecfe --- /dev/null +++ b/src/sass/views/integrations/mercadolibre.scss @@ -0,0 +1,202 @@ +#mercadolibre { + $item-lateral-padding: 20px; + $item-vertical-padding: 10px; + $item-border-color: #EFEFEF; + $item-label-color: #6C6C6E; + @extend .deflash-blue; + .icon-amazon { + background-image: url("../img/mercado-libre/icon-ml.svg"); + } + .spinner svg { + stroke: black; + fill: black; + } + + .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.8rem; + + img { + margin-right: 1rem; + height: 35px; + width: 35px; + } + + span { + text-transform: capitalize; + } + + .big-icon-svg { + padding: 0 7px 0 0; + margin-right: 0.6rem; + } + + .big-icon-svg > .bg { + height: 28px; + box-shadow: none; + } + + } + .amount-label{ + line-height: 30px; + .amount{ + font-size: 38px; + margin-bottom: .5rem; + + > .unit { + font-family: "Roboto-Light"; + } + } + .alternative { + font-size: 12px; + 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; + } + } + } + } +} + +#meli-list-cards { + img.item-logo { + width: auto; + height: auto; + border-radius: 0; + } +} + +#meli-card { + .card-head { + margin: 20px 0; + text-align: center; + .date { + font-size: 12px; + margin: 10px 0; + } + .amount { + font-size: 16px; + font-weight: bold; + } + } + .card-status { + text-align: center; + margin-bottom: 25px; + .card-status-desc { + margin-top: 5px; + font-size: 12px; + color: $v-text-secondary-color; + } + .redeem-pin { + font-weight: bold; + font-size: 22px; + } + .button-redeem { + margin-top: 10px; + background: transparent; + border: none; + font-size: 12px; + color: $v-text-accent-color; + } + } + .card-remove { + text-align: center; + margin-top: 30px; + .button-remove { + margin-top: 10px; + background: transparent; + border: none; + font-size: 12px; + color: red; + } + } + +} diff --git a/src/sass/views/tab-home.scss b/src/sass/views/tab-home.scss index 3886ba7ee..99dc0f4da 100644 --- a/src/sass/views/tab-home.scss +++ b/src/sass/views/tab-home.scss @@ -17,6 +17,10 @@ .icon-amazon { background-image: url("../img/icon-amazon.svg"); } + .icon-ml { + background-image: url("../img/mercado-libre/icon-ml.svg"); + height: 27px; + } .bg { &.wallet { padding: .25rem diff --git a/www/img/mercado-libre/24px.svg b/www/img/mercado-libre/24px.svg new file mode 100644 index 000000000..7a667609c --- /dev/null +++ b/www/img/mercado-libre/24px.svg @@ -0,0 +1,24 @@ + + + + 24px + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/img/mercado-libre/giftcard-pt.svg b/www/img/mercado-libre/giftcard-pt.svg new file mode 100644 index 000000000..f980b61f6 --- /dev/null +++ b/www/img/mercado-libre/giftcard-pt.svg @@ -0,0 +1,2311 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/img/mercado-libre/icon-ml.svg b/www/img/mercado-libre/icon-ml.svg new file mode 100644 index 000000000..01b1fe7a7 --- /dev/null +++ b/www/img/mercado-libre/icon-ml.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/www/img/mercado-libre/meli-card-24px.png b/www/img/mercado-libre/meli-card-24px.png new file mode 100644 index 000000000..2e42ef103 Binary files /dev/null and b/www/img/mercado-libre/meli-card-24px.png differ diff --git a/www/img/mercado-libre/mlbr.svg b/www/img/mercado-libre/mlbr.svg new file mode 100644 index 000000000..031c72393 --- /dev/null +++ b/www/img/mercado-libre/mlbr.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/views/amount.html b/www/views/amount.html index 062722793..5dfee29e5 100644 --- a/www/views/amount.html +++ b/www/views/amount.html @@ -1,7 +1,7 @@ - {{'Enter Amount' | translate}} + {{'Enter amount' | translate}} diff --git a/www/views/buyMercadoLibre.html b/www/views/buyMercadoLibre.html new file mode 100644 index 000000000..04461ad7a --- /dev/null +++ b/www/views/buyMercadoLibre.html @@ -0,0 +1,110 @@ + + + + + + {{'Buy'|translate}} + + + + + +
+ +
+
+ +
+
+ Mercado Livre Brazil Gift Cards +
+
+
{{amountUnitStr}}
+
+
+ +
+
+
From
+
+ + + + {{wallet ? wallet.name : '...'}} +
+ +
+
+
+ Details +
+
+ Gift card + + {{amount | currency:'$ ':2}} {{currencyIsoCode}} + +
+
+ Invoice Fee + + {{invoiceFee | currency:'$ ':2}} {{currencyIsoCode}} + +
+
+ Network Fee + + {{networkFee | currency:'$ ':2}} {{currencyIsoCode}} + +
+
+ Total + + {{totalAmount | currency:'$ ':2}} {{currencyIsoCode}} + ({{totalAmountStr}}) + +
+
+
+
+ +
+ + + {{'Confirm purchase'|translate}} + + + {{'Slide to buy'|translate}} + + + + Your purchase could not be completed + + + Your purchase was added to the list of pending + + + Bought {{mlGiftCard.amount}} {{mlGiftCard.currency}} + +
+ Gift card generated and ready to use. +
+
+ + + +
diff --git a/www/views/mercadoLibre.html b/www/views/mercadoLibre.html new file mode 100644 index 000000000..98e41fa09 --- /dev/null +++ b/www/views/mercadoLibre.html @@ -0,0 +1,63 @@ + + + + + + {{'Mercado Livre Brazil Gift Cards'|translate}} + + + + +
+ Sandbox version. Only for testing purpose. +
+
+ +
+ Only redeemable on Mercado Livre (Brazil) +
+
+ + +
+
+
+ +
+ Sandbox version. Only for testing purpose. +
+ +
+ Mercado Libre +
+ Only redeemable on Mercado Livre (Brazil) +
+
+ + +
+
diff --git a/www/views/mercadoLibreCards.html b/www/views/mercadoLibreCards.html new file mode 100644 index 000000000..ba7eba1ec --- /dev/null +++ b/www/views/mercadoLibreCards.html @@ -0,0 +1,34 @@ + + + + + + {{'Your Gift Cards'|translate}} + + + + +
+
+ + + + Error + Invoice expired + Still pending + Pending + + Inactive + Expired + +

+ {{item.amount | currency : '' : 2}} {{item.currency}} +

+

{{item.date | amTimeAgo}}

+
+
+
+
diff --git a/www/views/modals/mercadolibre-card-details.html b/www/views/modals/mercadolibre-card-details.html new file mode 100644 index 000000000..3d272e3de --- /dev/null +++ b/www/views/modals/mercadolibre-card-details.html @@ -0,0 +1,75 @@ + + + +

Details

+
+ + + +
+ Mercado Livre Brazil Gift Card + +
+ {{card.amount | currency : '' : 2}} {{card.currency}} +
+
+ +
+
+
{{card.pin}}
+ +
+ +
+ Inactive +
Gift Card is not available to use anymore
+
+ +
+ Expired +
Gift Card is not available to use anymore
+
+ +
+ + Pending + + + Still pending + + + Error + + + Invoice expired + +
+
+ +
+
+ Created + + {{card.date | amTimeAgo}} + +
+ +
+ + See invoice +
+
+ +
+ +
+ +
+