diff --git a/.changeset/kkiapay-widget-flow.md b/.changeset/kkiapay-widget-flow.md index 9d5669f..031d49c 100644 --- a/.changeset/kkiapay-widget-flow.md +++ b/.changeset/kkiapay-widget-flow.md @@ -9,6 +9,7 @@ Kkiapay, and payments started by the customer in a widget. - `@payenv/core`: `nextAction.type === 'widget'`, `supportedWidgets` and `Capability.widget` for providers whose payments start in a widget; `payenv.confirm` verifies the provider transaction's merchant reference and amount before linking it. - New error code `REFERENCE_MISMATCH`. + New error code `REFERENCE_MISMATCH`. `toE164` converts phone numbers typed by customers + to E.164. - `@payenv/connector-kkiapay` (new): Kkiapay widget payments in Benin (MTN, Moov), server-side verification, webhook verification and parsing. diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd342b..d557ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Before `1.0.0`, minor versions may contain breaking changes. ## [Unreleased] ### Added +- 2026-10-06 — `@payenv/core`: `toE164(phone, country)` converts numbers typed by customers + (e.g. `61 00 00 00` in Benin) to E.164, with per-country trunk-prefix rules. - 2026-10-05 — `@payenv/core`: payments started by the customer in a widget (`nextAction.type === 'widget'`, `supportedWidgets`, `Capability.widget`) and `payenv.confirm`, which verifies the provider transaction's merchant reference and diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 570fdeb..128af8b 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -48,6 +48,14 @@ console.log(payment.status); // 'pending': the customer confirms on their phone In the FedaPay sandbox, `+22964000001` and `+22966000001` succeed; any other number simulates a failed payment. In live mode, the customer receives a USSD prompt and confirms with their PIN. +Payenv only accepts E.164 phone numbers. Convert what the customer typed with `toE164`: + +```ts +import { toE164 } from '@payenv/core'; + +toE164('61 00 00 00', 'BJ'); // '+22961000000' +``` + Invalid requests throw a `PayenvError` (`INVALID_PHONE`, `NO_ROUTE`, …). Everything that happens with the provider is returned in the payment instead. diff --git a/examples/fedapay-demo/page.ts b/examples/fedapay-demo/page.ts new file mode 100644 index 0000000..77245a1 --- /dev/null +++ b/examples/fedapay-demo/page.ts @@ -0,0 +1,139 @@ +/** The demo page. Kept apart from the server so a test can check its inline script. */ +export interface PageOptions { + live: boolean; + maxLiveAmount: number; + withKkiapay: boolean; +} + +export function page({ live, maxLiveAmount, withKkiapay }: PageOptions): string { + return ` + + + + +Payenv demo + + + +

Payenv demo

+

Paiement mobile money via FedaPay${withKkiapay ? ' et Kkiapay' : ''} (${live ? 'LIVE' : 'sandbox'})

+${live ? `

⚠️ Mode LIVE : vrai argent. Montant plafonné à ${maxLiveAmount} XOF.

` : ''} +
+ + + + ${live ? 'Votre vrai numéro, au format international (+229…). Vous recevrez une demande de validation USSD.' : 'Sandbox : +22964000001 ou +22966000001 = succès · tout autre numéro = échec'} + ${ + withKkiapay + ? ` + Widget Kkiapay (sandbox) : MTN 61000000 · Moov 68000000 = succès · …02 = fonds insuffisants` + : '' + } + +
+
+
Statut :
+

+

+
+${withKkiapay ? '' : ''} + + +`; +} diff --git a/examples/fedapay-demo/server.ts b/examples/fedapay-demo/server.ts index 72fe31a..67228c1 100644 --- a/examples/fedapay-demo/server.ts +++ b/examples/fedapay-demo/server.ts @@ -28,7 +28,9 @@ import { PayenvError, type PayenvOptions, type Payment, + toE164, } from '@payenv/core'; +import { page } from './page.ts'; const live = process.env.DEMO_LIVE === 'yes'; const maxLiveAmount = Number(process.env.DEMO_LIVE_MAX_AMOUNT ?? 200); @@ -120,7 +122,7 @@ async function route(request: IncomingMessage, response: ServerResponse): Promis if (request.method === 'GET' && url.pathname === '/') { response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - response.end(page()); + response.end(page({ live, maxLiveAmount, withKkiapay })); return; } @@ -145,7 +147,13 @@ async function route(request: IncomingMessage, response: ServerResponse): Promis const client = body.outage === 'on' ? payenvWithOutage : payenv; const payment = await client.collect({ amount: { value: Number(body.amount), currency: 'XOF' }, - method: { type: 'mobile_money', network: body.network, country: 'BJ', phone: body.phone }, + // Customers type local numbers: convert to E.164 before calling Payenv. + method: { + type: 'mobile_money', + network: body.network, + country: 'BJ', + phone: toE164(body.phone, 'BJ'), + }, customer: { firstName: 'Demo', lastName: 'Payenv', email: 'demo@example.com' }, description: live ? 'Payenv demo (live test)' : 'Payenv demo', idempotencyKey: `demo_${Date.now()}`, @@ -279,122 +287,3 @@ function send(response: ServerResponse, status: number, body: unknown): void { response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); } - -function page(): string { - return ` - - - - -Payenv demo - - - -

Payenv demo

-

Paiement mobile money via FedaPay${withKkiapay ? ' et Kkiapay' : ''} (${live ? 'LIVE' : 'sandbox'})

-${live ? `

⚠️ Mode LIVE : vrai argent. Montant plafonné à ${maxLiveAmount} XOF.

` : ''} -
- - - - ${live ? 'Votre vrai numéro, au format international (+229…). Vous recevrez une demande de validation USSD.' : 'Sandbox : +22964000001 ou +22966000001 = succès · tout autre numéro = échec'} - ${ - withKkiapay - ? ` - Widget Kkiapay (sandbox) : 61000000 ou 97000000 (MTN) = succès · 61000002 = fonds insuffisants` - : '' - } - -
-
-
Statut :
-

-

-
-${withKkiapay ? '' : ''} - - -`; -} diff --git a/examples/fedapay-demo/test/page.test.ts b/examples/fedapay-demo/test/page.test.ts new file mode 100644 index 0000000..8110b68 --- /dev/null +++ b/examples/fedapay-demo/test/page.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { page } from '../page.ts'; + +/** Inline