USSD prompt
Asks the user to approve a Mobile Money payment on their phone. It shows the approval code with copy and dial actions, an animated waiting state, a countdown, and a retry button once the request times out.
Preview
The code *123# is a made-up example. On a phone, “Dial now” opens the dialer with it filled in.
Installation
npx shadcn@latest add @mpui-kit/ussd-promptRequires the @mpui-kit namespace. See Getting started.
Usage
import { UssdPrompt } from "@/components/mpui-kit/ussd-prompt"
// Inside <MpuiKitProvider country={cm}>
const [expiresAt, setExpiresAt] = useState<number>()
// When your backend has started the payment:
setExpiresAt(Date.now() + 90_000)
{expiresAt && (
<UssdPrompt
code="*123#" // optional: shown with copy and dial actions
phone="+237651234567" // optional: "Request sent to +237 6 51 23 45 67."
expiresAt={expiresAt}
onExpire={() => console.log("timed out")}
onRetry={() => {
// start the payment again, then:
setExpiresAt(Date.now() + 90_000)
}}
/>
)}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| code | string | — | The USSD code to dial if the prompt does not reach the phone, such as "*123#". Leave it out when the operator pushes the prompt without a code. A "Dial now" link is added only if the code is dialable. |
| expiresAt | number | — | When the request times out, as a Unix time in milliseconds. Starts the countdown. Without it the prompt waits indefinitely. |
| durationMs | number | — | The total time allowed. Adds a bar that shrinks smoothly as time runs out. Without it there is only the countdown text. |
| phone | string | — | The number the request was sent to, shown as a reminder. It never wraps. |
| onRetry | () => void | — | Called when the user presses “Try again” after the timeout. No button without it. |
| onExpire | () => void | — | Called once when the countdown reaches zero. |
| country | CountryConfig | MpuiKitProvider's country | Only used to format phone. |
| locale | "fr" | "en" | MpuiKitProvider's locale, then “fr” | Language of the prompt. |
| classNames | { indicator, title, phone, codeBox, code, status, bar, countdown, retry } | — | Class names for parts of the prompt. Each part also has a data-slot attribute, such as ussd-prompt-code. |
| ...props | ComponentProps<'section'> | — | Other section props, such as className. |
useCountdown
The countdown is a separate hook, installed with the prompt. It works out the time left from the real clock instead of counting ticks, so it stays correct when the browser throttles timers, and it re-syncs the moment the tab is visible again. That matters on phones: people leave the page to dial, and mobile browsers pause background tabs.
import { useCountdown } from "@/hooks/mpui-kit/use-countdown"
const remainingMs = useCountdown(expiresAt, { onExpire: () => console.log("done") })
// 0 once the time is up; pass null for "no countdown"Notes
- State only. The prompt never calls a payment API. You start the payment, poll for the result and decide the outcome. The checkout block does that for you.
- Accessible countdown. The ticking numbers are hidden from screen readers. A polite live region announces milestones instead: each minute, then at 60, 30 and 10 seconds.
- Motion. While waiting, a ring ripples out from the phone icon and the bar shrinks smoothly. Both are skipped for users who have asked for reduced motion. The icon and the countdown text stay.
- Focus. If keyboard focus was on the copy button when the request timed out, it moves to “Try again” instead of being dropped.
- Dial link. It uses
tel:with#encoded as%23, so the code is not cut off. It only appears for text that is a dialable code. On a desktop browser it opens whatever handles phone links, if anything. - Copy. Uses the Clipboard API and falls back to a hidden textarea for older Android browsers and pages served over http.
- A late approval on the phone after the timeout is not something this component can know about. Tell users what happens, or keep polling your backend.