paysafe-form
v0.4.0
Published
React lifecycle wrapper for Paysafe.js.
Readme
paysafe-form
React lifecycle wrapper for Paysafe.js. The component owns the Paysafe setup lifecycle and tokenization calls for cards, Google Pay, and Apple Pay. Your app owns the setup options, tokenization options, and the JSX containers rendered for each payment method.
Install
npm install paysafe-formReact and React DOM are peer dependencies.
Paysafe script
This package expects the Paysafe browser SDK to already be loaded on window.paysafe before the form sets up. If the SDK is missing, setup throws Paysafe script has not loaded.
Basic Usage
import type { FC } from 'react';
import { useCallback, useId } from 'react';
import type { CardOptions, PaymentType, SetupOptions, TokenizeOptions } from 'paysafe-form';
import { isSetupError, isTokenizeError, PaysafeForm, useFormContext } from 'paysafe-form';
interface Props {
environment: 'LIVE' | 'TEST';
merchantRefNum: string;
currencyCode: string;
amount: number;
customerDetails: TokenizeOptions['customerDetails'];
}
export const CheckoutForm: FC<Props> = ({ environment, merchantRefNum, currencyCode, amount, customerDetails }) => {
const id = useId().replaceAll(':', '');
const getSetupOptions = useCallback((setupKey: string): SetupOptions => ({
environment,
currencyCode,
fields: {
cardNumber: {
selector: `#card-number-${id}-${setupKey}`,
placeholder: 'Card Number',
},
cvv: {
selector: `#cvv-${id}-${setupKey}`,
placeholder: 'CVV',
},
expiryDate: {
selector: `#expiry-date-${id}-${setupKey}`,
placeholder: 'Exp. Date',
},
},
}), [ environment, currencyCode, id ]);
const getCardTokenizeOptions = useCallback((): CardOptions => ({
amount,
merchantRefNum,
paymentType: 'CARD',
transactionType: 'PAYMENT',
customerDetails,
}), [ amount, merchantRefNum, customerDetails ]);
const handleSetupError = (err: unknown) => {
if (isSetupError(err)) {
console.log(err.displayMessage);
} else {
console.log(err);
}
}
const handleTokenize = useCallback((token: string, paymentType: PaymentType) => {
console.log(token, paymentType);
}, []);
const handleTokenizeError = useCallback((err: unknown, paymentType: PaymentType) => {
if (isTokenizeError(err)) {
console.error(paymentType, err.displayMessage)
} else {
console.error(paymentType, err)
}
}, []);
return (
<PaysafeForm
apiKey={import.meta.env.VITE_PAYSAFE_API_KEY}
cardContainer={<CardContainer id={id} />}
getSetupOptions={getSetupOptions}
getCardTokenizeOptions={getCardTokenizeOptions}
onTokenize={handleTokenize}
onSetupError={handleSetupError}
onTokenizeError={handleTokenizeError}
/>
);
}
const CardContainer: FC<{id: string}> = ({ id }) => {
const { initialized, setupKey } = useFormContext();
return (
<>
<div id={`card-number-${id}-${setupKey}`} />
<div id={`cvv-${id}-${setupKey}`} />
<div id={`expiry-date-${id}-${setupKey}`} />
<button type="submit" disabled={!initialized.card}>
Pay by card
</button>
</>
);
}Payment Containers
Provide the JSX for each enabled payment method through its corresponding container prop:
| Payment method | Container prop | Tokenization trigger |
| --- | --- | --- |
| Card | cardContainer | The card form is submitted |
| Google Pay | googlePayContainer | A click bubbles from the container |
| Apple Pay | applePayContainer | A click bubbles from the container |
Each container is optional and rendered separately. cardContainer is rendered inside a <form> owned by PaysafeForm, so it should not render another form. It should include a submit control when card submission is required.
The Google Pay and Apple Pay wrappers remain hidden until their corresponding payment method is initialized. Paysafe renders its payment button into the element selected by getSetupOptions.
Each container subtree is keyed by the current setup key and remounts when a new Paysafe setup begins.
Form Context
Components supplied through the container props can call useFormContext().
The context contains:
setupKey: string- Current internal setup key. Use it to build field container ids that matchgetSetupOptions.initialized.card: boolean- Whether card fields initialized successfully.initialized.googlePay: boolean- Whether Google Pay initialized successfully.initialized.applePay: boolean- Whether Apple Pay initialized successfully.instance: PaysafeInstance | null- Current Paysafe instance, available after initialization.
The hook must be called by a descendant of PaysafeForm, not by the component that renders PaysafeForm itself.
Setup Options and Markup
getSetupOptions: (setupKey: string) => SetupOptions | Promise<SetupOptions>- Returns, or resolves to, the options passed topaysafe.fields.setupfor the current setup key.
Your rendered field container ids must match the selectors returned or resolved from getSetupOptions for the same setupKey.
Whether synchronous or asynchronous, getSetupOptions should retain the same identity while its captured setup inputs remain unchanged. Changing its identity starts a new Paysafe setup.
Correct:
const getSetupOptions = (setupKey: string) => ({
environment: 'TEST',
currencyCode: 'USD',
fields: {
cardNumber: { selector: `#paysafe-${setupKey}-cardNumber` },
cvv: { selector: `#paysafe-${setupKey}-cvv` },
expiryDate: { selector: `#paysafe-${setupKey}-expiryDate` },
},
});
const PaymentContainers: FC = () => {
const { setupKey } = useFormContext();
return (
<>
<div id={`paysafe-${setupKey}-cardNumber`} />
<div id={`paysafe-${setupKey}-cvv`} />
<div id={`paysafe-${setupKey}-expiryDate`} />
<button type="submit">Pay Now</button>
</>
);
}The static-id version can break when React remounts or reruns effects and the Paysafe SDK is still finishing older asynchronous setup work. The setup key gives each Paysafe setup fresh field ids as well as fresh keyed container subtrees.
Tokenization Options
Each payment method has its own optional tokenization-options getter:
getCardTokenizeOptions: () => CardOptionsgetGooglePayTokenizeOptions: () => GooglePayOptionsgetApplePayTokenizeOptions: () => ApplePayOptions
The appropriate getter is called when that payment method is triggered. Return the complete TokenizeOptions object you want passed to paysafeInstance.tokenize(...).
Use this function to create per-submit values such as merchantRefNum when needed:
const getCardTokenizeOptions = useCallback((): CardOptions => ({
amount,
merchantRefNum: generateRefNum(),
paymentType: 'CARD',
transactionType: 'PAYMENT',
customerDetails,
}), [ merchantRefNum, amount, customerDetails ]);If your backend requires one stable merchant reference per order, create it at the order level and return that value from the relevant getters.
After Apple Pay tokenization, PaysafeForm calls instance.complete('success') before onTokenize, or instance.complete('fail') before onTokenizeError.
Stable Function Props
getSetupOptions is a setup input. If its reference changes, PaysafeForm creates a new Paysafe setup.
Use React's normal dependency model: make getSetupOptions change identity when any setup value it captures changes. You can do that with React Compiler, useCallback, or by defining stable functions outside render.
The payment-specific tokenization getters are used only when their payment method is triggered. Changing their references does not create a new Paysafe setup. Their references should still follow normal React rules so tokenization uses current values.
Changing onTokenize, onTokenizeError, or onSetupError does not create a new Paysafe setup.
Props
Required
apiKey: string- Paysafe API key.getSetupOptions: (setupKey: string) => SetupOptions- Returns the options passed topaysafe.fields.setupfor the current setup key.onTokenize: (token: string, paymentType: PaymentType) => void- Called after successful tokenization.paymentTypeidentifies the payment-method handler that initiated the request.
Optional
cardContainer?: ReactNode- JSX rendered inside the card form.googlePayContainer?: ReactNode- JSX containing the Google Pay element selected during setup.applePayContainer?: ReactNode- JSX containing the Apple Pay element selected during setup.getCardTokenizeOptions?: () => TokenizeOptions- Returns the options passed unchanged totokenizefor a card form submission.getGooglePayTokenizeOptions?: () => TokenizeOptions- Returns the options passed unchanged totokenizefor a Google Pay click.getApplePayTokenizeOptions?: () => TokenizeOptions- Returns the options passed unchanged totokenizefor an Apple Pay click.onSetupError?: (err: unknown) => void- Called when setup orshow()rejects, or when a payment method reports a setup error.onTokenizeError?: (err: unknown, paymentType: PaymentType) => void- Called when tokenization rejects.
Callback return values are ignored. If a callback is asynchronous, PaysafeForm does not wait for it to settle.
Error Handling
Error callbacks receive unknown. Use the exported guards before accessing properties from the documented Paysafe error shapes.
<PaysafeForm
// ...
onTokenizeError={(err, paymentType) => {
if (isTokenizeError(err)) {
console.error(paymentType, err.displayMessage);
return;
}
console.error(paymentType, err);
}}
/>Card Field Events
Set up Paysafe field event handlers inside a container using the current instance from useFormContext().
Subscribe in an Effect that depends on instance. When a new Paysafe setup begins, the container remounts and receives the new instance.
const CardContainer: FC = () => {
const { instance, initialized } = useFormContext();
const [ allValid, setAllValid ] = useState(false);
useEffect(() => {
if (!instance || !initialized.card) {
return;
}
let active = true;
instance
.fields('CardNumber Cvv ExpiryDate')
.on('Valid Invalid', function (_, event) {
// guard against stale callbacks
if (!active) {
return;
}
this.classList.toggle('is-valid', event.type === 'Valid');
this.classList.toggle('is-invalid', event.type === 'Invalid');
// Update the container's validation state here.
});
return () => {
active = false;
};
}, [instance, initialized.card]);
return (
<button type="submit" disabled={!initialized.card || !allValid}>
Pay by card
</button>
);
};