Payment fields
Your layout, your pay button, Wajub renders only the secure inputs.
Payment fields invert the hosted checkout. You keep the page, the order summary and the pay button. Wajub renders the inputs that must not touch your DOM, each in its own iframe, and you submit them when you are ready.
That trade buys you layout control and costs you the parts of the hosted page that need a full flow: coupons, custom fields, OTP and the complete shipping step. If you need those, use hosted checkout instead.
The five components
| Type | Renders | Who pays |
|---|---|---|
card | The card provider's own card fields (Stripe, Adyen, Mollie), PayPal's card button, or a button that opens the provider's payment window (Paystack, Flutterwave, FedaPay, Paddle, PayDunya, CinetPay, Kkiapay) | You, with confirmPayment() |
mobileMoney | Operator and phone number | You, with confirmPayment() |
payment | A method selector and the matching form | You, with confirmPayment() |
address | Shipping or billing address | Nobody, read it with getValue() |
wallet | Apple Pay or Google Pay | The native button inside the component |
The shape of it
A factory holds the session and the shared options. Each create() makes one component, and
mount() puts it on the page.
import { components, confirmPayment } from '@wajub/js';
const factory = await components(sessionId, {
locale: 'fr',
appearance: { primaryColor: '#2563eb', labels: 'floating' },
});
const card = factory
.create('card')
.on('change', (event) => (payButton.disabled = !event.complete))
.mount('#card');
payButton.onclick = () => confirmPayment({ sessionId, components: card });Options given to the factory become the defaults for every component it creates. Options given to
create() win for that one component.
Components size themselves
Each component starts at min-height: 120px and the SDK sets the iframe height from the
checkout every time the content changes. There is nothing to measure and nothing to reserve.
Configuration
| Option | Type | Applies to | What it does |
|---|---|---|---|
sessionId | string | All | Required, set once on the factory |
locale | string | All | fr, en, es, pt, ar |
appearance | AppearanceConfig | All | Appearance |
layout | CheckoutLayout | payment | How the method selector is arranged |
collectAddress | shipping or billing | payment | Adds an address block inside the form |
addressMode | shipping or billing | address | Which set of labels to use |
collectName | boolean | payment, address | Adds the payer name field |
fields.phone | always, auto, never | payment, address | Phone field visibility |
componentOrigin | string | All | Your canonical origin, for multi domain shops |
componentOrigin defaults to the origin of the page doing the mounting, so a single domain needs
nothing here.
factory
.create('address', { addressMode: 'shipping', collectName: true, fields: { phone: 'auto' } })
.mount('#address');
factory
.create('payment', { collectAddress: 'shipping', collectName: true, layout: 'tabs' })
.mount('#payment');Events
Subscribe with .on(event, handler), which returns the component so calls chain. The same
handlers exist as onReady, onChange and the rest in ComponentConfig.
| Event | Payload | Fires when |
|---|---|---|
ready | ComponentInstance | Mounted and interactive |
change | { complete, empty, error, value } | A field changed or its validity changed |
focus | Nothing | A field took focus |
blur | Nothing | A field lost focus |
success | The transaction | The payment succeeded |
error | WajubError | The payment failed |
loaderror | WajubError | The component could not load |
resize | { height } | The component grew or shrank |
statechange | { state, method } | The checkout moved to another state |
methodchange | { methodId } | The payer picked another method |
component.on('change', (event) => {
payButton.disabled = !event.complete;
errorLabel.textContent = event.error ? event.error.message : '';
});complete means valid, not paid
change.complete says the fields would pass validation. The money moves on success, and only
your webhook proves it moved.
Submitting
confirmPayment takes the session and one mounted component, submits it, and waits for the
outcome.
try {
const { status, transaction } = await confirmPayment({ sessionId, components: card });
showReceipt(status, transaction);
} catch (error) {
showRetry(error.code, error.message);
}| Option | Default | What it does |
|---|---|---|
sessionId | Required | The session the component was created with |
components | Required | One mounted card, mobileMoney or payment component |
timeout | 60000 | Milliseconds before rejecting with confirmation_timeout. 0 waits forever |
Four rejections happen before anything is submitted, so they are wiring mistakes rather than payment failures.
| Code | Cause |
|---|---|
missing_component | No component passed, or an object that cannot submit |
wallet_not_supported | A wallet component. Its own native button pays |
address_not_payment | An address component. Read it with getValue() |
confirmation_timeout | No outcome arrived within timeout |
Driving a component
| Method | What it does |
|---|---|
mount(container) | Attaches to a selector or an element |
update({ locale, appearance, layout }) | Applies the change live, no remount |
focus(), blur() | Moves focus in and out of the first field |
submit() | Submits without confirmPayment, you handle the events |
selectMethod(id) | Switches method on a payment component |
isComplete() | Whether the fields would pass validation now |
getState() | The current CheckoutState, INITIATED before anything happens |
getError() | The last WajubError, or null |
getValue() | Address values, { name, phone, address, mode } |
unmount(), destroy() | Remove from the DOM, with or without keeping the instance |
update() works here, unlike the hosted embed
A component sends locale, appearance and layout to its iframe and repaints. The hosted
checkout applies everything except layout. That asymmetry is real, not a documentation
shortcut.
getValue() returns null until the address component has reported a value, which it does on its
first change. Read it in the handler, or read it when the payer submits, not on mount.
Through a framework
Each component exists as a ready made wrapper, with the same options as props. The wrapper also
watches appearance, locale and layout and calls update() for you.
const { confirm, isConfirming } = useConfirmPayment(sessionId);
const field = useRef<ComponentInstance | null>(null);
<PaymentComponent sessionId={sessionId} onInstance={(i) => (field.current = i)} />;
<button disabled={isConfirming} onClick={() => field.current && confirm(field.current)}>
Pay now
</button>;The provider, the prop tables and the server side wiring are on React, Vue and Svelte.
What you give up
| Feature | Hosted checkout | Payment fields |
|---|---|---|
| Saved payment methods | Yes | Yes |
| 3DS | Yes | Yes |
| Wallets | Yes | Yes, through the native button |
| Coupons | Yes | No |
| Custom checkout fields | Yes | No |
| Full shipping flow | Yes | Address block only |
| OTP | Yes | No |