Headless and framework-free
The checkout logic does not depend on React or on any UI. Use it with Vue, Svelte, Solid or plain JavaScript, or build your own React interface on the hooks.
Three layers
Core
Controllers and pure helpers with no UI and no dependencies: createCheckoutController, createPhoneField, createCountdown, formatFcfa, validatePhone, the FR/EN dictionary and the Cameroon data.
registry/mpui-kit/lib, and the @mpui-kit/core npm package built from it
Hooks
Thin React wrappers over the core: useMomoCheckout, usePhoneField, useCountdown. They render nothing, so you bring your own markup.
registry/mpui-kit/hooks
Components
The shadcn/ui components, built on the hooks.
registry/mpui-kit/components
Every controller has the same small shape: getSnapshot() returns the current state (the same object until something changes) and subscribe(listener) tells you when it changes. That is exactly what useSyncExternalStore, a Vue shallowRef, a Svelte store or a plain render() need.
Plain JavaScript, running
This page loads no React and no framework. It is a single HTML file that imports the core bundle, with a simulated payment. It is also checked in a real browser in this repository.
import { cm, createCheckoutController, createPhoneField, formatFcfa } from "@mpui-kit/core"
const field = createPhoneField({ country: cm })
input.addEventListener("input", () => {
const { formatted, caretIndex } = field.input(input.value, input.selectionStart ?? undefined)
input.value = formatted
if (caretIndex !== null) input.setSelectionRange(caretIndex, caretIndex)
})
const checkout = createCheckoutController({
amount: 25000,
currency: "XAF",
onPay: async ({ methodId, phone, payload }) => {
// Call YOUR backend. MPUI-KIT never talks to a payment provider.
const { reference } = await startPayment({ methodId, phone, payload })
return { status: "pending", reference }
},
onCheckStatus: async (reference) => fetchStatus(reference),
})
checkout.subscribe(() => render(checkout.getSnapshot()))
checkout.pay({ methodId: "mtn", phone: field.getSnapshot().validation.e164 })React, Vue and Svelte
import { useSyncExternalStore } from "react"
const { state } = useSyncExternalStore(checkout.subscribe, checkout.getSnapshot)The Vue and Svelte adapters below are sketches. They are not run in this repository's tests, but the controller does the same thing in every framework, so only the wiring differs.
// SKETCH: not run in this repository.
import { shallowRef, onUnmounted } from "vue"
export function useCheckout(options) {
const controller = createCheckoutController(options)
const snapshot = shallowRef(controller.getSnapshot())
const stop = controller.subscribe(() => (snapshot.value = controller.getSnapshot()))
onUnmounted(() => {
stop()
controller.destroy()
})
return { snapshot, pay: controller.pay, retry: controller.retry, cancel: controller.cancel }
}// SKETCH: not run in this repository.
// Svelte calls the callback with the current value straight away, so wrap getSnapshot.
import { readable } from "svelte/store"
const snapshot = readable(controller.getSnapshot(), (set) =>
controller.subscribe(() => set(controller.getSnapshot()))
)The npm package
@mpui-kit/core is built from the same source as the registry, as ESM and CommonJS with type declarations, and published on npm: npm install @mpui-kit/core. Cameroon's operator prefixes were verified against the ART numbering plan on 2026-09-28; region and city names are still unverified community data.
The core never calls a payment provider and never asks for card details. Always confirm a payment on your server before you deliver anything.