Merge pull request #5334 from cmgustavo/feat/coinbase-integration
Re-enable Coinbase integration
This commit is contained in:
commit
6bbb1fd442
30 changed files with 1632 additions and 1241 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue