@bloonio/lokotro-pay-js
v1.1.0
Published
Framework-agnostic JavaScript SDK for Lokotro Pay - drop-in payment checkout widget and headless payment engine for React, Next.js, Vue, Svelte or plain JS. Cards, mobile money, e-wallets, bank transfer.
Maintainers
Readme
@bloonio/lokotro-pay-js
Framework-agnostic JavaScript SDK for Lokotro Pay — a drop-in payment
checkout widget plus a headless payment engine. Works in React / Next.js,
Vue, Svelte, Angular, or plain <script> pages. Cards (Mastercard Hosted
Session), mobile money, e-wallets, Flash, and bank transfer.
This package is the JS equivalent of the @bloonio/lokotro-pay Angular SDK:
same gateway flow, same models, same theming — no framework required.
✨ Features
- 🧩 Framework-agnostic — vanilla DOM widget + headless core, zero runtime dependencies
- ⚛️ React/Next.js-ready — mount in a
useEffect; SSR-safe imports;useSyncExternalStore-compatible state - 🎨 Clean white-surface UI — neutral payment look out of the box, fully themeable, styles scoped to the widget
- 🌍 Multi-language — English, French, Chinese (中文)
- 💳 Multiple payment methods — Card (Hosted Session), Mobile Money, E-Wallet, Flash, Bank Transfer
- 🔒 Secure by default — fail-closed environment selection, redirect URL allow-listing, no sensitive logging
- 🎯 Type safe — full TypeScript declarations, ESM + CJS builds
📦 Installation
npm install @bloonio/lokotro-pay-js🚀 Quick Start (plain JS)
import { LokotroCheckout } from '@bloonio/lokotro-pay-js';
const checkout = new LokotroCheckout({
configs: {
token: 'your_session_token', // minted by YOUR backend (see Authentication)
isProduction: false, // REQUIRED: true = production, false = sandbox
acceptLanguage: 'en',
},
paymentBody: {
paymentMethod: 'wallet',
userInfo: 'full',
paymentMethodInfo: 'none',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '2439978540000',
email: '[email protected]',
},
language: 'en',
onResponse: (response) => console.log('Payment successful!', response),
onError: (error) => console.error('Payment failed:', error.message),
onClosed: () => console.log('Widget closed'),
});
checkout.mount('#checkout'); // element or CSS selector
// later:
checkout.unmount();The widget injects its own scoped stylesheet on first mount — no CSS import
needed. All styles live under .lokotro-pay-root and cannot leak into your app.
⚛️ React / Next.js
The package never touches window or document at import time, so it is safe
to import in Next.js (App Router or Pages Router). Mount in an effect:
'use client';
import { useEffect, useRef } from 'react';
import { LokotroCheckout, type LokotroPayOnResponse } from '@bloonio/lokotro-pay-js';
export function PayButton({ sessionToken }: { sessionToken: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const checkout = new LokotroCheckout({
configs: { token: sessionToken, isProduction: false },
paymentBody: { paymentMethod: 'mobile_money' },
onResponse: (response: LokotroPayOnResponse) => {
// navigate to your success page, update order state, etc.
},
onError: (error) => console.error(error.message),
});
checkout.mount(containerRef.current);
return () => checkout.unmount();
}, [sessionToken]);
return <div ref={containerRef} />;
}Headless usage (custom UI)
Skip the widget entirely and drive the payment state machine yourself. The
(getState, subscribe) pair plugs straight into useSyncExternalStore:
import { useSyncExternalStore, useMemo } from 'react';
import {
LokotroHttpClient,
LokotroPaymentService,
LokotroPayEnv,
} from '@bloonio/lokotro-pay-js';
function usePayment(token: string) {
const service = useMemo(() => {
LokotroPayEnv.initialize(false); // sandbox
const client = new LokotroHttpClient();
const svc = new LokotroPaymentService(client);
svc.setAppKey(token);
return svc;
}, [token]);
const state = useSyncExternalStore(
(cb) => service.subscribe(cb, { emitCurrent: false }),
service.getState,
service.getState,
);
return { service, state };
}state.currentScreen tells you what to render (loadingScreen,
paymentMethodSelectionScreen, ewalletOtpScreen, successScreen, …) and the
service exposes the same operations the widget uses: createPayment,
selectPaymentMethod, submitPaymentDetails, verifyOtp, resendOtp,
fetchAvailableBanks, confirmBankTransferSwitch.
🔑 Authentication
configs.token is a session token minted server-to-server by your backend
against the Lokotro Gateway's session endpoint. Never embed long-lived API
keys in browser code — your backend creates a short-lived session bound to the
amount/currency/reference, and hands the token to the page.
📖 Configuration
configs (LokotroPayConfig)
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| token | string | Yes | – | Session token from your backend |
| isProduction | boolean | Yes | – | true → production gateway (real money), false → sandbox (simulated). No default — omitting it throws. |
| customApiUrl | string | No | – | Custom gateway URL (e.g. https://api.example.com) |
| timeout | number | No | 30000 | Request timeout (ms) |
| acceptLanguage | string | No | 'fr' | Language for API responses |
paymentBody (LokotroPaymentBody)
Same shape as the Angular SDK — payment method, user info mode, payment-method info mode, prefill fields per channel (wallet number/PIN, mobile-money phone, card holder info, flash number/PIN), redirect/notify URLs, merchant branding and metadata. All fields optional when your session token already binds amount/currency/reference.
| userInfo / paymentMethodInfo | Behavior |
|----------------------------------|----------|
| 'full' | Values are sent with the request; the corresponding form is skipped |
| 'none' | The widget shows a form and collects the values from the customer |
Widget options (LokotroCheckoutOptions)
| Option | Type | Description |
|--------|------|-------------|
| title | string | Optional header title (header renders only when set) |
| themeConfig | LokotroPayThemeConfig | Brand colors/typography — see Theming |
| language | 'en' \| 'fr' \| 'zh' | UI language (defaults to browser language) |
| darkTheme | boolean | Opt-in dark surface |
| enableHapticFeedback | boolean | Vibrate on selection where supported (default true) |
| onResponse | (r: LokotroPayOnResponse) => void | Success callback — fires when the payment reaches the success screen, and again on Done/auto-redirect |
| onError | (e: LokotroPayOnError) => void | Failure callback |
| onClosed | () => void | Widget dismissed (close, cancel, done, auto-redirect) |
🎨 Theming
Pass your colours once as themeConfig; every part of the widget reads them —
buttons and gradients, inputs and focus rings, spinners and loaders,
processing steps, selected states, the result screens and the payment-status
widget. Only the keys you set are overridden, and everything is scoped to the
widget root. The keys are the same as the Flutter SDK's LokotroPayThemeConfig
and @bloonio/lokotro-pay (Angular), so one brand definition serves every
platform.
Example — eBlood red:
import type { LokotroPayThemeConfig } from '@bloonio/lokotro-pay-js';
const ebloodTheme: LokotroPayThemeConfig = {
primaryColor: '#FF0000',
secondaryColor: '#D32F2F',
tertiaryColor: '#E57373',
};
new LokotroCheckout({ configs, paymentBody, themeConfig: ebloodTheme }).mount('#checkout');
new LokotroPaymentStatusWidget({ statusConfig, themeConfig: ebloodTheme }).mount('#status');| Key | CSS custom properties | Drives | Default |
|-----|-----------------------|--------|---------|
| primaryColor | --lokotro-primary | Primary buttons and their gradient start, focused input borders, spinner rings, button glow | #0F172A |
| secondaryColor | --lokotro-secondary | Gradient end of primary buttons | #475569 — follows primaryColor when omitted |
| tertiaryColor | --lokotro-tertiary, --lokotro-accent | Selected payment method, focus rings, loader pulse, phone-prefix chips, links | #2563EB — follows primaryColor when omitted |
| onPrimaryColor | --lokotro-on-primary | Text and icons on brand fills | #FFFFFF |
| backgroundColor | --lokotro-background | Widget background | #FFFFFF |
| surfaceColor | --lokotro-surface, --lokotro-card | Cards, inputs, method tiles, summaries | #F7F8FA / #FFFFFF |
| textColor | --lokotro-text-primary | Body text | #0F172A |
| secondaryTextColor | --lokotro-text-secondary | Labels, hints | #475569 |
| borderColor | --lokotro-border | Borders | #E2E8F0 |
| errorColor | --lokotro-error | Error text, error result | #DC2626 |
| successColor | --lokotro-success | Success result | #16A34A |
| warningColor | --lokotro-warning | Pending status, warning result | #D97706 |
| infoColor | --lokotro-info | Info result | #2563EB |
| borderRadius | --lokotro-radius-sm / -md / -lg | Corners of buttons, inputs, cards. A number is px; small and large scale ×2/3 and ×4/3 | 12px (8 / 12 / 16) |
| fontFamily | --lokotro-font-family | Typography | system font stack |
Tints — selected backgrounds, focus rings, status halos, button glow — are
mixed from these colours at runtime with color-mix(), so they follow the
brand too. Browsers without color-mix() (before Chrome 111 / Safari 16.2 /
Firefox 113) drop the tint and keep the solid colours.
The same properties can be set from your own CSS on .lokotro-pay-root. Set
that way there is no fallback chain: give --lokotro-secondary and
--lokotro-tertiary explicitly, or they keep their defaults.
lokotroThemeToCssVariables(theme) returns the property map (handy for SSR or
framework wrappers) and LOKOTRO_THEME_CSS_VARIABLES lists which properties
each key sets. demo/index.html shows the eBlood theme next to the default.
💳 Payment Methods
| Method | paymentMethod value | Notes |
|--------|----------------------|-------|
| E-Wallet | wallet | Wallet number + PIN, OTP verification |
| Card | card | Mastercard Hosted Session — card data never touches your page or this SDK |
| Mobile Money | mobile_money | Push-to-phone with automatic status polling |
| Flash | flash | Flash number + PIN |
| Bank Transfer | bank_transfer | City → bank → account selection, proof upload by email link |
When the chosen method can't carry the amount, the gateway may offer a bank-transfer switch; the widget shows a confirm screen — nothing is switched silently.
📡 Payment Status Tracking
Track a payment by ID only (e.g. initiated from a mobile app or server):
import { LokotroPaymentStatusWidget } from '@bloonio/lokotro-pay-js';
const status = new LokotroPaymentStatusWidget({
statusConfig: {
paymentId: 'your-payment-id',
config: { token: 'your_session_token', isProduction: false },
pollingInterval: 5000, // default 5s
maxPollingAttempts: 60, // default 60 (≈5 minutes)
},
themeConfig: ebloodTheme, // optional, same keys as the checkout
onStatusChange: (r) => console.log('status:', r.status),
onPaymentComplete: (r) => console.log('completed!', r),
onPaymentFailed: (r) => console.error('failed:', r.message),
onClose: () => {},
});
status.mount('#status');Headless equivalent: LokotroPaymentStatusTracker (same polling logic, no UI).
🌐 Languages
| Language | Code |
|----------|------|
| English | en |
| French | fr |
| Chinese (Simplified) | zh |
Unknown codes are ignored gracefully (the widget stays on its current
language). Custom strings can be patched per language via
LokotroLocalizationService.addTranslations.
🔒 Security Notes
- Fail-closed environments:
isProductionhas no default. Omitting it is a TypeScript error and a runtime error before any network call. - Redirect allow-listing: server-supplied redirect/hosted-checkout URLs are validated (http/https only; https-only in production) before navigation.
- No sensitive logging: the SDK never logs tokens, request bodies or response bodies.
- Hosted card sessions: for
card, the SDK mints a hosted-checkout session; PAN/CVV/expiry are entered on the card network's own page.
📋 Requirements
- Browsers: modern evergreen (uses
fetch,AbortController, CSS custom properties) - Node 18+ for SSR imports (widgets themselves require a browser to
mount())
📄 License
MIT © Bloonio
