@neocash/bnpl-widget
v0.1.12
Published
Embeddable NeoCash BNPL checkout widget
Readme
@neocash/bnpl-widget
Embeddable NeoCash BNPL checkout widget — let your customers split a purchase into a small upfront payment plus a few monthly installments ("Pay Small Small"), without leaving your site.
The widget renders as a modal overlay on top of your page. It handles the full BNPL flow end-to-end: plan selection, identity verification (BVN + selfie liveness), credit decisioning, and the deposit-account hand-off for the customer's first payment. You give it a cart and a public key — it returns an application id when the customer submits.
Quick start
Drop-in script tag
<script src="https://unpkg.com/@neocash/bnpl-widget"></script>
<script>
document.getElementById('pay-button').onclick = () => {
NeoCashBNPL.init({
publicKey: 'pk_test_xxx',
cart: {
items: [{ name: 'Hisense 55" TV', qty: 1, price: 21500000 }],
total: 21500000,
},
onApprovalPending: (applicationId) => {
// Customer submitted — application is under review.
console.log('pending:', applicationId);
},
onClose: () => console.log('widget closed'),
});
};
</script>npm / bundler
npm install @neocash/bnpl-widgetimport { init } from '@neocash/bnpl-widget';
const handle = init({ publicKey, cart, onApprovalPending, onClose });
// later, if you want to dismiss the widget yourself:
handle.close();The money contract
All amounts are in kobo (integer naira × 100.) Pass
21500000for ₦215,000.
This applies to every monetary field the widget accepts or returns:
CartItem.price,Cart.totalPartnerPrefill.monthlySalary- Server-returned deposit amounts
The widget converts kobo to naira only at display time. The backend stores and returns kobo. Don't pre-convert on your side.
Configuration
init(options) accepts:
| Field | Type | Required | Description |
|---|---|:---:|---|
| publicKey | string | yes | Your merchant public key (pk_test_... or pk_live_...). Issued from the NeoCash dashboard. |
| cart | Cart | yes | The basket the customer is checking out. See below. |
| partnerPrefill | PartnerPrefill | no | Customer details you already know — skips re-entry on identity/employment screens. |
| theme | ThemeOptions | no | Color and font overrides. See Theming. |
| container | HTMLElement \| string | no | Element (or selector) to mount inside. Defaults to a fresh <div> appended to <body>. |
| onApprovalPending | (applicationId: string) => void | no | Fires once when the customer submits and the application enters review. |
| onClose | () => void | no | Fires whenever the modal is dismissed (X, backdrop, or handle.close()). |
| onError | (err: Error) => void | no | Surfaced for unrecoverable widget errors. Useful for telemetry. |
Cart shape
{
items: [
{ name: 'iPhone 15 Pro', qty: 2, price: 145000000, imageUrl: '...' },
{ name: 'AirPods Pro', qty: 1, price: 18500000 },
],
total: 308500000, // kobo
currency: 'NGN', // optional, defaults to NGN
}imageUrl is optional; if provided it shows on the cart summary screen.
PartnerPrefill (optional)
Any subset of the customer's details you already have. The widget pre-populates the matching fields and skips screens where every required field is filled:
partnerPrefill: {
firstName: 'Adaeze',
lastName: 'Okafor',
phone: '+2348012345678',
email: '[email protected]',
bvn: '12345678901',
employer: 'Acme Industries',
role: 'Product Manager',
monthlySalary: 85000000, // kobo — ₦850,000
officeAddress: '12 Bourdillon Rd, Ikoyi, Lagos',
homeAddress: '7 Admiralty Way, Lekki, Lagos',
}Theming
The widget ships with NeoCash defaults but you can match it to your brand. Pass any subset of these — omitted keys keep their built-in values:
NeoCashBNPL.init({
publicKey,
cart,
theme: {
primary: '#ff5a1f',
primaryStrong: '#e84a10', // hover/active shade
primarySoft: '#ffb89a', // muted accent
primaryWash: '#fff2eb', // selected-row background
ink: '#1a1a2e', // main text color
surface: '#f7f8fa', // panel background
border: '#e9eaf0', // hairline borders
fontFamily: '"Inter", system-ui, sans-serif',
},
});All values are passed straight through to CSS, so any valid color string (#rrggbb, rgb(), hsl(), named) and any valid font stack work. Theme overrides are scoped to the widget's mount root and never leak to your page.
If you only set primary, the derived shades keep their defaults — fine for purple-ish brands, less so for a hot-orange brand. Set the full primary* family when the contrast needs to match.
Handling outcomes
The widget is a self-contained flow; you don't poll for state. The two events you care about:
NeoCashBNPL.init({
// ...
onApprovalPending: (applicationId) => {
// The customer has submitted. The application is now in review
// (typically seconds, sometimes minutes for manual checks).
// Persist `applicationId` against your order so you can reconcile
// when the decision webhook fires.
},
onClose: () => {
// Modal dismissed — could be after success, mid-flow drop-off,
// or after an error. Use this to clean up UI on your page.
},
onError: (err) => {
// Network failure, malformed config, or an unexpected widget error.
// Forward to your monitoring (Sentry/Datadog/etc).
},
});Decision delivery (approved / declined / additional checks required) happens via the webhook configured in your NeoCash dashboard, keyed by applicationId. The widget itself does not surface the final decision to your page.
Mounting
By default the widget appends a <div> to <body> and renders an overlay on top of your page. To mount inside an existing element instead:
init({
publicKey,
cart,
container: '#checkout-slot', // selector
// or: container: document.getElementById('checkout-slot'),
});init() returns a handle so you can dismiss the widget programmatically:
const handle = init({ ... });
// some time later:
handle.close();Each init() call is independent — you can mount multiple instances on the same page if needed; closing one does not affect another.
Test mode
Public keys come in two flavors:
pk_test_...— sandbox. Uses test BVNs, test liveness, and never debits real accounts. Use this throughout development and CI.pk_live_...— production. Real identity checks, real money.
Switching modes is a key swap only; no other code changes are required.
TypeScript
Types ship in the package:
import type {
InitOptions,
ThemeOptions,
Cart,
CartItem,
PartnerPrefill,
WidgetHandle,
} from '@neocash/bnpl-widget';Peer dependencies (npm install only)
The identity-verification step uses AWS Amplify's FaceLivenessDetector, which is lazy-loaded so it never enters your main bundle.
If you install the widget via npm, also install the peers:
npm install @aws-amplify/ui-react-liveness aws-amplifyThe script-tag (unpkg) build loads these from CDN on demand, so drop-in embeds need no extra setup.
Browser support
Evergreen Chromium, Firefox, Safari (current and previous major). The liveness step requires camera access (getUserMedia) and so won't work in restricted browser contexts (iframes without allow="camera", etc.).
Support
Integration questions: [email protected] Dashboard, keys, webhooks: dashboard.neocash.ng
