Merge pull request #1229 from chjj/paypro
WIP Reimplement Payment Protocol
This commit is contained in:
commit
f6e9084548
7 changed files with 44270 additions and 547 deletions
|
|
@ -169,26 +169,32 @@ angular.module('copayApp.controllers').controller('SendController',
|
||||||
$rootScope.pendingPayment = null;
|
$rootScope.pendingPayment = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX Payment Protocol is temporarily disabled.
|
var uri;
|
||||||
// var uri;
|
if (address.indexOf('bitcoin:') === 0) {
|
||||||
// if (address.indexOf('bitcoin:') === 0) {
|
uri = new bitcore.BIP21(address).data;
|
||||||
// uri = new bitcore.BIP21(address).data;
|
} else if (/^https?:\/\//.test(address)) {
|
||||||
// } else if (/^https?:\/\//.test(address)) {
|
uri = {
|
||||||
// uri = {
|
merchant: address
|
||||||
// merchant: address
|
};
|
||||||
// };
|
}
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (uri && uri.merchant) {
|
|
||||||
// w.createPaymentTx({
|
|
||||||
// uri: uri.merchant,
|
|
||||||
// memo: commentText
|
|
||||||
// }, done);
|
|
||||||
// } else {
|
|
||||||
// w.createTx(address, amount, commentText, done);
|
|
||||||
// }
|
|
||||||
|
|
||||||
w.createTx(address, amount, commentText, done);
|
// If we're setting the domain, ignore the change.
|
||||||
|
if ($rootScope.merchant
|
||||||
|
&& $rootScope.merchant.domain
|
||||||
|
&& address === $rootScope.merchant.domain) {
|
||||||
|
uri = {
|
||||||
|
merchant: $rootScope.merchant.request_url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri && uri.merchant) {
|
||||||
|
w.createPaymentTx({
|
||||||
|
uri: uri.merchant,
|
||||||
|
memo: commentText
|
||||||
|
}, done);
|
||||||
|
} else {
|
||||||
|
w.createTx(address, amount, commentText, done);
|
||||||
|
}
|
||||||
|
|
||||||
// reset fields
|
// reset fields
|
||||||
$scope.address = $scope.amount = $scope.commentText = null;
|
$scope.address = $scope.amount = $scope.commentText = null;
|
||||||
|
|
@ -464,6 +470,13 @@ angular.module('copayApp.controllers').controller('SendController',
|
||||||
var value = scope.address || '';
|
var value = scope.address || '';
|
||||||
var uri;
|
var uri;
|
||||||
|
|
||||||
|
// If we're setting the domain, ignore the change.
|
||||||
|
if ($rootScope.merchant
|
||||||
|
&& $rootScope.merchant.domain
|
||||||
|
&& value === $rootScope.merchant.domain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value.indexOf('bitcoin:') === 0) {
|
if (value.indexOf('bitcoin:') === 0) {
|
||||||
uri = new bitcore.BIP21(value).data;
|
uri = new bitcore.BIP21(value).data;
|
||||||
} else if (/^https?:\/\//.test(value)) {
|
} else if (/^https?:\/\//.test(value)) {
|
||||||
|
|
@ -520,12 +533,18 @@ angular.module('copayApp.controllers').controller('SendController',
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var url = merchantData.request_url;
|
||||||
|
var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1];
|
||||||
|
|
||||||
merchantData.unitTotal = (+merchantData.total / config.unitToSatoshi) + '';
|
merchantData.unitTotal = (+merchantData.total / config.unitToSatoshi) + '';
|
||||||
merchantData.expiration = new Date(
|
merchantData.expiration = new Date(
|
||||||
merchantData.pr.pd.expires * 1000).toISOString();
|
merchantData.pr.pd.expires * 1000).toISOString();
|
||||||
|
merchantData.domain = domain;
|
||||||
|
|
||||||
$rootScope.merchant = merchantData;
|
$rootScope.merchant = merchantData;
|
||||||
|
|
||||||
|
scope.sendForm.address.$setViewValue(domain);
|
||||||
|
scope.sendForm.address.$render();
|
||||||
scope.sendForm.address.$isValid = true;
|
scope.sendForm.address.$isValid = true;
|
||||||
|
|
||||||
scope.sendForm.amount.$setViewValue(merchantData.unitTotal);
|
scope.sendForm.amount.$setViewValue(merchantData.unitTotal);
|
||||||
|
|
@ -537,6 +556,14 @@ angular.module('copayApp.controllers').controller('SendController',
|
||||||
var unregister = scope.$watch('address', function() {
|
var unregister = scope.$watch('address', function() {
|
||||||
var val = scope.sendForm.address.$viewValue || '';
|
var val = scope.sendForm.address.$viewValue || '';
|
||||||
var uri;
|
var uri;
|
||||||
|
// If we're setting the domain, ignore the change.
|
||||||
|
if ($rootScope.merchant
|
||||||
|
&& $rootScope.merchant.domain
|
||||||
|
&& val === $rootScope.merchant.domain) {
|
||||||
|
uri = {
|
||||||
|
merchant: $rootScope.merchant.request_url
|
||||||
|
};
|
||||||
|
}
|
||||||
if (val.indexOf('bitcoin:') === 0) {
|
if (val.indexOf('bitcoin:') === 0) {
|
||||||
uri = new bitcore.BIP21(val).data;
|
uri = new bitcore.BIP21(val).data;
|
||||||
} else if (/^https?:\/\//.test(val)) {
|
} else if (/^https?:\/\//.test(val)) {
|
||||||
|
|
@ -564,9 +591,4 @@ angular.module('copayApp.controllers').controller('SendController',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX Payment Protocol is temporarily disabled.
|
|
||||||
$scope.onChanged = function() {
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('copayApp.directives')
|
angular.module('copayApp.directives')
|
||||||
.directive('validAddress', function() {
|
.directive('validAddress', ['$rootScope', function($rootScope) {
|
||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
var Address = bitcore.Address;
|
var Address = bitcore.Address;
|
||||||
var bignum = bitcore.Bignum;
|
var bignum = bitcore.Bignum;
|
||||||
|
|
@ -11,6 +11,14 @@ angular.module('copayApp.directives')
|
||||||
link: function(scope, elem, attrs, ctrl) {
|
link: function(scope, elem, attrs, ctrl) {
|
||||||
var validator = function(value) {
|
var validator = function(value) {
|
||||||
|
|
||||||
|
// If we're setting the domain, ignore the change.
|
||||||
|
if ($rootScope.merchant
|
||||||
|
&& $rootScope.merchant.domain
|
||||||
|
&& value === $rootScope.merchant.domain) {
|
||||||
|
ctrl.$setValidity('validAddress', true);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
// Regular url
|
// Regular url
|
||||||
if (/^https?:\/\//.test(value)) {
|
if (/^https?:\/\//.test(value)) {
|
||||||
ctrl.$setValidity('validAddress', true);
|
ctrl.$setValidity('validAddress', true);
|
||||||
|
|
@ -35,7 +43,7 @@ angular.module('copayApp.directives')
|
||||||
ctrl.$formatters.unshift(validator);
|
ctrl.$formatters.unshift(validator);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
}])
|
||||||
.directive('enoughAmount', ['$rootScope',
|
.directive('enoughAmount', ['$rootScope',
|
||||||
function($rootScope) {
|
function($rootScope) {
|
||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ inherits(Wallet, events.EventEmitter);
|
||||||
Wallet.builderOpts = {
|
Wallet.builderOpts = {
|
||||||
lockTime: null,
|
lockTime: null,
|
||||||
signhash: bitcore.Transaction.SIGNHASH_ALL,
|
signhash: bitcore.Transaction.SIGNHASH_ALL,
|
||||||
fee: null,
|
fee: undefined,
|
||||||
feeSat: null,
|
feeSat: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1284,21 +1284,13 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var trusted = certs.map(function(cert) {
|
|
||||||
var der = cert.toString('hex');
|
|
||||||
var pem = PayPro.prototype._DERtoPEM(der, 'CERTIFICATE');
|
|
||||||
return PayPro.RootCerts.getTrusted(pem);
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
// Verify Signature
|
// Verify Signature
|
||||||
var verified = pr.verify();
|
var trust = pr.verify(true);
|
||||||
|
|
||||||
if (!verified) {
|
if (!trust.verified) {
|
||||||
return cb(new Error('Server sent a bad signature.'));
|
return cb(new Error('Server sent a bad signature.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
var ca = trusted[0];
|
|
||||||
|
|
||||||
details = PayPro.PaymentDetails.decode(details);
|
details = PayPro.PaymentDetails.decode(details);
|
||||||
var pd = new PayPro();
|
var pd = new PayPro();
|
||||||
pd = pd.makePaymentDetails(details);
|
pd = pd.makePaymentDetails(details);
|
||||||
|
|
@ -1338,8 +1330,9 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
||||||
merchant_data: merchant_data.toString('hex')
|
merchant_data: merchant_data.toString('hex')
|
||||||
},
|
},
|
||||||
signature: sig.toString('hex'),
|
signature: sig.toString('hex'),
|
||||||
ca: ca,
|
ca: trust.caName,
|
||||||
untrusted: !ca
|
untrusted: !trust.caTrusted,
|
||||||
|
selfSigned: trust.selfSigned
|
||||||
},
|
},
|
||||||
request_url: options.uri,
|
request_url: options.uri,
|
||||||
total: bignum('0', 10).toString(10),
|
total: bignum('0', 10).toString(10),
|
||||||
|
|
@ -1703,7 +1696,8 @@ Wallet.prototype.verifyPaymentRequest = function(ntxid) {
|
||||||
pr = pr.makePaymentRequest(data);
|
pr = pr.makePaymentRequest(data);
|
||||||
|
|
||||||
// Verify the signature so we know this is the real request.
|
// Verify the signature so we know this is the real request.
|
||||||
if (!pr.verify()) {
|
var trust = pr.verify(true);
|
||||||
|
if (!trust.verified) {
|
||||||
// Signature does not match cert. It may have
|
// Signature does not match cert. It may have
|
||||||
// been modified by an untrustworthy person.
|
// been modified by an untrustworthy person.
|
||||||
// We should not sign this transaction proposal!
|
// We should not sign this transaction proposal!
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
bitcore-0.1.35-paypro.js
|
|
||||||
44166
lib/bitcore.js
Normal file
44166
lib/bitcore.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -42,7 +42,6 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"async": "0.9.0",
|
"async": "0.9.0",
|
||||||
"bitcore": "0.1.35",
|
|
||||||
"blanket": "1.1.6",
|
"blanket": "1.1.6",
|
||||||
"browser-pack": "2.0.1",
|
"browser-pack": "2.0.1",
|
||||||
"browser-request": "0.3.2",
|
"browser-request": "0.3.2",
|
||||||
|
|
@ -63,6 +62,7 @@
|
||||||
"grunt-contrib-uglify": "^0.5.1",
|
"grunt-contrib-uglify": "^0.5.1",
|
||||||
"grunt-contrib-watch": "0.5.3",
|
"grunt-contrib-watch": "0.5.3",
|
||||||
"grunt-markdown": "0.5.0",
|
"grunt-markdown": "0.5.0",
|
||||||
|
"bitcore": "0.1.36",
|
||||||
"grunt-mocha-test": "0.8.2",
|
"grunt-mocha-test": "0.8.2",
|
||||||
"grunt-shell": "0.6.4",
|
"grunt-shell": "0.6.4",
|
||||||
"grunt-angular-gettext": "^0.2.15",
|
"grunt-angular-gettext": "^0.2.15",
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,28 @@
|
||||||
<div ng-show="txs.length != 0" class="large-12 line-dashed" style="padding: 0;"></div>
|
<div ng-show="txs.length != 0" class="large-12 line-dashed" style="padding: 0;"></div>
|
||||||
|
|
||||||
<h1>{{title|translate}}</h1>
|
<h1>{{title|translate}}</h1>
|
||||||
<div class="row collapse m0">
|
|
||||||
<div class="large-6 columns">
|
<div class="large-6 columns">
|
||||||
<form name="sendForm" ng-submit="submitForm(sendForm)" novalidate>
|
<form name="sendForm" ng-submit="submitForm(sendForm)" novalidate>
|
||||||
<div class="row collapse">
|
<div class="row">
|
||||||
<div class="large-12 columns">
|
<div class="large-12 columns">
|
||||||
<div class="row collapse">
|
<div class="row collapse">
|
||||||
<label for="address"><span translate>To address</span>
|
<label for="address">To address
|
||||||
<small translate ng-hide="!sendForm.address.$pristine || address">required</small>
|
<small ng-hide="!sendForm.address.$pristine || address">required</small>
|
||||||
<small translate class="is-valid" ng-show="!sendForm.address.$invalid && address">valid!</small>
|
<small class="is-valid" ng-show="!sendForm.address.$invalid && address">valid!</small>
|
||||||
<small translate class="has-error" ng-show="sendForm.address.$invalid && address">
|
<small class="has-error" ng-show="sendForm.address.$invalid && address">
|
||||||
not valid</small>
|
not valid</small>
|
||||||
</label>
|
</label>
|
||||||
<div class="small-10 columns">
|
<div class="small-10 columns">
|
||||||
<input type="text" id="address" name="address" ng-disabled="loading"
|
<input type="text" id="address" name="address" ng-disabled="loading || !!$root.merchant"
|
||||||
placeholder="{{'Send to'|translate}}" ng-model="address" ng-change="onChanged()" valid-address required>
|
placeholder="Send to" ng-model="address" ng-change="onChanged()" valid-address required>
|
||||||
<small class="icon-input" ng-show="!sendForm.address.$invalid && address"><i class="fi-check"></i></small>
|
<small class="icon-input" ng-show="!sendForm.address.$invalid && address"><i class="fi-check"></i></small>
|
||||||
<small class="icon-input" ng-show="sendForm.address.$invalid && address"><i class="fi-x"></i></small>
|
<small class="icon-input" ng-show="sendForm.address.$invalid && address"><i class="fi-x"></i></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-2 columns" ng-hide="disableScanner" ng-hide="showScanner">
|
<div class="small-2 columns" ng-hide="showScanner">
|
||||||
<a class="postfix button black" ng-click="openScanner()"><i class="fi-camera"></i></a>
|
<a class="postfix button black" ng-click="openScanner()"><i class="fi-camera"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-2 columns" ng-show="showScanner">
|
<div class="small-2 columns" ng-show="showScanner">
|
||||||
<a translate class="postfix button warning" ng-click="cancelScanner()">Cancel</a>
|
<a class="postfix button warning" ng-click="cancelScanner()">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="scanner" class="row" ng-if="showScanner">
|
<div id="scanner" class="row" ng-if="showScanner">
|
||||||
|
|
@ -53,30 +52,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row collapse">
|
<div class="row">
|
||||||
<div class="large-5 medium-5 columns">
|
<div class="large-6 medium-6 columns">
|
||||||
<div class="row collapse">
|
<div class="row collapse">
|
||||||
<label for="amount"><span translate>Amount</span>
|
<label for="amount">Amount
|
||||||
<small translate ng-hide="!sendForm.amount.$pristine">required</small>
|
<small ng-hide="!sendForm.amount.$pristine">required</small>
|
||||||
<small translate class="is-valid" ng-show="!sendForm.amount.$invalid && !sendForm.amount.$pristine">Valid</small>
|
<small class="is-valid" ng-show="!sendForm.amount.$invalid && !sendForm.amount.$pristine">Valid</small>
|
||||||
<small translate class="has-error" ng-show="sendForm.amount.$invalid && !sendForm.amount.$pristine && !notEnoughAmount">
|
<small class="has-error" ng-show="sendForm.amount.$invalid && !sendForm.amount.$pristine && !notEnoughAmount">
|
||||||
Not valid
|
Not valid
|
||||||
</small>
|
</small>
|
||||||
<small translate ng-show="notEnoughAmount" class="has-error">Insufficient funds</small>
|
<small ng-show="notEnoughAmount" class="has-error">Insufficient funds</small>
|
||||||
</label>
|
</label>
|
||||||
<div class="small-9 columns">
|
<div class="small-9 columns">
|
||||||
<input type="number" id="amount"
|
<input type="number" id="amount"
|
||||||
ng-disabled="loading || ($root.merchant && +$root.merchant.total > 0) || $root.merchantError"
|
ng-disabled="loading || ($root.merchant && +$root.merchant.total > 0) || $root.merchantError"
|
||||||
name="amount" placeholder="{{'Amount'|translate}}" ng-model="amount"
|
name="amount" placeholder="Amount" ng-model="amount"
|
||||||
min="{{minAmount}}" max="10000000000" enough-amount required
|
min="{{minAmount}}" max="10000000000" enough-amount required
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
<small class="icon-input" ng-show="!sendForm.amount.$invalid && amount"><i class="fi-check"></i></small>
|
<small class="icon-input" ng-show="!sendForm.amount.$invalid && amount"><i class="fi-check"></i></small>
|
||||||
<small class="icon-input" ng-show="sendForm.amount.$invalid && !sendForm.amount.$pristine && !notEnoughAmount"><i class="fi-x"></i></small>
|
<small class="icon-input" ng-show="sendForm.amount.$invalid && !sendForm.amount.$pristine && !notEnoughAmount"><i class="fi-x"></i></small>
|
||||||
<a class="small input-note" title="{{'Send all funds'|translate}}"
|
<a class="small input-note" title="Send all funds"
|
||||||
ng-show="$root.availableBalance > 0 && (!$root.merchant || +$root.merchant.total === 0)"
|
ng-show="$root.availableBalance > 0 && (!$root.merchant || +$root.merchant.total === 0)"
|
||||||
ng-click="topAmount(sendForm)">
|
ng-click="topAmount(sendForm)">
|
||||||
<span translate>Use all funds</span> ({{getAvailableAmount()}} {{$root.unitName}})
|
Use all funds ({{getAvailableAmount()}} {{$root.unitName}})
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-3 columns">
|
<div class="small-3 columns">
|
||||||
|
|
@ -86,11 +85,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="large-6 medium-6 columns">
|
<div class="large-6 medium-6 columns">
|
||||||
<div class="row collapse">
|
<div class="row collapse">
|
||||||
<label for="alternative"><span translate>Amount in</span> {{ alternativeName }} </label>
|
<label for="alternative">Amount in {{ alternativeName }} </label>
|
||||||
<div class="small-9 columns">
|
<div class="small-9 columns">
|
||||||
<input type="number" id="alternative_amount"
|
<input type="number" id="alternative_amount"
|
||||||
ng-disabled="loading || !isRateAvailable "
|
ng-disabled="loading || !isRateAvailable || ($root.merchant && +$root.merchant.total > 0) || $root.merchantError"
|
||||||
name="alternative" placeholder="{{'Amount'|translate}}" ng-model="alternative"
|
name="alternative" placeholder="Amount" ng-model="alternative"
|
||||||
required
|
required
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
|
|
@ -102,37 +101,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row collapse" ng-show="wallet.isShared()">
|
<div class="row" ng-show="wallet.isShared()">
|
||||||
<div class="large-12 columns">
|
<div class="large-12 columns">
|
||||||
<div class="row collapse">
|
<div class="row collapse">
|
||||||
<label for="comment"><span translate>Notes</span>
|
<label for="comment">Note
|
||||||
<small translate ng-hide="!sendForm.comment.$pristine">optional</small>
|
<small ng-hide="!sendForm.comment.$pristine">optional</small>
|
||||||
<small translate class="has-error" ng-show="sendForm.comment.$invalid && !sendForm.comment.$pristine">too long!</small>
|
<small class="has-error" ng-show="sendForm.comment.$invalid && !sendForm.comment.$pristine">too long!</small>
|
||||||
</label>
|
</label>
|
||||||
<div class="large-12 columns">
|
<div class="large-12 columns">
|
||||||
<textarea id="comment" ng-disabled="loading"
|
<textarea id="comment" ng-disabled="loading"
|
||||||
name="comment" placeholder="{{'Leave a private message to your copayers'|translate}}" ng-model="commentText" ng-maxlength="100"></textarea>
|
name="comment" placeholder="Leave a private message to your copayers" ng-model="commentText" ng-maxlength="100"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row collapse">
|
<div class="row">
|
||||||
<div class="large-5 medium-6 small-12 columns">
|
<div class="large-5 medium-3 small-4 columns">
|
||||||
<button translate type="submit" class="button primary expand text-center" ng-disabled="sendForm.$invalid || loading" loading="Sending">
|
<button type="submit" class="button primary expand text-center" ng-disabled="sendForm.$invalid || loading" loading="Sending">
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- end of row -->
|
|
||||||
|
|
||||||
<div class="large-6 columns show-for-large-up" ng-show="!!$root.merchant">
|
<div class="large-6 columns show-for-large-up" ng-show="!!$root.merchant">
|
||||||
<div class="send-note">
|
<div class="send-note">
|
||||||
<h6>Send to</h6>
|
<h6>Send to</h6>
|
||||||
<p class="text-gray" ng-class="{'hidden': sendForm.address.$invalid || !address}">
|
<p class="text-gray" ng-class="{'hidden': sendForm.address.$invalid || !address}"
|
||||||
{{address}}
|
title="{{$root.merchant.request_url}}">
|
||||||
|
{{$root.merchant.domain}}
|
||||||
</p>
|
</p>
|
||||||
<h6>Total amount for this transaction:</h6>
|
<h6>Total amount for this transaction:</h6>
|
||||||
<p class="text-gray" ng-class="{'hidden': sendForm.amount.$invalid || !amount > 0}">
|
<p class="text-gray" ng-class="{'hidden': sendForm.amount.$invalid || !amount > 0}">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue