This example is extended version of ACH Payment Method with Plaid
adding support for 0Auth user verification using returnUrl
parameter in our API.
After successful user authorization Plaid can redirect user to the page determined by returnUrl
parameter.
Example of this return page can be found at this localization.
Details of 0Auth and when redirect will happen can be found in official Plaid Link 0Auth guide
Possible verification scenarios in Plaid can be divided into two groups: instant and asynchronous.
Instant verification happens when PaymentMethod is ready to be Charged immediately after customer has finished Plaid setup process. The event "payment_method_update" would be created and PaymentMethod would have status "chargeable" .
To finish test Payment Method verified instantly setup you can use following steps during Plaid configuration:
Houndstooth Bank
in Link.user_good
and pass_good
in the Credential pane.Plaid Savings (****1111)
.021000021
1111222233331111
Asynchronous verification happens when PaymentMethod is not yet ready to be Charged after customer has finished Plaid setup process.
An event fo type payment_method_update
would be created and PaymentMethod would have status pending
.
This verification process of such account can take up to a week.
After it is finished an event payment_method_update
would be created and PaymentMethod would get status chargeable
or failed
.
In Test mode PaymentMethod will became chargeable in few seconds to simplify testing.
To finish test Payment Method verified asynchronously you can use following steps during Plaid configuration:
Houndstooth Bank
in Link.user_good
and microdeposits_good
in the Credential pane.Plaid Checking (****0000)
.021000021
1111222233330000
Prior to processing an ACH payment, you must first get permission from the customer by presenting a mandate. This mandate only needs to be displayed the first time you collect the ACH payment details and must be presented as part of the payment flow on your website. Final language should be reviewed and approved by your legal council review before adding it to your payments checkout page.
Example mandate is present in the example view for reference.
One can also use our ACH without Plaid, receiving account details directly from customer. See our ACH Payment Method with bank account details for more details.
<div id="payment-error" class="alert alert-danger" style="display: none"></div>
<form id="payment-form" class="grid grid-cols-1 gap-4">
<div class="pending hidden">
<div class="result-mark">
<i class="fal fa-sync fa-spin mb-5"></i> Pending
</div>
</div>
<div class="successful hidden">
<div class="result-mark success ">
<i class="fal fa-check mb-5"></i> Successful
</div>
<div class="text-center mt-2">
<a id="charge-example-link" class="btn-primary w-full flex justify-center">Charge this Payment Method</a>
</div>
<div class="text-center mt-2">
<a class="try-again" href="/examples/ach-payment-method-with-plaid-0auth">Or try again</a>
</div>
</div>
<div class="failed hidden">
<div class="result-mark ">
<i class="fal text-danger fa-exclamation-circle mb-5"></i> Failed
</div>
<div class="text-center">
<a class="try-again" href="/examples/ach-payment-method-with-plaid-0auth">Try again</a>
</div>
</div>
<div class="init-form">
<!-- custom fields can be added here -->
<div class="grid gap-3 grid-cols-8">
<label class="col-span-2 flex items-center" for="name">
Name
</label>
<div class="col-span-6">
<input id="name" name="name" type="text" class="form-control">
</div>
</div>
<div class="alert alert-warning mt-4">
<h5 class="h5">Mandate</h5>
<p class="indent-4">
By continuing you hereby authorize each of Example Store (“Example Company”), its processor, and any financial institution (the “Suppliers”) working in connection with either of
them, to debit your bank account up to the full amount of your purchase, including for any applicable fees, taxes, and other costs, including shipping costs.
</p>
<p class="indent-4">
Additionally, you authorize Example Company to debit or credit your bank account to correct any erroneous debit, to make necessary adjustments to your payment, to issue a refund back to your bank
account, or to facilitate any recurring and/or sequential debits initiated by you.
This authority remains in full force and effect until Example Company has received written notification from you of its termination in such time and manner as to afford ExampleCompany and the
Suppliers to act on it.
</p>
<p class="indent-4">
If you wish Example Company to charge you for services or purchases that occur on a regular basis, you are authorizing Shift4 Example Store to debit your bank account periodically.
</p>
</div>
<div class="mt-4">
<button id="setup-payment-method-btn" type="submit" class="btn-primary w-full flex justify-center">Continue</button>
</div>
</div>
</form>
<script type="text/javascript" src="https://js.dev.shift4.com/shift4.js"></script>
<script type="text/javascript" src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
<script type="text/javascript">
// Setup Shift4.js with your public key
const shift4 = Shift4("pu_test_WVMFC9GFuvm54b0uorifKkCh");
const clientObjectId = new URLSearchParams(window.location.search).get('clientObjectId');
window.addEventListener('load', ev => {
if (clientObjectId) {
// Payment Method already created. Show pending indicator to user and handle Method's current state.
changeFormState({ pending: true });
handlePaymentMethodState(clientObjectId);
} else {
// Start ACH setup when button is clicked
document.getElementById('payment-form').addEventListener('submit', async ev => {
// Prevent default submitting action
ev.preventDefault();
// Show pending indicator in form
changeFormState({ pending: true });
try {
// create payment method to get required Plaid.create options
const request = {
billingName: document.getElementById('name').value
};
const { clientObjectId } = await httpPost('/payment-method', request);
// Update browser URL
addClientObjectIdToURL(clientObjectId);
// handle current method state
await handlePaymentMethodState(clientObjectId);
} catch (e) {
displayError(e);
}
});
}
function addClientObjectIdToURL(clientObjectId) {
const url = new URL(window.location.href);
url.searchParams.set('clientObjectId', clientObjectId);
window.history.pushState({}, '', url.href);
}
async function handlePaymentMethodState(clientObjectId) {
const paymentMethod = await shift4.handlePaymentMethodNextAction(clientObjectId);
// check if method is pending
if (paymentMethod.status === 'pending') {
// flow.nextAction = plaid, so we open plaid with received options
if (paymentMethod.flow && paymentMethod.flow.nextAction === 'plaid') {
processPlaid(clientObjectId, paymentMethod.flow.plaid.linkOptions);
} else {
displayError("Unexpected payment method flow state")
}
} else if (paymentMethod.status === 'chargeable') {
document.getElementById('charge-example-link').href = '/examples/charge-with-ach-payment-method?paymentMethodClientObjectId=' + clientObjectId;
changeFormState({ successful: true });
} else if (paymentMethod.status === 'failed') {
displayError('Failed to verify an account');
} else {
displayError("Unexpected payment method status");
}
}
function processPlaid(clientObjectId, plaidOptions) {
const plaidLinkOptions = {
// use options supplied in flow
...plaidOptions,
// send details to shift4 server after Plaid succeeded
onSuccess: async (public_token, metadata) => {
try {
// pass received data to shift4.js
await shift4.updatePaymentMethod(clientObjectId, {
plaid: {
publicToken: public_token,
publicTokenMetadata: metadata
}
});
// handle new payment method state
await handlePaymentMethodState(clientObjectId);
} catch (err) {
console.error(err, metadata);
displayError(err);
}
},
// show error if Plaid failed
onExit: (err) => {
displayError(err || 'User cancelled Plaid process');
}
};
Plaid.create(plaidLinkOptions).open();
// Optional: save clientObjectId to support Plaid Link reinitialization in 0auth flow in webview
localStorage.setItem('plaid_clientObjectId', clientObjectId);
}
// HELPER FUNCTIONS
function displayError(err) {
const msg = err && (typeof err === 'string' ? err : err.message);
const errorElement = document.getElementById('payment-error');
errorElement.textContent = msg || 'Unknown error';
errorElement.style.display = 'block';
changeFormState({ failed: true });
}
function changeFormState({ failed, pending, successful }) {
document.querySelectorAll('.init-form').forEach(element => {
element.classList.add('hidden');
});
document.querySelectorAll('.failed').forEach(element => {
failed
? element.classList.remove('hidden')
: element.classList.add('hidden');
});
document.querySelectorAll('.pending').forEach(element => {
pending
? element.classList.remove('hidden')
: element.classList.add('hidden');
});
document.querySelectorAll('.successful').forEach(element => {
successful
? element.classList.remove('hidden')
: element.classList.add('hidden');
});
}
function httpPost(url, body) {
const serverUrl = '/ajax/examples/ach-payment-method-with-plaid-0auth';
const request = {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(body)
};
return fetch(serverUrl + url, request).then(response =>
response.json().then(
body => response.ok ? body : Promise.reject(new Error(body.error && body.error.message)),
() => Promise.reject(new Error('Request failed with malformed body and status: ' + response.status))
));
}
});
</script>
@AjaxController
@RequestMapping("/ajax/examples/ach-payment-method-with-plaid-0auth")
class ExamplesAjaxAchWithPlaidController {
@PostMapping("/payment-method")
Map createPaymentMethod(@RequestBody ExampleRequest initRequest) throws IOException {
try (Shift4Gateway shift4Gateway = createShift4Gateway()) {
// create payment method
PaymentMethodRequest paymentMethodRequest = new PaymentMethodRequest()
.set("type", "ach")
.set("ach", Map.of(
"verificationProvider", "plaid")
)
.billing(new BillingRequest()
.name(initRequest.billingName))
.set("flow", Map.of(
"returnUrl", getSiteUrl() + "/examples/ach-payment-method-with-plaid-0auth/return"
));
// pass clientObjectId of payment method to frontend to finish setup there
return singletonMap("clientObjectId", paymentMethod.getClientObjectId());
} catch (Shift4Exception e) {
throw new BadRequestException(e.getMessage());
}
}
private static class ExampleRequest {
String billingName;
}
...
}