broadleaf:
stripe:
micro-deposit-verification-url: https://heatclinic.localhost:8456/checkout/stripe-ach-microdeposit-verification (1)
payment-intent-access-token-secret: token_secret (2)
By default, Stripe provides a hosted micro-deposit verification page & sends misc. ACH-related emails to the customer. While this provides a path to quickly complete integrations, it provides little to no control over the look & feel of these interactions. To provide a more white-glove experience for your customers, Stripe allows these interactions to be taken over by the integrator. Within the Broadleaf context, this means providing the necessary data & plumbing to allow our clients to create a custom micro-deposit verification page and send custom emails via the NotificationService.
Add the following configuration:
broadleaf:
stripe:
micro-deposit-verification-url: https://heatclinic.localhost:8456/checkout/stripe-ach-microdeposit-verification (1)
payment-intent-access-token-secret: token_secret (2)
The full URL to the custom micro-deposit verification page
The secret key that is used to generate the payment intent access token
By default, the PaymentIntent id and access token will be added as a parameters to this URL - {microDepositVerificationUrl}?paymentIntentId={paymentIntentId}&accessToken={accessToken}
. This id and token should be used by the frontend to retrieve the clientSecret
(GET /api/payment/stripe/payment-intent/{paymentIntentId} --header 'X-Payment-Intent-Access-Token: accessToken'
) that then is used to get the Stripe PaymentIntent
object on the client side.
The access token was added because if the custom page is used to validate the payment with micro-deposit, it is required to retrieve the PaymentIntent on the client side using the client secret. Storing the client secret is not secure, so we generate the validation URL with the payment intent ID and access token. These values should be used to get the client secret using the server-side API.
Note
|
The token secret should be stored securely, as you would with the Stripe API credentials. |
The expectation is that this URL is surfaced to the customer via the order confirmation page & email after checkout has been submitted.
import React, { useEffect, useState } from 'react';
import { GetServerSidePropsContext } from 'next';
import { get, range, toUpper } from 'lodash';
import { useStripe } from '@stripe/react-stripe-js';
import { PaymentIntent, StripeError } from '@stripe/stripe-js';
import { Form, Formik, Field } from 'formik';
import { useStripePaymentServicesClient } from '@app/common/contexts';
import { usePaymentAuthState } from '@app/common/hooks';
import { useReadPaymentIntentInfo } from '@broadleaf/stripe-payment-services-react';
type Props = {
paymentIntentId: string;
accessToken: string;
};
type ServerSideProps = {
props: Props;
};
export default function StripeAchMicroDepositVerification(
props: Props
): JSX.Element {
const { paymentIntentId, accessToken } = props;
const stripe = useStripe();
const [clientSecret, setClientSecret] = useState<string>();
const [paymentIntent, setPaymentIntent] = useState<PaymentIntent>();
const [stripeError, setStripeError] = useState<StripeError>();
const stripePaymentServicesClient = useStripePaymentServicesClient();
const authState = usePaymentAuthState();
const { readPaymentIntentInfo, error: readPaymentIntentError } =
useReadPaymentIntentInfo({
stripePaymentServicesClient,
authState,
});
useEffect(() => {
if (paymentIntentId && !clientSecret) {
readPaymentIntentInfo(paymentIntentId, accessToken)
.then(paymentIntentResponse => {
setClientSecret(paymentIntentResponse.clientSecret);
})
.catch(() => {
console.error('There was an error processing your request. Please check your info and try again.');
});
}
}, [paymentIntentId, accessToken, readPaymentIntentInfo, clientSecret]);
useEffect(() => {
if (stripe && clientSecret && !paymentIntent) {
stripe
.retrievePaymentIntent(clientSecret)
.then(stripeResponse => {
if (stripeResponse.error) {
setStripeError(stripeResponse.error);
} else {
setPaymentIntent(stripeResponse.paymentIntent);
}
})
.catch(() => {
console.error('There was an error processing your request. Please check your info and try again.');
});
}
}, [stripe, clientSecret, paymentIntent]);
const handleSubmit = useHandleSubmit({
clientSecret,
setPaymentIntent,
setStripeError,
});
if (!stripe || !paymentIntent) {
return <div>'Loading ...'</div>;
}
if (paymentIntent?.last_payment_error?.message) {
return (
<div className="flex flex-col justify-center">
<strong className="block mx-auto my-4 max-w-md text-center text-red-600 text-lg font-normal">
{paymentIntent.last_payment_error.message}
</strong>
<div className="flex justify-center mb-8">
<a href="/">Continue Shopping</a>
</div>
</div>
);
}
if (
paymentIntent.status !== 'requires_action' ||
paymentIntent.next_action?.type !== 'verify_with_microdeposits'
) {
return (
<div>
<h2 className="mb-6 mt-4 text-center text-4xl font-bold">
Thank you, your bank account has been verified!
</h2>
<div className="flex justify-center mb-8">
<a href="/">Continue Shopping</a>
</div>
</div>
);
}
const microDepositType = get(
paymentIntent,
'next_action.verify_with_microdeposits.microdeposit_type'
);
let initialValues;
if (microDepositType === 'descriptor_code') {
initialValues = {
descriptorCode0: 'S',
descriptorCode1: 'M',
};
}
return (
<main className="container mx-auto px-4 py-8 xl:px-0">
<section className="flex flex-col">
<header className="mb-4">
{microDepositType === 'descriptor_code' && (
<h2 className="mb-6 mt-4 text-center text-4xl font-bold">
Enter the 6-digit code from your bank statement to complete
payment
</h2>
)}
{microDepositType !== 'descriptor_code' && (
<h2 className="mb-6 mt-4 mx-auto max-w-lg text-center text-2xl font-bold">
Enter a two positive integers, in cents, equal to the values of
the micro-deposits sent to your bank account
</h2>
)}
</header>
<div className="flex justify-center">
<Formik
initialValues={initialValues}
onSubmit={async (values, actions) =>
handleSubmit(values, actions, microDepositType)
}
>
{({ isSubmitting }) => (
<Form className="flex flex-col">
{microDepositType === 'descriptor_code' && (
<>
<p className="my-2 max-w-xs text-justify text-sm">
Stripe deposited $0.01 to your account. To complete your
payment, enter the 6-digit code starting with "SM" from
deposit
</p>
<p className="flex self-center my-2 text-sm">Enter Code</p>
<div className="flex flex-nowrap self-center mb-2 h-20">
{range(6).map(n => (
<Field name={`descriptorCode${n}`}>
{({ field }) => (
<input
{...field}
className="uppercase"
maxLength={1}
readOnly={isSubmitting || n === 0 || n === 1}
type="text"
onChange={field.onChange}
/>
)}
</Field>
))}
</div>
</>
)}
{microDepositType !== 'descriptor_code' && (
<div>
<InputField
className="mx-2 my-2"
name="deposit0"
label={formatMessage(
messages.microDepositAmountsInputLabel,
{
valueNumber: 1,
}
)}
readOnly={isSubmitting}
required
maxLength={2}
minLength={2}
/>
<InputField
className="mx-2 my-2"
name="deposit1"
label={formatMessage(
messages.microDepositAmountsInputLabel,
{
valueNumber: 2,
}
)}
readOnly={isSubmitting}
required
maxLength={2}
minLength={2}
/>
</div>
)}
{(stripeError || readPaymentIntentError) && (
<strong className="block my-4 max-w-xs text-center text-red-600 text-lg font-normal">
{stripeError?.message ||
'There was an error processing your request'}
</strong>
)}
<button type="submit" disabled={isSubmitting}>
Verify
</button>
<p className="my-2 max-w-xs text-justify text-sm">
Can't find it? Check back in 1-2 days to try again with
another deposit.
</p>
</Form>
)}
</Formik>
</div>
</section>
</main>
);
}
const useHandleSubmit = ({
clientSecret,
setPaymentIntent,
setStripeError,
}) => {
const stripe = useStripe();
return async (values, actions, microDepositType) => {
let data;
if (microDepositType === 'descriptor_code') {
const {
descriptorCode2,
descriptorCode3,
descriptorCode4,
descriptorCode5,
} = values;
const descriptorCode = toUpper(
`SM${descriptorCode2}${descriptorCode3}${descriptorCode4}${descriptorCode5}`
);
data = {
descriptor_code: descriptorCode,
};
} else {
data = {
amounts: [values.deposit0, values.deposit1],
};
}
const stripeResponse = await stripe.verifyMicrodepositsForPayment(
clientSecret,
data
);
if (stripeResponse.error) {
setStripeError(stripeResponse.error);
} else {
setPaymentIntent(stripeResponse.paymentIntent);
}
actions.setSubmitting(false);
};
};
export async function getServerSideProps({
query,
res,
}: GetServerSidePropsContext): Promise<ServerSideProps> {
if (res.statusCode > 299) {
// if we've already encountered an error or redirect, skip the normal activities
return {
props: { paymentIntentId: '', accessToken: '' },
};
}
const { paymentIntentId, accessToken } = query;
return {
props: {
paymentIntentId: paymentIntentId as string,
accessToken: accessToken as string,
},
};
}
When ACH transactions are attempted with Stripe, there will always be a several-day delay between the initiation of the transaction & when the transaction is processed. During that time, it’s important to communicate the state of the order & payment processing to the customer. Additionally, if micro-deposit verification is required, it’s important to clearly highlight to the customer that additional action is required from them to successfully process the transaction.
By default, Stripe ACH transactions with Broadleaf include the following touch-points with the customer during checkout, or after checkout has been submitted:
ACH mandate approval email sent by Stripe
Email sent by Broadleaf acknowledging the order submission, but highlighting that we’re awaiting payment results to finalize the checkout
Micro-deposit verification email sent by Stripe
Order confirmation email sent by Broadleaf if/when the ACH transaction succeeds
Payment failure email sent by Broadleaf if/when the ACH transaction fails
To create more of a white-glove experience, all of these emails can be customized. The Stripe-sent emails can be disabled via the Stripe Dashboard email settings section, & replaced by emails sent via Broadleaf. Furthermore, all emails sent via Broadleaf can be completely customized via overriding the default NotificationService templates.
Relevant NotificationService Email Templates:
stripe_ach_mandate_confirmation.html
awaiting_payment_results.html
NOTE: If micro-deposit verification is required, we expect the verification url to be communicated via this email.
pending_payment_failed.html
order_confirmation.html
When customizing the ACH Mandate Confirmation emails, make sure to review the Stripe documentation on this topic. The PaymentCustomerNotification
payload coming from PaymentTransactionServices & the BLC-Stripe payment module includes the recommended fields from the ACH payment method - i.e. authorization date, account holder name, financial institution, routing number, & the last four digits of the account number. Additionally, this payload identifies if the ACH payment method is being saved for future use so that you know whether to include both portions of Stripe’s provided mandate text or not.
To declare the intention of sending custom mandate approval emails, the following property should be declared with your application that contains PaymentTransactionServices:
broadleaf:
stripe:
send-custom-mandate-email: true
To assist with the display of the mandate text in your email template, declare the following property with your application that contains NotificationServices:
broadleaf:
notification:
message:
variables-by-application-id:
'your_application_id':
business-name: 'Your Business Name'
Note
|
|