MPUI-KIT

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.