mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-02 22:57:00 +00:00
Updates for Stripe ACH
This commit is contained in:
@@ -15,7 +15,7 @@ Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and add
|
||||
- [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure.
|
||||
- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility.
|
||||
|
||||
All Pro and Enterprise features from the hosted app are included in the source-available code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
|
||||
All Pro and Enterprise features from the hosted app are included in the source-available code. We offer a $40 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
|
||||
|
||||
#### Get social with us
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class Gateway extends StaticModel
|
||||
case 56:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'charge.refunded', 'payment_intent.payment_failed']],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded','charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded', 'charge.succeeded', 'customer.source.updated', 'setup_intent.succeeded', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']],
|
||||
GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'charge.refunded', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']],
|
||||
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
|
||||
|
||||
@@ -51,10 +51,46 @@ class ACH implements LivewireMethodInterface
|
||||
/**
|
||||
* Authorize a bank account - requires microdeposit verification
|
||||
*/
|
||||
// public function authorizeView(array $data)
|
||||
// {
|
||||
// $data['gateway'] = $this->stripe;
|
||||
|
||||
// return render('gateways.stripe.ach.authorize', array_merge($data));
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Instant Verification methods with fall back to microdeposits.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function authorizeView(array $data)
|
||||
{
|
||||
$data['gateway'] = $this->stripe;
|
||||
|
||||
|
||||
$customer = $this->stripe->findOrCreateCustomer();
|
||||
|
||||
// Create SetupIntent with Financial Connections for instant verification
|
||||
$intent = \Stripe\SetupIntent::create([
|
||||
'customer' => $customer->id,
|
||||
'usage' => 'off_session',
|
||||
'payment_method_types' => ['us_bank_account'],
|
||||
'payment_method_options' => [
|
||||
'us_bank_account' => [
|
||||
'financial_connections' => [
|
||||
'permissions' => ['payment_method'],
|
||||
// Optional: add 'balances', 'ownership' for additional data
|
||||
],
|
||||
'verification_method' => 'automatic', // instant with microdeposit fallback
|
||||
// Or use 'instant' to require instant only (no fallback)
|
||||
],
|
||||
],
|
||||
], $this->stripe->stripe_connect_auth);
|
||||
|
||||
$data['client_secret'] = $intent->client_secret;
|
||||
$data['customer'] = $customer;
|
||||
|
||||
return render('gateways.stripe.ach.authorize', array_merge($data));
|
||||
}
|
||||
|
||||
@@ -62,36 +98,96 @@ class ACH implements LivewireMethodInterface
|
||||
{
|
||||
$this->stripe->init();
|
||||
|
||||
$stripe_response = json_decode($request->input('gateway_response'));
|
||||
$setup_intent = json_decode($request->input('gateway_response'));
|
||||
|
||||
if (!$setup_intent || !isset($setup_intent->payment_method)) {
|
||||
throw new PaymentFailed('Invalid response from payment gateway.');
|
||||
}
|
||||
|
||||
$customer = $this->stripe->findOrCreateCustomer();
|
||||
|
||||
try {
|
||||
$source = Customer::createSource($customer->id, ['source' => $stripe_response->token->id], array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)]));
|
||||
// Retrieve the payment method to get bank account details
|
||||
$payment_method = $this->stripe->getStripePaymentMethod($setup_intent->payment_method);
|
||||
|
||||
if (!$payment_method || !isset($payment_method->us_bank_account)) {
|
||||
throw new PaymentFailed('Unable to retrieve bank account details.');
|
||||
}
|
||||
|
||||
$bank_account = $payment_method->us_bank_account;
|
||||
|
||||
// Determine verification state based on SetupIntent status
|
||||
/** @var string $status */
|
||||
$status = $setup_intent->status ?? 'unauthorized'; //@phpstan-ignore-line
|
||||
$state = match ($status) {
|
||||
'succeeded' => 'authorized',
|
||||
'requires_action' => 'unauthorized', // Microdeposit verification pending
|
||||
default => 'unauthorized',
|
||||
};
|
||||
|
||||
// Build a new stdClass object for storage (Stripe objects are immutable)
|
||||
$method = new \stdClass();
|
||||
$method->id = $setup_intent->payment_method; //@phpstan-ignore-line
|
||||
$method->bank_name = $bank_account->bank_name;
|
||||
$method->last4 = $bank_account->last4;
|
||||
$method->state = $state;
|
||||
|
||||
// If microdeposit verification is required, store the verification URL
|
||||
if ($status === 'requires_action' &&
|
||||
isset($setup_intent->next_action) &&
|
||||
($setup_intent->next_action->type ?? null) === 'verify_with_microdeposits') { //@phpstan-ignore-line
|
||||
$method->next_action = $setup_intent->next_action->verify_with_microdeposits->hosted_verification_url ?? null; //@phpstan-ignore-line
|
||||
}
|
||||
|
||||
// Note: We don't attach the payment method here - it's already linked to the
|
||||
// customer via the SetupIntent. For us_bank_account, the payment method must be
|
||||
// verified before it can be used. Verification happens via:
|
||||
// - Instant verification (Financial Connections) - already verified
|
||||
// - Microdeposits - verified via webhook (setup_intent.succeeded)
|
||||
|
||||
$client_gateway_token = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer);
|
||||
|
||||
// If instant verification succeeded, redirect to payment methods
|
||||
if ($state === 'authorized') {
|
||||
return redirect()->route('client.payment_methods.show', ['payment_method' => $client_gateway_token->hashed_id])
|
||||
->with('message', ctrans('texts.payment_method_added'));
|
||||
}
|
||||
|
||||
// If microdeposit verification required, send notification and redirect
|
||||
$verification = route('client.payment_methods.verification', [
|
||||
'payment_method' => $client_gateway_token->hashed_id,
|
||||
'method' => GatewayType::BANK_TRANSFER
|
||||
], false);
|
||||
|
||||
$mailer = new NinjaMailerObject();
|
||||
|
||||
$mailer->mailable = new ACHVerificationNotification(
|
||||
auth()->guard('contact')->user()->client->company,
|
||||
route('client.contact_login', [
|
||||
'contact_key' => auth()->guard('contact')->user()->contact_key,
|
||||
'next' => $verification
|
||||
])
|
||||
);
|
||||
|
||||
$mailer->company = auth()->guard('contact')->user()->client->company;
|
||||
$mailer->settings = auth()->guard('contact')->user()->client->company->settings;
|
||||
$mailer->to_user = auth()->guard('contact')->user();
|
||||
|
||||
NinjaMailerJob::dispatch($mailer);
|
||||
|
||||
return redirect()->route('client.payment_methods.verification', [
|
||||
'payment_method' => $client_gateway_token->hashed_id,
|
||||
'method' => GatewayType::BANK_TRANSFER
|
||||
]);
|
||||
|
||||
} catch (InvalidRequestException $e) {
|
||||
throw new PaymentFailed($e->getMessage(), $e->getCode());
|
||||
}
|
||||
|
||||
$client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer);
|
||||
|
||||
$verification = route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER], false);
|
||||
|
||||
$mailer = new NinjaMailerObject();
|
||||
|
||||
$mailer->mailable = new ACHVerificationNotification(
|
||||
auth()->guard('contact')->user()->client->company,
|
||||
route('client.contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'next' => $verification])
|
||||
);
|
||||
|
||||
$mailer->company = auth()->guard('contact')->user()->client->company;
|
||||
$mailer->settings = auth()->guard('contact')->user()->client->company->settings;
|
||||
$mailer->to_user = auth()->guard('contact')->user();
|
||||
|
||||
NinjaMailerJob::dispatch($mailer);
|
||||
|
||||
return redirect()->route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle customer.source.updated webhook (legacy Sources API)
|
||||
*/
|
||||
public function updateBankAccount(array $event)
|
||||
{
|
||||
$stripe_event = $event['data']['object'];
|
||||
@@ -108,6 +204,57 @@ class ACH implements LivewireMethodInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle setup_intent.succeeded webhook (new SetupIntent/Financial Connections flow)
|
||||
*
|
||||
* This is called when microdeposit verification is completed for us_bank_account payment methods.
|
||||
*/
|
||||
public function handleSetupIntentSucceeded(array $event): void
|
||||
{
|
||||
$setup_intent = $event['data']['object'];
|
||||
|
||||
// Only handle us_bank_account payment methods
|
||||
if (!isset($setup_intent['payment_method']) || !isset($setup_intent['payment_method_types'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array('us_bank_account', $setup_intent['payment_method_types'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_method_id = $setup_intent['payment_method'];
|
||||
$customer_id = $setup_intent['customer'] ?? null;
|
||||
|
||||
if (!$payment_method_id || !$customer_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the token by payment method ID
|
||||
$token = ClientGatewayToken::query()
|
||||
->where('token', $payment_method_id)
|
||||
->where('gateway_customer_reference', $customer_id)
|
||||
->first();
|
||||
|
||||
if (!$token) {
|
||||
nlog("ACH SetupIntent succeeded but no matching token found for payment_method: {$payment_method_id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the token state to authorized
|
||||
$meta = $token->meta;
|
||||
$meta->state = 'authorized';
|
||||
|
||||
// Clear the next_action since verification is complete
|
||||
if (isset($meta->next_action)) {
|
||||
unset($meta->next_action);
|
||||
}
|
||||
|
||||
$token->meta = $meta;
|
||||
$token->save();
|
||||
|
||||
nlog("ACH bank account verified via SetupIntent webhook: {$payment_method_id}");
|
||||
}
|
||||
|
||||
public function verificationView(ClientGatewayToken $token)
|
||||
{
|
||||
|
||||
@@ -379,12 +526,16 @@ class ACH implements LivewireMethodInterface
|
||||
$response = json_decode($request->gateway_response);
|
||||
$bank_account_response = json_decode($request->bank_account_response);
|
||||
|
||||
if ($response->status == 'requires_source_action' && $response->next_action->type == 'verify_with_microdeposits') {
|
||||
$method = $bank_account_response->payment_method->us_bank_account;
|
||||
$method = $bank_account_response->payment_method->us_bank_account;
|
||||
if (in_array($response->status,['requires_action','requires_source_action']) && ($response->next_action->type ?? null) == 'verify_with_microdeposits') {
|
||||
$method = $bank_account_response->payment_method->us_bank_account ?? null;
|
||||
|
||||
if (!$method) {
|
||||
throw new PaymentFailed('Unable to retrieve bank account details');
|
||||
}
|
||||
|
||||
$method->id = $response->payment_method;
|
||||
$method->state = 'unauthorized';
|
||||
$method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url;
|
||||
$method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url ?? null;
|
||||
|
||||
$customer = $this->stripe->getCustomer($request->customer);
|
||||
$cgt = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer);
|
||||
|
||||
@@ -727,6 +727,12 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac
|
||||
$ach->updateBankAccount($request->all());
|
||||
}
|
||||
|
||||
// Handle SetupIntent succeeded for ACH microdeposit verification
|
||||
if ($request->type === 'setup_intent.succeeded') {
|
||||
$ach = new ACH($this);
|
||||
$ach->handleSetupIntentSucceeded($request->all());
|
||||
}
|
||||
|
||||
if ($request->type === 'payment_intent.processing') {
|
||||
PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
|
||||
return response()->json([], 200);
|
||||
|
||||
File diff suppressed because one or more lines are too long
9
public/build/assets/stripe-ach-1f0bff4a.js
vendored
Normal file
9
public/build/assets/stripe-ach-1f0bff4a.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
var i=Object.defineProperty;var u=(n,e,t)=>e in n?i(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var s=(n,e,t)=>(u(n,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 d{constructor(){s(this,"setupStripe",()=>(this.stripe_connect?this.stripe=Stripe(this.key,{stripeAccount:this.stripe_connect}):this.stripe=Stripe(this.key),this));s(this,"getFormData",()=>{var e;return{account_holder_name:document.getElementById("account-holder-name").value,account_holder_type:document.querySelector('input[name="account-holder-type"]:checked').value,email:((e=document.querySelector('meta[name="contact-email"]'))==null?void 0:e.content)||""}});s(this,"handleError",e=>{document.getElementById("save-button").disabled=!1,document.querySelector("#save-button > svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),this.errors.textContent="",this.errors.textContent=e,this.errors.hidden=!1});s(this,"handleSuccess",e=>{document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()});s(this,"handleSubmit",async e=>{if(e.preventDefault(),!document.getElementById("accept-terms").checked){this.errors.textContent="You must accept the mandate terms prior to adding this payment method.",this.errors.hidden=!1;return}document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),this.errors.textContent="",this.errors.hidden=!0;const t=this.getFormData();try{const{setupIntent:r,error:c}=await this.stripe.collectBankAccountForSetup({clientSecret:this.clientSecret,params:{payment_method_type:"us_bank_account",payment_method_data:{billing_details:{name:t.account_holder_name,email:t.email}}}});if(c)return this.handleError(c.message);if(r.status==="requires_payment_method")return this.handleError("Please complete the bank account verification process.");if(r.status==="requires_confirmation"){const{setupIntent:a,error:o}=await this.stripe.confirmUsBankAccountSetup(this.clientSecret);return o?this.handleError(o.message):this.handleSuccess(a)}return r.status==="requires_action"?this.handleSuccess(r):r.status==="succeeded"?this.handleSuccess(r):this.handleSuccess(r)}catch(r){return this.handleError(r.message||"An unexpected error occurred.")}});var e,t;this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=(e=document.querySelector('meta[name="stripe-account-id"]'))==null?void 0:e.content,this.clientSecret=(t=document.querySelector('meta[name="stripe-client-secret"]'))==null?void 0:t.content}handle(){document.getElementById("save-button").addEventListener("click",e=>this.handleSubmit(e))}}new d().setupStripe().handle();
|
||||
9
public/build/assets/stripe-ach-fe366ca7.js
vendored
9
public/build/assets/stripe-ach-fe366ca7.js
vendored
@@ -1,9 +0,0 @@
|
||||
var s=Object.defineProperty;var o=(n,e,t)=>e in n?s(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var r=(n,e,t)=>(o(n,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 c{constructor(){r(this,"setupStripe",()=>(this.stripeConnect?this.stripe=Stripe(this.key,{stripeAccount:this.stripeConnect}):this.stripe=Stripe(this.key),this));r(this,"getFormData",()=>({country:document.getElementById("country").value,currency:document.getElementById("currency").value,routing_number:document.getElementById("routing-number").value,account_number:document.getElementById("account-number").value,account_holder_name:document.getElementById("account-holder-name").value,account_holder_type:document.querySelector('input[name="account-holder-type"]:checked').value}));r(this,"handleError",e=>{document.getElementById("save-button").disabled=!1,document.querySelector("#save-button > svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),this.errors.textContent="",this.errors.textContent=e,this.errors.hidden=!1});r(this,"handleSuccess",e=>{document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()});r(this,"handleSubmit",e=>{if(!document.getElementById("accept-terms").checked){errors.textContent="You must accept the mandate terms prior to making payment.",errors.hidden=!1;return}document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),this.errors.textContent="",this.errors.hidden=!0,this.stripe.createToken("bank_account",this.getFormData()).then(t=>t.hasOwnProperty("error")?this.handleError(t.error.message):this.handleSuccess(t))});var e;this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=(e=document.querySelector('meta[name="stripe-account-id"]'))==null?void 0:e.content}handle(){document.getElementById("save-button").addEventListener("click",e=>this.handleSubmit(e))}}new c().setupStripe().handle();
|
||||
@@ -12,7 +12,7 @@
|
||||
"file": "assets/wait-8f4ae121.js"
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-8ade2cf8.js",
|
||||
"file": "assets/app-03a29a9d.js",
|
||||
"imports": [
|
||||
"_index-08e160a7.js",
|
||||
"__commonjsHelpers-725317a4.js"
|
||||
@@ -203,7 +203,7 @@
|
||||
"src": "resources/js/clients/payments/stripe-ach-pay.js"
|
||||
},
|
||||
"resources/js/clients/payments/stripe-ach.js": {
|
||||
"file": "assets/stripe-ach-fe366ca7.js",
|
||||
"file": "assets/stripe-ach-1f0bff4a.js",
|
||||
"isEntry": true,
|
||||
"src": "resources/js/clients/payments/stripe-ach.js"
|
||||
},
|
||||
|
||||
104
resources/js/clients/payments/stripe-ach.js
vendored
104
resources/js/clients/payments/stripe-ach.js
vendored
@@ -17,36 +17,30 @@ class AuthorizeACH {
|
||||
this.stripe_connect = document.querySelector(
|
||||
'meta[name="stripe-account-id"]'
|
||||
)?.content;
|
||||
this.clientSecret = document.querySelector(
|
||||
'meta[name="stripe-client-secret"]'
|
||||
)?.content;
|
||||
}
|
||||
|
||||
setupStripe = () => {
|
||||
|
||||
if (this.stripeConnect){
|
||||
|
||||
this.stripe = Stripe(this.key, {
|
||||
stripeAccount: this.stripeConnect,
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
if (this.stripe_connect) {
|
||||
this.stripe = Stripe(this.key, {
|
||||
stripeAccount: this.stripe_connect,
|
||||
});
|
||||
} else {
|
||||
this.stripe = Stripe(this.key);
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
getFormData = () => {
|
||||
return {
|
||||
country: document.getElementById('country').value,
|
||||
currency: document.getElementById('currency').value,
|
||||
routing_number: document.getElementById('routing-number').value,
|
||||
account_number: document.getElementById('account-number').value,
|
||||
account_holder_name: document.getElementById('account-holder-name')
|
||||
.value,
|
||||
account_holder_name: document.getElementById('account-holder-name').value,
|
||||
account_holder_type: document.querySelector(
|
||||
'input[name="account-holder-type"]:checked'
|
||||
).value,
|
||||
email: document.querySelector('meta[name="contact-email"]')?.content || '',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,40 +54,82 @@ class AuthorizeACH {
|
||||
this.errors.hidden = false;
|
||||
};
|
||||
|
||||
handleSuccess = (response) => {
|
||||
document.getElementById('gateway_response').value = JSON.stringify(
|
||||
response
|
||||
);
|
||||
|
||||
handleSuccess = (setupIntent) => {
|
||||
document.getElementById('gateway_response').value = JSON.stringify(setupIntent);
|
||||
document.getElementById('server_response').submit();
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!document.getElementById('accept-terms').checked) {
|
||||
errors.textContent = "You must accept the mandate terms prior to making payment.";
|
||||
errors.hidden = false;
|
||||
return;
|
||||
this.errors.textContent = "You must accept the mandate terms prior to adding this payment method.";
|
||||
this.errors.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('save-button').disabled = true;
|
||||
document.querySelector('#save-button > svg').classList.remove('hidden');
|
||||
document.querySelector('#save-button > span').classList.add('hidden');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.errors.textContent = '';
|
||||
this.errors.hidden = true;
|
||||
|
||||
this.stripe
|
||||
.createToken('bank_account', this.getFormData())
|
||||
.then((result) => {
|
||||
if (result.hasOwnProperty('error')) {
|
||||
return this.handleError(result.error.message);
|
||||
const formData = this.getFormData();
|
||||
|
||||
try {
|
||||
// Step 1: Collect bank account using Financial Connections
|
||||
const { setupIntent, error } = await this.stripe.collectBankAccountForSetup({
|
||||
clientSecret: this.clientSecret,
|
||||
params: {
|
||||
payment_method_type: 'us_bank_account',
|
||||
payment_method_data: {
|
||||
billing_details: {
|
||||
name: formData.account_holder_name,
|
||||
email: formData.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return this.handleError(error.message);
|
||||
}
|
||||
|
||||
// Check the SetupIntent status
|
||||
if (setupIntent.status === 'requires_payment_method') {
|
||||
// Customer closed the modal without completing - show error
|
||||
return this.handleError('Please complete the bank account verification process.');
|
||||
}
|
||||
|
||||
if (setupIntent.status === 'requires_confirmation') {
|
||||
// User completed Financial Connections, now confirm the SetupIntent
|
||||
const { setupIntent: confirmedSetupIntent, error: confirmError } =
|
||||
await this.stripe.confirmUsBankAccountSetup(this.clientSecret);
|
||||
|
||||
if (confirmError) {
|
||||
return this.handleError(confirmError.message);
|
||||
}
|
||||
|
||||
return this.handleSuccess(result);
|
||||
});
|
||||
return this.handleSuccess(confirmedSetupIntent);
|
||||
}
|
||||
|
||||
if (setupIntent.status === 'requires_action') {
|
||||
// Microdeposit verification required - redirect to verification
|
||||
return this.handleSuccess(setupIntent);
|
||||
}
|
||||
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
// Instant verification succeeded
|
||||
return this.handleSuccess(setupIntent);
|
||||
}
|
||||
|
||||
// Handle any other status
|
||||
return this.handleSuccess(setupIntent);
|
||||
|
||||
} catch (err) {
|
||||
return this.handleError(err.message || 'An unexpected error occurred.');
|
||||
}
|
||||
};
|
||||
|
||||
handle() {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
@else
|
||||
<meta name="stripe-publishable-key" content="{{ $gateway->company_gateway->getPublishableKey() }}">
|
||||
@endif
|
||||
<meta name="stripe-client-secret" content="{{ $client_secret }}">
|
||||
<meta name="contact-email" content="{{ auth()->guard('contact')->user()->email }}">
|
||||
@endsection
|
||||
|
||||
@section('gateway_content')
|
||||
@@ -27,10 +29,6 @@
|
||||
|
||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||
|
||||
<div class="alert alert-warning mb-4">
|
||||
<h2>Adding a bank account here requires verification, which may take several days. In order to use Instant Verification please pay an invoice first, this process will automatically verify your bank account.</h2>
|
||||
</div>
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')])
|
||||
<span class="flex items-center mr-4">
|
||||
<input class="form-radio mr-2" type="radio" value="individual" name="account-holder-type" checked>
|
||||
@@ -46,40 +44,6 @@
|
||||
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}" required value="{{ auth()->guard('contact')->user()->client->present()->name() }}">
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')])
|
||||
<select name="countries" id="country" class="form-select input w-full bg-white">
|
||||
<option disabled selected></option>
|
||||
@foreach($countries as $country)
|
||||
@if($country->iso_3166_2 == 'US')
|
||||
<option value="{{ $country->iso_3166_2 }}" selected>{{ $country->iso_3166_2 }} ({{ $country->getName() }})</option>
|
||||
@else
|
||||
<option value="{{ $country->iso_3166_2 }}">{{ $country->iso_3166_2 }} ({{ $country->getName() }})</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.currency')])
|
||||
<select name="currencies" id="currency" class="form-select input w-full">
|
||||
<option disabled selected></option>
|
||||
@foreach($currencies as $currency)
|
||||
@if($currency->code == 'USD')
|
||||
<option value="{{ $currency->code }}" selected>{{ $currency->code }} ({{ $currency->getName() }})</option>
|
||||
@else
|
||||
<option value="{{ $currency->code }}">{{ $currency->code }} ({{ $currency->name }})</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')])
|
||||
<input class="input w-full" id="routing-number" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')])
|
||||
<input class="input w-full" id="account-number" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element-single')
|
||||
<input type="checkbox" class="form-checkbox mr-1" id="accept-terms" required>
|
||||
<label for="accept-terms" class="cursor-pointer">{{ ctrans('texts.ach_authorization', ['company' => auth()->guard('contact')->user()->company->present()->name, 'email' => auth()->guard('contact')->user()->client->company->settings->email]) }}</label>
|
||||
|
||||
Reference in New Issue
Block a user