mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-20 02:54:37 -04:00
Authorize.net: New payment flow (#68)
* fixes for validation errors * authorize.net * pass livewirePaymentView & processPaymentView thru base driver * add paymentData to the interface * authorize.net credit card
This commit is contained in:
parent
364a57c857
commit
e7f41c1dba
@ -21,6 +21,7 @@ use App\Models\PaymentHash;
|
|||||||
use App\Models\PaymentType;
|
use App\Models\PaymentType;
|
||||||
use App\Models\SystemLog;
|
use App\Models\SystemLog;
|
||||||
use App\PaymentDrivers\AuthorizePaymentDriver;
|
use App\PaymentDrivers\AuthorizePaymentDriver;
|
||||||
|
use App\PaymentDrivers\Common\LivewireMethodInterface;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use net\authorize\api\contract\v1\DeleteCustomerPaymentProfileRequest;
|
use net\authorize\api\contract\v1\DeleteCustomerPaymentProfileRequest;
|
||||||
use net\authorize\api\contract\v1\DeleteCustomerProfileRequest;
|
use net\authorize\api\contract\v1\DeleteCustomerProfileRequest;
|
||||||
@ -30,7 +31,7 @@ use net\authorize\api\controller\DeleteCustomerProfileController;
|
|||||||
/**
|
/**
|
||||||
* Class AuthorizeCreditCard.
|
* Class AuthorizeCreditCard.
|
||||||
*/
|
*/
|
||||||
class AuthorizeCreditCard
|
class AuthorizeCreditCard implements LivewireMethodInterface
|
||||||
{
|
{
|
||||||
use MakesHash;
|
use MakesHash;
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ class AuthorizeCreditCard
|
|||||||
$this->authorize = $authorize;
|
$this->authorize = $authorize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processPaymentView($data)
|
public function paymentData(array $data): array
|
||||||
{
|
{
|
||||||
$tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id)
|
$tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id)
|
||||||
->where('company_gateway_id', $this->authorize->company_gateway->id)
|
->where('company_gateway_id', $this->authorize->company_gateway->id)
|
||||||
@ -54,6 +55,13 @@ class AuthorizeCreditCard
|
|||||||
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
|
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
|
||||||
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
|
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processPaymentView($data)
|
||||||
|
{
|
||||||
|
$data = $this->paymentData($data);
|
||||||
|
|
||||||
return render('gateways.authorize.credit_card.pay', $data);
|
return render('gateways.authorize.credit_card.pay', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,4 +321,9 @@ class AuthorizeCreditCard
|
|||||||
'invoices' => $vars['invoices'],
|
'invoices' => $vars['invoices'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function livewirePaymentView(array $data): string
|
||||||
|
{
|
||||||
|
return 'gateways.authorize.credit_card.pay_livewire';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -870,4 +870,14 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function livewirePaymentView(array $data): string
|
||||||
|
{
|
||||||
|
return $this->payment_method->livewirePaymentView($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processPaymentViewData(array $data): array
|
||||||
|
{
|
||||||
|
return $this->payment_method->paymentData($data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,6 @@ class RFFService
|
|||||||
if ($return_errors) {
|
if ($return_errors) {
|
||||||
return $validator->getMessageBag()->getMessages();
|
return $validator->getMessageBag()->getMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('validation_errors', $validator->getMessageBag()->getMessages());
|
session()->flash('validation_errors', $validator->getMessageBag()->getMessages());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "invoiceninja",
|
"name": "@invoiceninja/invoiceninja",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
12
public/build/assets/authorize-credit-card-payment-5206050e.js
vendored
Normal file
12
public/build/assets/authorize-credit-card-payment-5206050e.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +0,0 @@
|
|||||||
var l=Object.defineProperty;var c=(d,e,t)=>e in d?l(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var o=(d,e,t)=>(c(d,typeof e!="symbol"?e+"":e,t),t);/**
|
|
||||||
* Invoice Ninja (https://invoiceninja.com)
|
|
||||||
*
|
|
||||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
|
||||||
*
|
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
|
||||||
*/class s{constructor(e,t){o(this,"handleAuthorization",()=>{if(m=="1"&&document.getElementById("cvv").value.length<3){var e=$("#errors");e.show().html("<p>CVV is required</p>"),document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden");return}var t=$("#my-card"),n={};n.clientKey=this.publicKey,n.apiLoginID=this.loginId;var a={};a.cardNumber=t.CardJs("cardNumber").replace(/[^\d]/g,""),a.month=t.CardJs("expiryMonth").replace(/[^\d]/g,""),a.year=t.CardJs("expiryYear").replace(/[^\d]/g,""),a.cardCode=document.getElementById("cvv").value.replace(/[^\d]/g,"");var r={};return r.authData=n,r.cardData=a,document.getElementById("pay-now")&&(document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden")),Accept.dispatchData(r,this.responseHandler),!1});o(this,"responseHandler",e=>{if(e.messages.resultCode==="Error"){var t=0,n=$("#errors");n.show().html("<p>"+e.messages.message[t].code+": "+e.messages.message[t].text+"</p>"),document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}else if(e.messages.resultCode==="Ok"){document.getElementById("dataDescriptor").value=e.opaqueData.dataDescriptor,document.getElementById("dataValue").value=e.opaqueData.dataValue;let a=document.querySelector("input[name=token-billing-checkbox]:checked");a&&(document.getElementById("store_card").value=a.value),document.getElementById("server_response").submit()}return!1});o(this,"handle",()=>{Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(n=>n.addEventListener("click",a=>{document.getElementById("save-card--container").style.display="none",document.getElementById("authorize--credit-card-container").style.display="none",document.getElementById("token").value=a.target.dataset.token}));let e=document.getElementById("toggle-payment-with-credit-card");e&&e.addEventListener("click",()=>{document.getElementById("save-card--container").style.display="grid",document.getElementById("authorize--credit-card-container").style.display="flex",document.getElementById("token").value=null});let t=document.getElementById("pay-now");return t&&t.addEventListener("click",n=>{let a=document.getElementById("token");a.value?this.handlePayNowAction(a.value):this.handleAuthorization()}),this});this.publicKey=e,this.loginId=t,this.cardHolderName=document.getElementById("cardholder_name")}handlePayNowAction(e){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),document.getElementById("token").value=e,document.getElementById("server_response").submit()}}const u=document.querySelector('meta[name="authorize-public-key"]').content,i=document.querySelector('meta[name="authorize-login-id"]').content,m=document.querySelector('meta[name="authnet-require-cvv"]').content;new s(u,i).handle();
|
|
@ -67,7 +67,10 @@
|
|||||||
"src": "resources/js/clients/payment_methods/wepay-bank-account.js"
|
"src": "resources/js/clients/payment_methods/wepay-bank-account.js"
|
||||||
},
|
},
|
||||||
"resources/js/clients/payments/authorize-credit-card-payment.js": {
|
"resources/js/clients/payments/authorize-credit-card-payment.js": {
|
||||||
"file": "assets/authorize-credit-card-payment-a217579b.js",
|
"file": "assets/authorize-credit-card-payment-5206050e.js",
|
||||||
|
"imports": [
|
||||||
|
"_wait-8f4ae121.js"
|
||||||
|
],
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/js/clients/payments/authorize-credit-card-payment.js"
|
"src": "resources/js/clients/payments/authorize-credit-card-payment.js"
|
||||||
},
|
},
|
||||||
|
@ -5,40 +5,65 @@
|
|||||||
*
|
*
|
||||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
*
|
*
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AuthorizeAuthorizeCard {
|
import { SimpleCard } from '@invoiceninja/simple-card';
|
||||||
|
import { wait, instant } from '../wait';
|
||||||
|
|
||||||
|
class AuthorizeAuthorizeCard {
|
||||||
constructor(publicKey, loginId) {
|
constructor(publicKey, loginId) {
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
this.loginId = loginId;
|
this.loginId = loginId;
|
||||||
this.cardHolderName = document.getElementById("cardholder_name");
|
this.cardHolderName = document.getElementById('cardholder_name');
|
||||||
|
|
||||||
|
this.sc = new SimpleCard({
|
||||||
|
fields: {
|
||||||
|
card: {
|
||||||
|
number: '#number',
|
||||||
|
date: '#date',
|
||||||
|
cvv: '#cvv',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sc.mount();
|
||||||
|
|
||||||
|
this.cvvRequired = document.querySelector(
|
||||||
|
'meta[name="authnet-require-cvv"]'
|
||||||
|
).content;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAuthorization = () => {
|
handleAuthorization = () => {
|
||||||
|
if (
|
||||||
if (cvvRequired == "1" && document.getElementById("cvv").value.length < 3) {
|
this.cvvRequired == '1' &&
|
||||||
var $errors = $('#errors');
|
document.getElementById('cvv').value.length < 3
|
||||||
$errors.show().html("<p>CVV is required</p>");
|
) {
|
||||||
|
const $errors = document.getElementById('errors');
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
$errors.innerText = 'CVV is required';
|
||||||
|
$errors.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('pay-now').disabled = false;
|
document.getElementById('pay-now').disabled = false;
|
||||||
document.querySelector('#pay-now > svg').classList.add('hidden');
|
document.querySelector('#pay-now > svg').classList.add('hidden');
|
||||||
document.querySelector('#pay-now > span').classList.remove('hidden');
|
document
|
||||||
|
.querySelector('#pay-now > span')
|
||||||
|
.classList.remove('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var myCard = $('#my-card');
|
|
||||||
|
|
||||||
var authData = {};
|
var authData = {};
|
||||||
authData.clientKey = this.publicKey;
|
authData.clientKey = this.publicKey;
|
||||||
authData.apiLoginID = this.loginId;
|
authData.apiLoginID = this.loginId;
|
||||||
|
|
||||||
var cardData = {};
|
var cardData = {};
|
||||||
cardData.cardNumber = myCard.CardJs('cardNumber').replace(/[^\d]/g, '');
|
cardData.cardNumber = this.sc.value('number')?.replace(/[^\d]/g, '');
|
||||||
cardData.month = myCard.CardJs('expiryMonth').replace(/[^\d]/g, '');
|
cardData.month = this.sc.value('month')?.replace(/[^\d]/g, '');
|
||||||
cardData.year = myCard.CardJs('expiryYear').replace(/[^\d]/g, '');
|
cardData.year = `20${this.sc.value('year')?.replace(/[^\d]/g, '')}`;
|
||||||
cardData.cardCode = document.getElementById("cvv").value.replace(/[^\d]/g, '');
|
cardData.cardCode = this.sc.value('cvv')?.replace(/[^\d]/g, '');
|
||||||
|
|
||||||
var secureData = {};
|
var secureData = {};
|
||||||
secureData.authData = authData;
|
secureData.authData = authData;
|
||||||
@ -58,102 +83,112 @@ class AuthorizeAuthorizeCard {
|
|||||||
Accept.dispatchData(secureData, this.responseHandler);
|
Accept.dispatchData(secureData, this.responseHandler);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
handlePayNowAction(token_hashed_id) {
|
handlePayNowAction(token_hashed_id) {
|
||||||
document.getElementById('pay-now').disabled = true;
|
document.getElementById('pay-now').disabled = true;
|
||||||
document.querySelector('#pay-now > svg').classList.remove('hidden');
|
document.querySelector('#pay-now > svg').classList.remove('hidden');
|
||||||
document.querySelector('#pay-now > span').classList.add('hidden');
|
document.querySelector('#pay-now > span').classList.add('hidden');
|
||||||
|
|
||||||
document.getElementById("token").value = token_hashed_id;
|
document.getElementById('token').value = token_hashed_id;
|
||||||
document.getElementById("server_response").submit();
|
document.getElementById('server_response').submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
responseHandler = (response) => {
|
responseHandler = (response) => {
|
||||||
if (response.messages.resultCode === "Error") {
|
if (response.messages.resultCode === 'Error') {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|
||||||
var $errors = $('#errors'); // get the reference of the div
|
const $errors = document.getElementById('errors'); // get the reference of the div
|
||||||
$errors.show().html("<p>" + response.messages.message[i].code + ": " + response.messages.message[i].text + "</p>");
|
|
||||||
|
if ($errors) {
|
||||||
|
$errors.innerText = `${response.messages.message[i].code}: ${response.messages.message[i].text}`;
|
||||||
|
$errors.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('pay-now').disabled = false;
|
document.getElementById('pay-now').disabled = false;
|
||||||
document.querySelector('#pay-now > svg').classList.add('hidden');
|
document.querySelector('#pay-now > svg').classList.add('hidden');
|
||||||
document.querySelector('#pay-now > span').classList.remove('hidden');
|
document
|
||||||
} else if (response.messages.resultCode === "Ok") {
|
.querySelector('#pay-now > span')
|
||||||
|
.classList.remove('hidden');
|
||||||
|
} else if (response.messages.resultCode === 'Ok') {
|
||||||
|
document.getElementById('dataDescriptor').value =
|
||||||
|
response.opaqueData.dataDescriptor;
|
||||||
|
document.getElementById('dataValue').value =
|
||||||
|
response.opaqueData.dataValue;
|
||||||
|
|
||||||
document.getElementById("dataDescriptor").value = response.opaqueData.dataDescriptor;
|
let storeCard = document.querySelector(
|
||||||
document.getElementById("dataValue").value = response.opaqueData.dataValue;
|
'input[name=token-billing-checkbox]:checked'
|
||||||
|
);
|
||||||
let storeCard = document.querySelector('input[name=token-billing-checkbox]:checked');
|
|
||||||
|
|
||||||
if (storeCard) {
|
if (storeCard) {
|
||||||
document.getElementById("store_card").value = storeCard.value;
|
document.getElementById('store_card').value = storeCard.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("server_response").submit();
|
document.getElementById('server_response').submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
handle = () => {
|
handle = () => {
|
||||||
Array
|
Array.from(
|
||||||
.from(document.getElementsByClassName('toggle-payment-with-token'))
|
document.getElementsByClassName('toggle-payment-with-token')
|
||||||
.forEach((element) => element.addEventListener('click', (e) => {
|
).forEach((element) =>
|
||||||
document
|
element.addEventListener('click', (e) => {
|
||||||
.getElementById('save-card--container').style.display = 'none';
|
document.getElementById('save-card--container').style.display =
|
||||||
document
|
'none';
|
||||||
.getElementById('authorize--credit-card-container').style.display = 'none';
|
document.getElementById(
|
||||||
|
'authorize--credit-card-container'
|
||||||
|
).style.display = 'none';
|
||||||
|
|
||||||
document
|
document.getElementById('token').value = e.target.dataset.token;
|
||||||
.getElementById('token').value = e.target.dataset.token;
|
})
|
||||||
}));
|
);
|
||||||
|
|
||||||
let payWithCreditCardToggle = document.getElementById('toggle-payment-with-credit-card');
|
let payWithCreditCardToggle = document.getElementById(
|
||||||
|
'toggle-payment-with-credit-card'
|
||||||
|
);
|
||||||
|
|
||||||
if (payWithCreditCardToggle) {
|
if (payWithCreditCardToggle) {
|
||||||
payWithCreditCardToggle
|
payWithCreditCardToggle.addEventListener('click', () => {
|
||||||
.addEventListener('click', () => {
|
document.getElementById('save-card--container').style.display =
|
||||||
document
|
'grid';
|
||||||
.getElementById('save-card--container').style.display = 'grid';
|
document.getElementById(
|
||||||
document
|
'authorize--credit-card-container'
|
||||||
.getElementById('authorize--credit-card-container').style.display = 'flex';
|
).style.display = 'flex';
|
||||||
|
|
||||||
document
|
document.getElementById('token').value = null;
|
||||||
.getElementById('token').value = null;
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let payNowButton = document.getElementById('pay-now');
|
let payNowButton = document.getElementById('pay-now');
|
||||||
|
|
||||||
if (payNowButton) {
|
if (payNowButton) {
|
||||||
payNowButton
|
payNowButton.addEventListener('click', (e) => {
|
||||||
.addEventListener('click', (e) => {
|
let token = document.getElementById('token');
|
||||||
let token = document.getElementById('token');
|
|
||||||
|
|
||||||
token.value
|
token.value
|
||||||
? this.handlePayNowAction(token.value)
|
? this.handlePayNowAction(token.value)
|
||||||
: this.handleAuthorization();
|
: this.handleAuthorization();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = document.querySelector(
|
function boot() {
|
||||||
'meta[name="authorize-public-key"]'
|
const publicKey = document.querySelector(
|
||||||
).content;
|
'meta[name="authorize-public-key"]'
|
||||||
|
).content;
|
||||||
|
|
||||||
const loginId = document.querySelector(
|
const loginId = document.querySelector(
|
||||||
'meta[name="authorize-login-id"]'
|
'meta[name="authorize-login-id"]'
|
||||||
).content;
|
).content;
|
||||||
|
|
||||||
const cvvRequired = document.querySelector(
|
/** @handle */
|
||||||
'meta[name="authnet-require-cvv"]'
|
new AuthorizeAuthorizeCard(publicKey, loginId).handle();
|
||||||
).content;
|
}
|
||||||
|
|
||||||
|
instant() ? boot() : wait('#authorize-net-credit-card-payment').then(() => boot());
|
||||||
|
|
||||||
/** @handle */
|
|
||||||
new AuthorizeAuthorizeCard(publicKey, loginId).handle();
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@section('gateway_head')
|
@section('gateway_head')
|
||||||
<meta name="authorize-public-key" content="{{ $public_client_id }}">
|
<meta name="authorize-public-key" content="{{ $public_client_id }}">
|
||||||
<meta name="authorize-login-id" content="{{ $api_login_id }}">
|
<meta name="authorize-login-id" content="{{ $api_login_id }}">
|
||||||
|
<meta name="instant-payment" content="yes" />
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
|
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
<meta name="authnet-require-cvv" content="{{ $gateway->company_gateway->require_cvv }}">
|
<meta name="authnet-require-cvv" content="{{ $gateway->company_gateway->require_cvv }}">
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
||||||
style="display: flex!important; justify-content: center!important;" id="authorize--credit-card-container">
|
style="display: flex!important; justify-content: center!important;" id="authorize--credit-card-container">
|
||||||
<div class="card-js" id="my-card" data-capture-name="true">
|
<div class="card-js" id="my-card" data-capture-name="true">
|
||||||
<input class="name" id="cardholder_name" name="card-holders-name" placeholder="{{ ctrans('texts.name')}}">
|
<input class="input w-full" id="cardholder_name" name="card_holders_name"
|
||||||
<input class="card-number my-custom-class" id="card_number" name="card-number">
|
placeholder="{{ ctrans('texts.name')}}">
|
||||||
<input class="expiry-month" name="expiry-month" id="expiration_month" autocomplete="cc-exp-month" x-autocompletetype="cc-exp-month">
|
<input type="text" class="input w-full" id="number" placeholder="0000 0000 0000 0000">
|
||||||
<input class="expiry-year" name="expiry-year" id="expiration_year" autocomplete="cc-exp-year" x-autocompletetype="cc-exp-year">
|
<div class="flex items-center gap-2">
|
||||||
<input class="cvc" name="cvc" id="cvv">
|
<input type="text" class="input w-1/2" id="date" placeholder="MM/YY">
|
||||||
|
<input type="text" class="input w-1/2" id="cvv" placeholder="000">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="errors"></div>
|
<div id="errors"></div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user