@fragment-dev/payment-elements
Embed Fragment's Payment Elements in your product to process payments through Fragment.
Install
npm install @fragment-dev/payment-elements
React 17, 18 or 19 must be a peer dependency.
Usage
Your server calls the server-side Fragment SDK's createPayment and passes the returned Payment to your product's payment page.
import {
Checkout,
PaymentElements,
usePayment,
} from '@fragment-dev/payment-elements';
const PayButton = () => {
const { state, canSubmit, submit, error } = usePayment();
if (state.status === 'accepted') {
return <p>Payment accepted.</p>;
}
return (
<>
<button
type="button"
disabled={!canSubmit}
// Call submit() inside the click handler. It never rejects.
onClick={() => submit()}
>
{state.status === 'processing' ? 'Paying…' : 'Pay'}
</button>
{state.status === 'declined' && <p role="alert">{state.message}</p>}
{error && <p role="alert">{error.message}</p>}
</>
);
};
const PaymentPage = ({ payment, email }) => (
<PaymentElements>
<Checkout
payment={payment}
billingDetails={{ email }}
onPaymentAccepted={close}
onPaymentDeclined={recordDecline}
onError={({ message }) => showError(message)}
>
<PayButton />
</Checkout>
</PaymentElements>
);
Checkout renders your Pay Button below the address and card fields. It takes
the same props as PaymentSession.
Use usePayment().state to render the status in your product.
type PaymentCheckoutState =
| { status: 'loading' }
| { status: 'ready' }
| { status: 'processing' }
| { status: 'declined'; message: string; declineCode: string | null }
| { status: 'accepted' };
On mount the session loads the Payment's status from Fragment and continues an
attempt that is still in flight, so a reload does not charge twice. state
reads loading until then.
submit()waits for any verification step the card's bank asks for, and resolves when the charge is accepted or declined, or when it fails with an error.onPaymentAcceptedfires when the charge is accepted. Read the Payment's status from Fragment on your server before you fulfill an order.onPaymentDeclinedfires when the card is declined. The person paying can enter another card and submit again. The decline stays instateuntil then.onErroris required and fires for every failure that is not a decline. Invalid payments report through this callback and render no children.
Custom layout
To customize the layout, render PaymentSession with AddressElement,
CardElement and your Pay Button as children. Mount one CardElement per
session. AddressElement is optional. See Billing details
to learn more.
Errors
Use usePayment().error to render failures that are not declines. It clears
when a field changes and when submit() starts. A failure leaves the session
ready, so the person paying can submit again.
Save Payment Methods
Set savePaymentMethod to true to save the card for the payer in billingDetails.email when the
payment confirms. Defaults to false.
Billing details
You can optionally provide the name and address of the person making the payment. Render the
AddressElement if you don't provide them.
Saving a card
To save an end-user's card without charging it, your server calls the
server-side Fragment SDK's createPaymentMethodSetup with the end-user's email
and passes the returned PaymentMethodSetup to your page.
import {
PaymentElements,
SavePaymentMethod,
usePaymentMethodSetup,
} from '@fragment-dev/payment-elements';
const SaveCardButton = () => {
const { state, canSubmit, submit, error } = usePaymentMethodSetup();
if (state.status === 'saved') {
return <p>Card saved.</p>;
}
return (
<>
<button
type="button"
disabled={!canSubmit}
// Call submit() inside the click handler. It never rejects.
onClick={() => submit()}
>
{state.status === 'processing' ? 'Saving…' : 'Save card'}
</button>
{state.status === 'declined' && <p role="alert">{state.message}</p>}
{error && <p role="alert">{error.message}</p>}
</>
);
};
const SaveCardPage = ({ paymentMethodSetup }) => (
<PaymentElements>
<SavePaymentMethod
paymentMethodSetup={paymentMethodSetup}
onPaymentMethodSaved={close}
onPaymentMethodDeclined={recordDecline}
onError={({ message }) => showError(message)}
>
<SaveCardButton />
</SavePaymentMethod>
</PaymentElements>
);
SavePaymentMethod renders your Save Button below the address and card fields.
It takes the same props as PaymentMethodSetupSession.
To customize the layout, render PaymentMethodSetupSession with
AddressElement, CardElement and your Save Button as children. Mount one
CardElement per session. AddressElement is optional. See the
billingDetails note below to learn more.
Use usePaymentMethodSetup().state to render the status in your product.
type PaymentMethodSetupState =
| { status: 'loading' }
| { status: 'ready' }
| { status: 'processing' }
| { status: 'declined'; message: string; declineCode: string | null }
| { status: 'saved' };
On mount the session loads the setup's status from Fragment and continues an
attempt that is still in flight. state reads loading until then.
submit()waits for any verification step the card's bank asks for, and resolves when the card is saved or declined, or when it fails with an error.onPaymentMethodSavedfires when the payment method was successfully saved. Thesavedstate carries no card details. To read the card, your server queriespaymentMethodSetupand reads itspaymentMethodfield, or lists the end-user's cards withpaymentMethods(filter: { user }).onPaymentMethodDeclinedfires when the card is declined. The end-user can enter another card and submit again. The decline stays instateuntil then.onErroris required and fires for every other non-decline failure. Invalid setups report through this callback and render no children.billingDetailstakes an optionalnameandaddress. Without anAddressElement, saving a card needsname,address.line1,address.postalCodeandaddress.country. Render theAddressElementif you don't provide them.
Theming
Pass theme to style the hosted fields. Every setting has a default:
<PaymentElements
theme={{
appearance: 'light',
fontFamily: 'Arial, sans-serif',
input: {
fontSize: '16px',
backgroundColor: '#ffffff',
borderColor: '#d6d1c7',
borderWidth: '1px',
borderRadius: '8px',
padding: '12px',
focus: { borderColor: '#00a7c9' },
invalid: { borderColor: '#c62828' },
},
label: { fontSize: '12px', color: '#555562', spacing: '8px' },
error: { fontSize: '12px', color: '#c62828' },
layout: { rowGap: '16px', columnGap: '10px' },
}}
>
input supports fontSize, color, backgroundColor, borderColor,
borderWidth, borderRadius and padding. Its focus and invalid objects
accept the same properties; unspecified properties inherit the base input style.
Invalid styling takes precedence when an invalid field is focused.
Set input.borderWidth to '0px' for borderless fields.
label and error style hosted labels and inline validation messages, not
messages your application renders from session errors. label.spacing controls
the gap below a label. layout controls gaps between fields, not their order.
These settings apply to both card and address elements.
appearance is light or dark. When omitted it follows the dark class
on <html>. The default uses 16px input text, 12px labels, 1px borders,
8px corners and 12px input padding.
Use literal colours and system fonts. Hosted fields cannot inherit your page's CSS custom properties or fonts. Keep input text at least 16px on mobile.
License
MIT