Skip to content

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

TypeRendersWho pays
cardThe 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()
mobileMoneyOperator and phone numberYou, with confirmPayment()
paymentA method selector and the matching formYou, with confirmPayment()
addressShipping or billing addressNobody, read it with getValue()
walletApple Pay or Google PayThe 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.

Card fields and your own button
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

OptionTypeApplies toWhat it does
sessionIdstringAllRequired, set once on the factory
localestringAllfr, en, es, pt, ar
appearanceAppearanceConfigAllAppearance
layoutCheckoutLayoutpaymentHow the method selector is arranged
collectAddressshipping or billingpaymentAdds an address block inside the form
addressModeshipping or billingaddressWhich set of labels to use
collectNamebooleanpayment, addressAdds the payer name field
fields.phonealways, auto, neverpayment, addressPhone field visibility
componentOriginstringAllYour canonical origin, for multi domain shops

componentOrigin defaults to the origin of the page doing the mounting, so a single domain needs nothing here.

An address on its own, and a form that carries one
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.

EventPayloadFires when
readyComponentInstanceMounted and interactive
change{ complete, empty, error, value }A field changed or its validity changed
focusNothingA field took focus
blurNothingA field lost focus
successThe transactionThe payment succeeded
errorWajubErrorThe payment failed
loaderrorWajubErrorThe 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
Gating your own button
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.

Resolves, or rejects with a WajubError
try {
  const { status, transaction } = await confirmPayment({ sessionId, components: card });
  showReceipt(status, transaction);
} catch (error) {
  showRetry(error.code, error.message);
}
OptionDefaultWhat it does
sessionIdRequiredThe session the component was created with
componentsRequiredOne mounted card, mobileMoney or payment component
timeout60000Milliseconds before rejecting with confirmation_timeout. 0 waits forever

Four rejections happen before anything is submitted, so they are wiring mistakes rather than payment failures.

CodeCause
missing_componentNo component passed, or an object that cannot submit
wallet_not_supportedA wallet component. Its own native button pays
address_not_paymentAn address component. Read it with getValue()
confirmation_timeoutNo outcome arrived within timeout

Driving a component

MethodWhat 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

FeatureHosted checkoutPayment fields
Saved payment methodsYesYes
3DSYesYes
WalletsYesYes, through the native button
CouponsYesNo
Custom checkout fieldsYesNo
Full shipping flowYesAddress block only
OTPYesNo

What did you think of this content?