import { FC, useState } from 'react';
import { useRouter } from 'next/router';
import { Form as FormikForm, Formik, FormikHelpers, Field } from 'formik';
import { get } from 'lodash';
import { useStripe } from '@stripe/react-stripe-js';
import type { PaymentIntent, StripeError } from '@stripe/stripe-js';
import {
Address,
PaymentRequest,
DefaultPaymentType,
} from '@broadleaf/commerce-cart';
import type { ApiError } from '@broadleaf/commerce-core';
import { maskCardNumber } from '@broadleaf/payment-js';
import { useSubmitPaymentRequest } from '@broadleaf/payment-react';
import { useCartContext, usePaymentsContext } from '@app/cart/contexts';
import {
usePaymentClient,
useStripePaymentServicesClient,
} from '@app/common/contexts';
import { usePaymentAuthState } from '@app/common/hooks';
import { useCreatePaymentIntentRequest } from '@broadleaf/stripe-payment-services-react';
type PaymentFormData = Address & {
customerEmail: string;
};
const StripeACHPaymentForm: FC = () => {
const [showConfirmationForm, setShowConfirmationForm] =
useState<boolean>(false);
const { error, stripeError, onSubmit } = useHandleSubmit({
showConfirmationForm,
setShowConfirmationForm,
});
return (
<div>
<Formik
initialValues={{
...billingAddress, // specify the billing address
customerEmail: '',
}}
onSubmit={onSubmit}
validateOnBlur
validateOnChange={false}
>
{({ isSubmitting }) => (
<FormikForm>
{!showConfirmationForm && (
<div className="relative ach-payment-form">
<Field name="deposit1">
{({ field }) => (
<input
{...field}
autoComplete="email"
readOnly={isSubmitting}
type="email"
onChange={field.onChange}
/>
)}
</Field>
</div>
)}
{showConfirmationForm && (
<div className="relative ach-confirmation-form mandate-text">
By clicking "Submit & Continue", you authorize ...
</div>
)}
{stripeError && (
<strong className="block my-4 text-red-600 text-lg font-normal">
{stripeError.message}
</strong>
)}
{error && (
<strong className="block my-4 text-red-600 text-lg font-normal">
There was an error processing your request
</strong>
)}
<button type="submit" disabled={isSubmitting}>
Submit & Continue
</button>
</FormikForm>
)}
</Formik>
</div>
);
};
type UseHandleSubmitResponse = {
error?: boolean | ApiError;
stripeError?: StripeError;
onSubmit: (
data: PaymentFormData,
actions: FormikHelpers<PaymentFormData>
) => Promise<void>;
};
const convertAddressToBillingDetails = values => {
const {
fullName: name,
phonePrimary,
addressLine1: line1,
addressLine2: line2,
city,
country,
stateProvinceRegion: state,
postalCode: postal_code,
} = values;
return {
name,
phone: get(phonePrimary, 'phoneNumber'),
address: {
line1,
line2,
city,
state,
country,
postal_code,
},
};
};
type HandleSubmitProps = {
showConfirmationForm: boolean;
setShowConfirmationForm: (showConfirmationForm: boolean) => void;
};
const useHandleSubmit = ({
showConfirmationForm,
setShowConfirmationForm,
}: HandleSubmitProps): UseHandleSubmitResponse => {
const [error, setError] = useState<boolean | ApiError>();
const [stripeError, setStripeError] = useState<StripeError>();
const { collectBankAccountForPayment, stripePaymentIntent } =
useCollectBankAccountForPayment({
setError,
setStripeError,
setShowConfirmationForm,
});
const { createPayment } = useCreatePayment({ setError });
const onSubmit = async (
data: PaymentFormData,
actions: FormikHelpers<PaymentFormData>
): Promise<void> => {
const { customerEmail, ...billingAddress } = data;
// specify the amount to pay
const amount = { amount: 11, currency: 'USD' };
if (!stripePaymentIntent || !showConfirmationForm) {
await collectBankAccountForPayment({
billingAddress,
customerEmail,
paymentTotal: amount,
});
} else {
await createPayment({
amount,
stripePaymentIntent,
billingAddress,
setSubmitting: actions.setSubmitting,
});
}
};
return { error, onSubmit, stripeError };
};
const useCollectBankAccountForPayment = ({
setError,
setStripeError,
setShowConfirmationForm,
}) => {
const stripe = useStripe();
const authState = usePaymentAuthState();
const stripePaymentServicesClient = useStripePaymentServicesClient();
const { cretePaymentIntent, error: createPaymentIntentError } =
useCreatePaymentIntentRequest({
stripePaymentServicesClient,
authState,
});
const [stripePaymentIntent, setStripePaymentIntent] =
useState<PaymentIntent>();
const collectBankAccountForPayment = async ({
billingAddress,
customerEmail,
paymentTotal,
}) => {
const { clientSecret } = await cretePaymentIntent({
paymentTotal,
});
if (createPaymentIntentError) {
setError(createPaymentIntentError);
return;
}
const { paymentIntent, error: collectBankAccountError } =
await stripe.collectBankAccountForPayment({
clientSecret,
params: {
payment_method_type: 'us_bank_account',
payment_method_data: {
billing_details: {
...convertAddressToBillingDetails(billingAddress),
email: customerEmail,
},
},
},
expand: ['payment_method'],
});
if (collectBankAccountError) {
console.error(collectBankAccountError.message);
// PaymentMethod collection failed for some reason.
setStripeError(collectBankAccountError);
} else if (paymentIntent.status === 'requires_confirmation') {
setStripePaymentIntent(paymentIntent);
// show confirmation form
setShowConfirmationForm(true);
}
};
return {
collectBankAccountForPayment,
stripePaymentIntent,
};
};
const useCreatePayment = ({ setError }) => {
const router = useRouter();
const { cart } = useCartContext();
const { payments, refetchPayments } = usePaymentsContext();
const paymentClient = usePaymentClient();
const authState = usePaymentAuthState();
const { handleSubmitPaymentInfo } = useSubmitPaymentRequest({
authState,
payments,
ownerId: cart?.id,
paymentClient,
multiplePaymentsAllowed: false,
rejectOnError: true,
});
const createPayment = async ({
amount,
stripePaymentIntent,
billingAddress,
setSubmitting,
}) => {
let displayAttributes: Record<string, string> = {};
let paymentName = DefaultPaymentType.ACH as string;
const usBankAccount = stripePaymentIntent?.payment_method?.us_bank_account;
if (usBankAccount) {
displayAttributes = {
bankName: usBankAccount.bank_name,
bankAccountNumber: maskCardNumber(usBankAccount.last4),
};
paymentName = `${displayAttributes.bankName} | ${displayAttributes.bankAccountNumber}`;
}
const paymentRequest = {
name: paymentName,
type: DefaultPaymentType.ACH,
gatewayType: 'STRIPE',
amount,
subtotal: cart.cartPricing.subtotal,
adjustmentsTotal: cart.cartPricing.adjustmentsTotal,
fulfillmentTotal: cart.cartPricing.fulfillmentTotal,
taxTotal: cart.cartPricing.totalTax,
isSingleUsePaymentMethod: true,
paymentMethodProperties: {
PAYMENT_INTENT_ID: stripePaymentIntent.id,
ONLINE_MANDATE: 'true',
ACCEPTED_AT: `${Math.floor(Date.now() / 1000)}`,
},
billingAddress,
displayAttributes,
} as PaymentRequest;
let paymentSummary;
try {
paymentSummary = await handleSubmitPaymentInfo(paymentRequest);
} catch (err) {
console.error('There was an error adding payment information', err);
setError(err);
} finally {
await refetchPayments();
setSubmitting(false);
if (paymentSummary) {
await router.push('/checkout/review');
}
}
};
return { createPayment };
};