Merge pull request #3289 from cmgustavo/feat/touch-id-ios
Set touch id to spend funds (only iOS)
This commit is contained in:
commit
0cb4b6b2a3
6 changed files with 199 additions and 97 deletions
|
|
@ -129,6 +129,9 @@ if [ ! -d $PROJECT ]; then
|
||||||
cordova plugin add org.apache.cordova.file
|
cordova plugin add org.apache.cordova.file
|
||||||
checkOK
|
checkOK
|
||||||
|
|
||||||
|
cordova plugin add https://github.com/EddyVerbruggen/cordova-plugin-touch-id && cordova prepare
|
||||||
|
checkOK
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $DBGJS
|
if $DBGJS
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="content preferences" ng-controller="preferencesController as preferences">
|
<div class="content preferences" ng-controller="preferencesController as preferences" ng-init="preferences.init()">
|
||||||
|
|
||||||
<ul class="no-bullet m0 size-14" ng-show="!index.noFocusedWallet">
|
<ul class="no-bullet m0 size-14" ng-show="!index.noFocusedWallet">
|
||||||
<h4 class="title m0">{{index.alias}} [{{index.walletName}}] <span translate>settings</span></h4>
|
<h4 class="title m0">{{index.alias}} [{{index.walletName}}] <span translate>settings</span></h4>
|
||||||
|
|
@ -40,13 +40,17 @@
|
||||||
<span translate>Request Password for Spending Funds</span>
|
<span translate>Request Password for Spending Funds</span>
|
||||||
<switch id="network-name" name="encrypt" ng-model="encrypt" class="green right"></switch>
|
<switch id="network-name" name="encrypt" ng-model="encrypt" class="green right"></switch>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="line-b p20" ng-show="preferences.touchidAvailable">
|
||||||
|
<span translate>Request Touch ID for Spending Funds</span>
|
||||||
|
<switch id="touchid" name="touchid" ng-model="touchid" class="green right"></switch>
|
||||||
|
</li>
|
||||||
<li class="line-b p20" ng-show="index.isPrivKeyExternal">
|
<li class="line-b p20" ng-show="index.isPrivKeyExternal">
|
||||||
<span translate>Hardware wallet</span>
|
<span translate>Hardware wallet</span>
|
||||||
<span class="right text-gray">
|
<span class="right text-gray">
|
||||||
{{preferences.externalSource}}
|
{{preferences.externalSource}}
|
||||||
<!-- (Accont {{preferences.externalAccount}}) -->
|
<!-- (Accont {{preferences.externalAccount}}) -->
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="line-b p20" ng-click="$root.go('backup')" ng-hide="index.isPrivKeyExternal">
|
<li class="line-b p20" ng-click="$root.go('backup')" ng-hide="index.isPrivKeyExternal">
|
||||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||||
<span class="text-warning right" ng-show="index.needsBackup">
|
<span class="text-warning right" ng-show="index.needsBackup">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, amMoment, nodeWebkit, addonManager, feeService, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, isMobile) {
|
angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, nodeWebkit, addonManager, feeService, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, isMobile) {
|
||||||
var self = this;
|
var self = this;
|
||||||
self.isCordova = isCordova;
|
self.isCordova = isCordova;
|
||||||
self.isChromeApp = isChromeApp;
|
self.isChromeApp = isChromeApp;
|
||||||
|
|
@ -1152,6 +1152,20 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
||||||
self.setTab(tab, reset);
|
self.setTab(tab, reset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$rootScope.$on('Local/RequestTouchid', function(event, cb) {
|
||||||
|
window.plugins.touchid.verifyFingerprint(
|
||||||
|
gettextCatalog.getString('Scan your fingerprint please'),
|
||||||
|
function(msg) {
|
||||||
|
// OK
|
||||||
|
return cb();
|
||||||
|
},
|
||||||
|
function(msg) {
|
||||||
|
// ERROR
|
||||||
|
return cb(gettext('Invalid Touch ID'));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$rootScope.$on('Local/ShowAlert', function(event, msg, cb) {
|
$rootScope.$on('Local/ShowAlert', function(event, msg, cb) {
|
||||||
self.showErrorPopup(msg, cb);
|
self.showErrorPopup(msg, cb);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,34 @@
|
||||||
|
|
||||||
angular.module('copayApp.controllers').controller('preferencesController',
|
angular.module('copayApp.controllers').controller('preferencesController',
|
||||||
function($scope, $rootScope, $filter, $timeout, $modal, $log, lodash, configService, profileService, uxLanguage) {
|
function($scope, $rootScope, $filter, $timeout, $modal, $log, lodash, configService, profileService, uxLanguage) {
|
||||||
var config = configService.getSync();
|
|
||||||
this.unitName = config.wallet.settings.unitName;
|
this.init = function() {
|
||||||
this.bwsurl = config.bws.url;
|
var config = configService.getSync();
|
||||||
this.currentLanguageName = uxLanguage.getCurrentLanguageName();
|
this.unitName = config.wallet.settings.unitName;
|
||||||
this.selectedAlternative = {
|
this.bwsurl = config.bws.url;
|
||||||
name: config.wallet.settings.alternativeName,
|
this.currentLanguageName = uxLanguage.getCurrentLanguageName();
|
||||||
isoCode: config.wallet.settings.alternativeIsoCode
|
this.selectedAlternative = {
|
||||||
};
|
name: config.wallet.settings.alternativeName,
|
||||||
$scope.spendUnconfirmed = config.wallet.spendUnconfirmed;
|
isoCode: config.wallet.settings.alternativeIsoCode
|
||||||
$scope.glideraEnabled = config.glidera.enabled;
|
};
|
||||||
$scope.glideraTestnet = config.glidera.testnet;
|
$scope.spendUnconfirmed = config.wallet.spendUnconfirmed;
|
||||||
var fc = profileService.focusedClient;
|
$scope.glideraEnabled = config.glidera.enabled;
|
||||||
if (fc) {
|
$scope.glideraTestnet = config.glidera.testnet;
|
||||||
$scope.encrypt = fc.hasPrivKeyEncrypted();
|
var fc = profileService.focusedClient;
|
||||||
this.externalSource = fc.getPrivKeyExternalSourceName() == 'ledger' ? "Ledger" : null;
|
if (fc) {
|
||||||
// TODO externalAccount
|
$scope.encrypt = fc.hasPrivKeyEncrypted();
|
||||||
//this.externalIndex = fc.getExternalIndex();
|
this.externalSource = fc.getPrivKeyExternalSourceName() == 'ledger' ? "Ledger" : null;
|
||||||
}
|
// TODO externalAccount
|
||||||
|
//this.externalIndex = fc.getExternalIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.touchidAvailable) {
|
||||||
|
var walletId = fc.credentials.walletId;
|
||||||
|
this.touchidAvailable = true;
|
||||||
|
config.touchIdFor = config.touchIdFor || {};
|
||||||
|
$scope.touchid = config.touchIdFor[walletId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var unwatchSpendUnconfirmed = $scope.$watch('spendUnconfirmed', function(newVal, oldVal) {
|
var unwatchSpendUnconfirmed = $scope.$watch('spendUnconfirmed', function(newVal, oldVal) {
|
||||||
if (newVal == oldVal) return;
|
if (newVal == oldVal) return;
|
||||||
|
|
@ -94,10 +104,43 @@ angular.module('copayApp.controllers').controller('preferencesController',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var unwatchRequestTouchid = $scope.$watch('touchid', function(newVal, oldVal) {
|
||||||
|
if (newVal == oldVal || $scope.touchidError) {
|
||||||
|
$scope.touchidError = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var walletId = profileService.focusedClient.credentials.walletId;
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
touchIdFor: {}
|
||||||
|
};
|
||||||
|
opts.touchIdFor[walletId] = newVal;
|
||||||
|
|
||||||
|
$rootScope.$emit('Local/RequestTouchid', function(err) {
|
||||||
|
if (err) {
|
||||||
|
$log.debug(err);
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.touchidError = true;
|
||||||
|
$scope.touchid = oldVal;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
configService.set(opts, function(err) {
|
||||||
|
if (err) {
|
||||||
|
$log.debug(err);
|
||||||
|
$scope.touchidError = true;
|
||||||
|
$scope.touchid = oldVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
unwatch();
|
unwatch();
|
||||||
unwatchSpendUnconfirmed();
|
unwatchSpendUnconfirmed();
|
||||||
unwatchGlideraEnabled();
|
unwatchGlideraEnabled();
|
||||||
unwatchGlideraTestnet();
|
unwatchGlideraTestnet();
|
||||||
|
unwatchRequestTouchid();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,19 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
var self = this;
|
var self = this;
|
||||||
$rootScope.hideMenuBar = false;
|
$rootScope.hideMenuBar = false;
|
||||||
$rootScope.wpInputFocused = false;
|
$rootScope.wpInputFocused = false;
|
||||||
$scope.currentSpendUnconfirmed = configService.getSync().wallet.spendUnconfirmed;
|
var config = configService.getSync();
|
||||||
|
var configWallet = config.wallet;
|
||||||
|
$scope.currentSpendUnconfirmed = configWallet.spendUnconfirmed;
|
||||||
|
|
||||||
// INIT
|
// INIT
|
||||||
var config = configService.getSync().wallet.settings;
|
var walletSettings = configWallet.settings;
|
||||||
this.unitToSatoshi = config.unitToSatoshi;
|
this.unitToSatoshi = walletSettings.unitToSatoshi;
|
||||||
this.satToUnit = 1 / this.unitToSatoshi;
|
this.satToUnit = 1 / this.unitToSatoshi;
|
||||||
this.unitName = config.unitName;
|
this.unitName = walletSettings.unitName;
|
||||||
this.alternativeIsoCode = config.alternativeIsoCode;
|
this.alternativeIsoCode = walletSettings.alternativeIsoCode;
|
||||||
this.alternativeName = config.alternativeName;
|
this.alternativeName = walletSettings.alternativeName;
|
||||||
this.alternativeAmount = 0;
|
this.alternativeAmount = 0;
|
||||||
this.unitDecimals = config.unitDecimals;
|
this.unitDecimals = walletSettings.unitDecimals;
|
||||||
this.isCordova = isCordova;
|
this.isCordova = isCordova;
|
||||||
this.addresses = [];
|
this.addresses = [];
|
||||||
this.isMobile = isMobile.any();
|
this.isMobile = isMobile.any();
|
||||||
|
|
@ -85,6 +87,16 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
$rootScope.hideMenuBar = false;
|
$rootScope.hideMenuBar = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var requestTouchid = function(cb) {
|
||||||
|
var fc = profileService.focusedClient;
|
||||||
|
config.touchIdFor = config.touchIdFor || {};
|
||||||
|
if (window.touchidAvailable && config.touchIdFor[fc.credentials.walletId]) {
|
||||||
|
$rootScope.$emit('Local/RequestTouchid', cb);
|
||||||
|
} else {
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
rateService.whenAvailable(function() {
|
rateService.whenAvailable(function() {
|
||||||
self.isRateAvailable = true;
|
self.isRateAvailable = true;
|
||||||
$rootScope.$digest();
|
$rootScope.$digest();
|
||||||
|
|
@ -249,7 +261,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.sign = function(txp) {
|
$scope.sign = function(txp) {
|
||||||
var fc = profileService.focusedClient;
|
var fc = profileService.focusedClient;
|
||||||
|
|
||||||
if (!fc.canSign() && !fc.isPrivKeyExternal())
|
if (!fc.canSign() && !fc.isPrivKeyExternal())
|
||||||
return;
|
return;
|
||||||
|
|
@ -264,45 +276,56 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self._setOngoingForSigning();
|
self._setOngoingForSigning();
|
||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.error = null;
|
$scope.error = null;
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
profileService.signTxProposal(txp, function(err, txpsi) {
|
requestTouchid(function(err) {
|
||||||
self.setOngoingProcess();
|
|
||||||
if (err) {
|
if (err) {
|
||||||
$scope.$emit('UpdateTx');
|
self.setOngoingProcess();
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not accept payment'));
|
profileService.lockFC();
|
||||||
|
$scope.error = err;
|
||||||
$scope.$digest();
|
$scope.$digest();
|
||||||
} else {
|
return;
|
||||||
//if txp has required signatures then broadcast it
|
|
||||||
var txpHasRequiredSignatures = txpsi.status == 'accepted';
|
|
||||||
if (txpHasRequiredSignatures) {
|
|
||||||
self.setOngoingProcess(gettext('Broadcasting transaction'));
|
|
||||||
$scope.loading = true;
|
|
||||||
fc.broadcastTxProposal(txpsi, function(err, txpsb, memo) {
|
|
||||||
self.setOngoingProcess();
|
|
||||||
$scope.loading = false;
|
|
||||||
if (err) {
|
|
||||||
$scope.$emit('UpdateTx');
|
|
||||||
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not broadcast payment'));
|
|
||||||
$scope.$digest();
|
|
||||||
} else {
|
|
||||||
$log.debug('Transaction signed and broadcasted')
|
|
||||||
if (memo)
|
|
||||||
$log.info(memo);
|
|
||||||
|
|
||||||
refreshUntilItChanges = true;
|
|
||||||
$modalInstance.close(txpsb);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$scope.loading = false;
|
|
||||||
$modalInstance.close(txpsi);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profileService.signTxProposal(txp, function(err, txpsi) {
|
||||||
|
self.setOngoingProcess();
|
||||||
|
if (err) {
|
||||||
|
$scope.$emit('UpdateTx');
|
||||||
|
$scope.loading = false;
|
||||||
|
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not accept payment'));
|
||||||
|
$scope.$digest();
|
||||||
|
} else {
|
||||||
|
//if txp has required signatures then broadcast it
|
||||||
|
var txpHasRequiredSignatures = txpsi.status == 'accepted';
|
||||||
|
if (txpHasRequiredSignatures) {
|
||||||
|
self.setOngoingProcess(gettext('Broadcasting transaction'));
|
||||||
|
$scope.loading = true;
|
||||||
|
fc.broadcastTxProposal(txpsi, function(err, txpsb, memo) {
|
||||||
|
self.setOngoingProcess();
|
||||||
|
$scope.loading = false;
|
||||||
|
if (err) {
|
||||||
|
$scope.$emit('UpdateTx');
|
||||||
|
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not broadcast payment'));
|
||||||
|
$scope.$digest();
|
||||||
|
} else {
|
||||||
|
$log.debug('Transaction signed and broadcasted')
|
||||||
|
if (memo)
|
||||||
|
$log.info(memo);
|
||||||
|
|
||||||
|
refreshUntilItChanges = true;
|
||||||
|
$modalInstance.close(txpsb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$scope.loading = false;
|
||||||
|
$modalInstance.close(txpsi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
@ -787,44 +810,56 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
} else {
|
} else {
|
||||||
feeService.getCurrentFeeValue(self.currentSendFeeLevel, cb);
|
feeService.getCurrentFeeValue(self.currentSendFeeLevel, cb);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getFee(function(err, feePerKb) {
|
requestTouchid(function(err) {
|
||||||
if (err) $log.debug(err);
|
if (err) {
|
||||||
fc.sendTxProposal({
|
profileService.lockFC();
|
||||||
toAddress: address,
|
self.setOngoingProcess();
|
||||||
amount: amount,
|
self.error = err;
|
||||||
message: comment,
|
$timeout(function() {
|
||||||
payProUrl: paypro ? paypro.url : null,
|
$scope.$digest();
|
||||||
feePerKb: feePerKb,
|
}, 1);
|
||||||
excludeUnconfirmedUtxos: $scope.currentSpendUnconfirmed ? false : true
|
return;
|
||||||
}, function(err, txp) {
|
}
|
||||||
if (err) {
|
|
||||||
self.setOngoingProcess();
|
|
||||||
profileService.lockFC();
|
|
||||||
return self.setSendError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fc.canSign() && !fc.isPrivKeyExternal()) {
|
getFee(function(err, feePerKb) {
|
||||||
$log.info('No signing proposal: No private key')
|
if (err) $log.debug(err);
|
||||||
self.setOngoingProcess();
|
fc.sendTxProposal({
|
||||||
self.resetForm();
|
toAddress: address,
|
||||||
txStatus.notify(txp, function() {
|
amount: amount,
|
||||||
return $scope.$emit('Local/TxProposalAction');
|
message: comment,
|
||||||
});
|
payProUrl: paypro ? paypro.url : null,
|
||||||
return;
|
feePerKb: feePerKb,
|
||||||
}
|
excludeUnconfirmedUtxos: $scope.currentSpendUnconfirmed ? false : true
|
||||||
|
}, function(err, txp) {
|
||||||
self.signAndBroadcast(txp, function(err) {
|
|
||||||
self.setOngoingProcess();
|
|
||||||
self.resetForm();
|
|
||||||
if (err) {
|
if (err) {
|
||||||
self.error = err.message ? err.message : gettext('The payment was created but could not be completed. Please try again from home screen');
|
self.setOngoingProcess();
|
||||||
$scope.$emit('Local/TxProposalAction');
|
profileService.lockFC();
|
||||||
$timeout(function() {
|
return self.setSendError(err);
|
||||||
$scope.$digest();
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fc.canSign() && !fc.isPrivKeyExternal()) {
|
||||||
|
$log.info('No signing proposal: No private key')
|
||||||
|
self.setOngoingProcess();
|
||||||
|
self.resetForm();
|
||||||
|
txStatus.notify(txp, function() {
|
||||||
|
return $scope.$emit('Local/TxProposalAction');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.signAndBroadcast(txp, function(err) {
|
||||||
|
self.setOngoingProcess();
|
||||||
|
self.resetForm();
|
||||||
|
if (err) {
|
||||||
|
self.error = err.message ? err.message : gettext('The payment was created but could not be completed. Please try again from home screen');
|
||||||
|
$scope.$emit('Local/TxProposalAction');
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.$digest();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1122,7 +1157,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
||||||
var fc = profileService.focusedClient;
|
var fc = profileService.focusedClient;
|
||||||
var ModalInstanceCtrl = function($scope, $modalInstance) {
|
var ModalInstanceCtrl = function($scope, $modalInstance) {
|
||||||
$scope.btx = btx;
|
$scope.btx = btx;
|
||||||
$scope.settings = config;
|
$scope.settings = walletSettings;
|
||||||
$scope.color = fc.backgroundColor;
|
$scope.color = fc.backgroundColor;
|
||||||
$scope.copayerId = fc.credentials.copayerId;
|
$scope.copayerId = fc.credentials.copayerId;
|
||||||
$scope.isShared = fc.credentials.n > 1;
|
$scope.isShared = fc.credentials.n > 1;
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ angular.element(document).ready(function() {
|
||||||
window.location = '#/preferences';
|
window.location = '#/preferences';
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
navigator.splashscreen.hide();
|
navigator.splashscreen.hide();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
@ -67,6 +65,11 @@ angular.element(document).ready(function() {
|
||||||
window.plugins.webintent.onNewIntent(handleBitcoinURI);
|
window.plugins.webintent.onNewIntent(handleBitcoinURI);
|
||||||
window.handleOpenURL = handleBitcoinURI;
|
window.handleOpenURL = handleBitcoinURI;
|
||||||
|
|
||||||
|
window.plugins.touchid.isAvailable(
|
||||||
|
function(msg) { window.touchidAvailable = true; }, // success handler: TouchID available
|
||||||
|
function(msg) { window.touchidAvailable = false; } // error handler: no TouchID available
|
||||||
|
);
|
||||||
|
|
||||||
startAngular();
|
startAngular();
|
||||||
}, false);
|
}, false);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue