checkout-page (page hébergée)
À quoi ça sert
Section intitulée « À quoi ça sert »checkout-page est la page web publique vers laquelle un marchand redirige son client pour payer. C’est la version « hébergée » du checkout : le marchand n’intègre rien dans sa propre page, il ouvre simplement une URL Titreo E-Pay et l’utilisateur termine son paiement sur un domaine que nous contrôlons.
Cette page ne contient aucune logique de paiement : elle est une coquille (un shell) qui charge dynamiquement le checkout-sdk (un Web Component) à partir d’un manifest de bootstrap signé servi par le backend. Tout ce qui touche aux PSP, aux cartes cadeaux et au split-tender vit dans le SDK et les adapters clients, pas ici.
En une phrase : checkout-page traduit une sessionId + un token client en une page de paiement complète, sécurisée et thémée.
Le code se trouve dans checkout-page/. Pile : Next.js 15 (App Router), React 19, TypeScript, Tailwind, Biome. Aucun framework de paiement ici — uniquement le rendu et le montage.
Position dans le flux
Section intitulée « Position dans le flux »Le marchand prépare la session côté serveur (avec sa clé privée), échange contre un token client de courte durée, puis redirige le navigateur vers cette page.
sequenceDiagram
participant M as "Backend marchand"
participant API as "Backend E-Pay (:3000)"
participant N as "checkout-page (:3100)"
participant B as "Navigateur client"
M->>API: "POST /v1/sessions (apiKey privée)"
API-->>M: "{ id: sess_xxx }"
M->>API: "POST /v1/sessions/:id/client-token"
API-->>M: "{ token: tepc_yyy }"
M-->>B: "redirect /fr/c/sess_xxx?t=tepc_yyy"
B->>N: "GET /fr/c/sess_xxx?t=tepc_yyy"
N->>API: "GET /v1/sessions/:id (Bearer tepc_)"
N->>API: "GET /v1/sessions/:id/bootstrap (Bearer tepc_)"
API-->>N: "session + manifest signé"
N-->>B: "HTML + monte <titreo-epay-checkout>"
B->>API: "paiement piloté par le SDK"
Le client n’a jamais la clé privée du marchand : il ne voit que le token public tepc_ (HMAC court, ~15 min, lié à la sessionId). Voir Sécurité et Contrats inter-briques.
Structure (App Router + locale)
Section intitulée « Structure (App Router + locale) »L’App Router de Next.js mappe l’arborescence de checkout-page/app/ sur des routes. Le segment [locale] (fr ou en) est validé par isLocale() ; toute autre valeur déclenche un notFound().
| Fichier | Route | Rôle |
|---|---|---|
app/page.tsx |
/ |
Redirige vers /{defaultLocale} (fr) |
app/[locale]/page.tsx |
/fr, /en |
Page d’accueil minimale (rappelle d’ouvrir une session) |
app/[locale]/c/[sessionId]/page.tsx |
/fr/c/sess_xxx?t=... |
Page checkout principale (monte le Web Component) |
app/[locale]/c/[sessionId]/success/page.tsx |
.../success |
Confirmation de paiement |
app/[locale]/c/[sessionId]/cancel/page.tsx |
.../cancel |
Annulation utilisateur |
app/[locale]/c/[sessionId]/error/page.tsx |
.../error |
Échec de paiement |
app/layout.tsx |
racine | <html>, polices Google (Inter, JetBrains Mono), robots: noindex |
Les composants de présentation sont dans checkout-page/components/ : CheckoutShell.tsx (le seul 'use client', monte le SDK), MerchantHeader.tsx, SecuredFooter.tsx, Icon.tsx. L’i18n est un simple dictionnaire fr/en dans lib/i18n.ts (fonction t(locale, key)), pas de librairie externe.
Le serveur : lib/api.ts et lib/config.ts
Section intitulée « Le serveur : lib/api.ts et lib/config.ts »lib/config.ts — où sont lues les variables d’environnement
Section intitulée « lib/config.ts — où sont lues les variables d’environnement »Conformément aux conventions du projet, process.env n’est lu qu’ici. Le module exporte un objet env figé :
Clé env |
Variable lue | Usage |
|---|---|---|
apiBaseUrl |
TITREO_API_BASE_URL |
Fetch côté serveur (SSR). Peut viser un hostname Docker interne. |
apiBaseUrlPublic |
NEXT_PUBLIC_TITREO_API_BASE_URL (fallback TITREO_API_BASE_URL) |
Fetch côté navigateur : doit être une URL publique. Passée au SDK. |
cdnAdyenAdapterUrl |
NEXT_PUBLIC_ADAPTER_ADYEN_URL |
Déclarée dans config.ts mais pas encore câblée (réservée à un futur override d’URL d’adapter ; aujourd’hui le manifest fait foi). |
defaultLocale |
— | 'fr' |
supportedLocales |
— | ['fr', 'en'] |
La distinction apiBaseUrl / apiBaseUrlPublic est importante : le serveur Next.js et le navigateur du client ne joignent pas forcément le backend par la même adresse (typique en Docker). Une troisième variable, NEXT_PUBLIC_BOOTSTRAP_PUBKEY_PEM, contient la clé publique Ed25519 (PEM SPKI) embarquée au build ; elle est lue dans components/CheckoutShell.tsx pour vérifier la signature du manifest. On la génère côté API avec pnpm keygen:bootstrap.
lib/api.ts — les deux appels serveur
Section intitulée « lib/api.ts — les deux appels serveur »Deux fonctions seulement, toutes deux exécutées côté serveur (Server Component) avec cache: 'no-store' et le token client en Authorization: Bearer :
export function fetchSession(sessionId: string, token: string): Promise<SessionDto>// GET /v1/sessions/:id → { id, status, totalAmount, currency, expiresAt, merchantId, metadata, ... }
export function fetchBootstrap(sessionId: string, token: string): Promise<ManifestDto>// GET /v1/sessions/:id/bootstrap → manifest signéLe ManifestDto décrit ce que la page doit charger : psp (type, module_url, integrity SRI, config), gift[], checkout_sdk, des theme_tokens optionnels, et surtout key_id + signature pour la vérification Ed25519. C’est le contrat de bootstrap entre le backend et le checkout — voir Contrats inter-briques. Un appel HTTP non-2xx lève une erreur, capturée par la page pour afficher un écran d’erreur plutôt que de planter.
Le flux de la page checkout
Section intitulée « Le flux de la page checkout »La route principale app/[locale]/c/[sessionId]/page.tsx est un Server Component (export const dynamic = 'force-dynamic'). Elle enchaîne :
- Lit
locale,sessionId(params) et le tokentainsi quesuccess/cancel/merchant(searchParams). Token absent → écran d’erreur immédiat. - Appelle en parallèle
fetchSessionetfetchBootstrap(Promise.all). Échec → écran d’erreur. - Convertit les
theme_tokensdu manifest en variables CSS (themeTokensToCss, avec liste blanche de caractères pour éviter l’injection), extrait le nom/logo du marchand depuissession.metadata(extractMerchantInfo, schémas d’URL restreints àhttps:///http:///data:image/), formate le montant avecIntl.NumberFormat. - Rend la mise en page (en-tête marchand, montant, récapitulatif) et délègue le paiement à
<CheckoutShell>.
flowchart TD
A["GET /fr/c/sess_xxx?t=tepc_"] --> B{"token présent ?"}
B -- non --> E["ErrorView"]
B -- oui --> C["Promise.all(fetchSession, fetchBootstrap)"]
C -- erreur --> E
C -- ok --> D["rend shell + theme tokens"]
D --> F["<CheckoutShell> (client)"]
F --> G["import @titreo-epay/checkout + loader"]
G --> H["loadFromManifest(manifest, pubKey)"]
H --> I["<titreo-epay-checkout> piloté par le SDK"]
CheckoutShell.tsx — le pont vers le SDK
Section intitulée « CheckoutShell.tsx — le pont vers le SDK »C’est le seul composant client ('use client'). À son montage (useEffect) il :
import('@titreo-epay/checkout')— charge le Web Component (shell Lit) du checkout-sdk.import('@titreo-epay/checkout/loader')puisloadFromManifest(manifest, { pubKey })— vérifie la signature Ed25519 du manifest avec la clé publique embarquée (parseEd25519PubKeyPem), puis charge les modules adapter avec contrôle d’intégrité SRI sha-384.- Rend l’élément personnalisé
<titreo-epay-checkout>avec les attributssession-id,api-base-url(l’URL publique),api-token(letepc_) etlocale, plus le mapping des variables CSS de thème (--titreo-*).
Le shell écoute ensuite deux événements DOM émis par le SDK :
| Événement DOM | Effet dans la page |
|---|---|
titreo-epay:completed |
Passe en phase completed, puis redirige vers successUrl (ou /{locale}/c/{id}/success) après ~4 s |
titreo-epay:failed |
Affiche l’erreur (detail.reason) ; si cancelUrl fourni, redirige après ~2 s |
Le délai sur le succès est volontaire : il laisse l’utilisateur voir la confirmation du widget avant le retour vers la boutique.
Lancer en local
Section intitulée « Lancer en local »Port 3100 (le backend tourne sur 3000) — voir le tableau des ports et l’environnement de dev local.
pnpm installpnpm dev # next dev -p 3100Configurer l’environnement (copier .env.example → .env.local) :
TITREO_API_BASE_URL=http://localhost:3000NEXT_PUBLIC_BOOTSTRAP_PUBKEY_PEM="-----BEGIN PUBLIC KEY-----MCowBQYDK2VwAyEA<remplacer>-----END PUBLIC KEY-----"Préparer une session de test côté backend, puis ouvrir la page :
curl -X POST http://localhost:3000/v1/sessions \ -H "Authorization: Bearer tep_live_xxx" -H "Content-Type: application/json" \ -d '{"totalAmount": 5000, "currency": "EUR"}' # → { id: "sess_xxx" }
curl -X POST http://localhost:3000/v1/sessions/sess_xxx/client-token \ -H "Authorization: Bearer tep_live_xxx" # → { token: "tepc_yyy" }
open "http://localhost:3100/fr/c/sess_xxx?t=tepc_yyy"Scripts npm disponibles (package.json) : dev, build, start (3100), typecheck (tsc --noEmit), lint / lint:fix (Biome).
Déploiement
Section intitulée « Déploiement »La route checkout est compatible edge runtime (Vercel, Cloudflare Pages) : ajouter export const runtime = 'edge' sur la route si besoin. Pour un domaine marchand personnalisé (ex. pay.marchand.com), utiliser un rewrite Next + le mapping de domaine vers checkout-page.