Angular
provideWajub, le service, les composants standalone et la configuration Angular SSR associée.
@wajub/angular enveloppe @wajub/js avec la syntaxe Angular : provideWajub() et un service
qui charge une fois l'environnement d'exécution, des composants standalone pour le checkout et pour
chacun des sept groupes de champs, et injectConfirmPayment(). L'API de paiement ne change pas. Le
comportement de chaque option est présenté dans
Afficher le checkout et
Champs de paiement. Cette page traite des éléments propres
à Angular.
@wajub/angular
Stable · GAnpm
- Version
- 1.8.1
- Runtime
- Angular 17+ · Angular SSR
Couvre
- Paiements
- Liens de paiement
Absent de ce paquet : Facturation, Transferts, Sync, Shield, Taxes.
Installer
@wajub/js est une dépendance homologue, non intégrée. Installez-la donc sur la même ligne. Le
package demande @wajub/js 1.8.0 ou une version ultérieure, et Angular 17 ou une version
ultérieure, @angular/core et @angular/common. Il est publié depuis la 1.8.1, uniquement en ESM,
et déclare sideEffects: false.
npm install @wajub/angular @wajub/jsIl est compilé dans le mode de compilation partielle d'Angular avec Angular 17 : l'Angular de votre projet, 17 ou toute version ultérieure, le lie au moment du build. Il fonctionne avec zone.js comme dans une application zoneless, et s'affiche sur le serveur, hydratation comprise.
provideWajub et le service
WajubService charge l'environnement d'exécution une seule fois, dans le navigateur uniquement,
dès sa première injection, que fait un composant Wajub. provideWajub() le configure : dans
app.config.ts pour toute l'application, ou dans les providers d'une route, qui reçoit alors son
propre service. Sans lui, les composants utilisent le service de l'application, qui charge
l'environnement d'exécution depuis js.wajub.com.
import { type ApplicationConfig } from '@angular/core';
import { provideWajub } from '@wajub/angular';
export const appConfig: ApplicationConfig = {
providers: [provideWajub()],
};| Option | Type | Valeur par défaut | Fonction |
|---|---|---|---|
loadOptions | { jsOrigin?, jsUrl? } | Valeurs du CDN | Remplace l'origine de chargement de l'environnement d'exécution |
defer | boolean | false | Ne charge rien tant que vous n'appelez pas load() sur le service |
Contrairement au provider des packages React, Vue et Svelte, defer fonctionne ici : load()
garde l'environnement d'exécution dans le service, et les composants de la page se montent avec
lui. Wajub.js se charge une seule fois par page : une fois là, load() avec d'autres options ne
charge aucune autre copie, et la première origine ou URL reste. Un chargement échoué est retenté à
l'appel suivant.
getRuntime() génère une erreur pendant le chargement
Il génère une erreur jusqu'à l'arrivée du script, de nouveau si le chargement échoue, et toujours
sur le serveur. Lisez runtime(), un signal qui vaut null tant que l'environnement n'est pas
chargé, sauf si vous savez qu'il l'est déjà.
import { Component, Input, inject } from '@angular/core';
import { WajubService } from '@wajub/angular';
@Component({
selector: 'app-upgrade-button',
standalone: true,
template: `
<button type="button" [disabled]="!wajub.runtime()" (click)="upgrade()">Upgrade plan</button>
`,
})
export class UpgradeButtonComponent {
@Input({ required: true }) sessionId!: string;
readonly wajub = inject(WajubService);
upgrade(): void {
this.wajub.runtime()?.wajub.open({
sessionId: this.sessionId,
onSuccess: () => window.location.assign('/account'),
});
}
}C'est le signal qui fait fonctionner ce bouton : il vaut null à la création du composant, puis il
se remplit à l'arrivée de l'environnement d'exécution. Sur le serveur, il reste à null, et rien
n'y est chargé.
| Membre | Description |
|---|---|
runtime | Un signal de { wajub, Wajub, WajubError }, null tant que l'environnement n'est pas chargé, et sur le serveur |
runtime$ | La même valeur en observable : l'environnement actuel tout de suite, puis chaque nouveau |
loading, error | Des signaux : l'environnement est en cours de chargement, et la raison de l'échec du dernier chargement, null sinon |
load(options?) | Une promesse de l'environnement, null sur le serveur. options vaut par défaut les loadOptions de provideWajub(), et ne compte que pour le premier chargement de la page |
getRuntime() | L'environnement. Génère une erreur pendant le chargement, après un chargement échoué et sur le serveur |
WAJUB_OPTIONS est le jeton d'injection que définit provideWajub(), si vous préférez lire
vous-même ses options.
Intégration du checkout
<wajub-checkout-embed>, la classe WajubCheckoutEmbedComponent, est le checkout hébergé dans
votre propre mise en page. Comme en Vue, ses inputs forment une liste fermée : sessionId,
minHeight, locale, embedOrigin, theme, appearance, layout, loadingText et
showLoading. Ses outputs sont les onze callbacks de
EmbeddedConfig, nommés sans on.
import { Component, Input } from '@angular/core';
import { WajubCheckoutEmbedComponent, type WajubError } from '@wajub/angular';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [WajubCheckoutEmbedComponent],
template: `
<wajub-checkout-embed
[sessionId]="sessionId"
layout="tabs"
[minHeight]="480"
(success)="paid()"
(error)="failed($event)"
></wajub-checkout-embed>
`,
})
export class CheckoutComponent {
@Input({ required: true }) sessionId!: string;
paid(): void {
window.location.assign('/order/complete');
}
failed(error: WajubError | Record<string, unknown>): void {
console.error(error.code, error.message);
}
}| Input ou output | Type | Valeur par défaut | Fonction |
|---|---|---|---|
sessionId | string | Obligatoire | La valeur authorization_token de POST /payments |
minHeight | number | 480 | Hauteur minimale de l'hôte en pixels, jusqu'à ce que le checkout fixe la sienne |
(ready) | CheckoutInstance | aucune | Le checkout est prêt, avec l'instance à piloter ensuite |
(resize) | number | aucune | La nouvelle hauteur du checkout en pixels, le nombre que reçoit le onResize de mount() |
Les autres outputs sont (success), (error), (loadError), (cancel), (expired),
(stateChange), (methodChange), (breakdown) et (close). L'élément hôte est le conteneur, en
display: block et pleine largeur : placez-y votre class et votre style. Le composant expose
aussi l'instance montée sous checkoutInstance, null avant le montage, pour un viewChild ou une
référence de template.
Ce que les quatre wrappers ont en commun, chaque input qui atteint le checkout par update() et une
nouvelle session qui charge un nouveau checkout, est décrit dans
Afficher le checkout, avec cet
exemple dans chaque framework.
Champs de paiement
Les sept composants affichent chacun un groupe de champs : <wajub-card>, <wajub-mobile-money>,
<wajub-wallet>, <wajub-payment>, <wajub-address>, <wajub-bank-transfer> et
<wajub-crypto>, les classes WajubCardComponent à WajubCryptoComponent. <wajub-component>,
la classe WajubComponentEmbedComponent, est le même composant avec un input type obligatoire,
lorsque le choix intervient pendant l'exécution. Chaque option de
ComponentConfig est un input du même nom, et chaque callback
un output nommé sans on.
import { Component, Input, signal } from '@angular/core';
import {
WajubAddressComponent,
WajubPaymentComponent,
injectConfirmPayment,
type ComponentInstance,
} from '@wajub/angular';
@Component({
selector: 'app-pay-form',
standalone: true,
imports: [WajubAddressComponent, WajubPaymentComponent],
template: `
<wajub-address [sessionId]="sessionId" addressMode="shipping"></wajub-address>
<wajub-payment
[sessionId]="sessionId"
collectAddress="shipping"
[collectName]="true"
(instance)="field.set($event)"
></wajub-payment>
<button type="button" [disabled]="payment.isConfirming() || !field()" (click)="pay()">
{{ payment.isConfirming() ? 'Processing…' : 'Pay now' }}
</button>
`,
})
export class PayFormComponent {
@Input({ required: true }) sessionId!: string;
// Lue à chaque confirm() : une session qui change est suivie.
readonly payment = injectConfirmPayment(() => this.sessionId);
// Un signal : (instance) émet null en pleine détection des changements quand le formulaire part.
readonly field = signal<ComponentInstance | null>(null);
pay(): void {
const field = this.field();
if (field) this.payment.confirm(field);
}
}| Input ou output | Type | Valeur par défaut | Fonction |
|---|---|---|---|
type | ComponentType | Obligatoire | Uniquement sur <wajub-component> |
loader | 'skeleton' ou 'none' | 'skeleton' | 'skeleton' : l'hôte garde la hauteur du formulaire dès le premier rendu, avec un squelette dans le style de vos champs, et le formulaire apparaît en fondu une fois prêt. 'none' : l'iframe telle qu'elle arrive, la valeur par défaut de @wajub/js lui-même |
minHeight | number | Non définie | Hauteur minimale de l'hôte en pixels, gardée pendant toute la vie du composant. Un min-height dans le style de l'hôte, en attribut ou en binding, compte de la même façon. Non définie, voir ci-dessous |
readyTimeout | number | Non définie | Millisecondes d'attente du formulaire. Au-delà, (loadError) reçoit une WajubError de code ready_timeout. Non définie, jamais |
beforeSubmit | () => boolean | Promise<boolean> | aucune | onBeforeSubmit, en input, car un output ne peut pas répondre. S'exécute avant le débit, pour passer votre commande. Il compte s'il est défini au chargement du formulaire, et il est appelé sans this |
(instance) | ComponentInstance | null | aucune | L'instance une fois montée, null une fois partie : un nouveau montage, ou le composant détruit |
(ready) | ComponentInstance | aucune | Le formulaire est prêt et interactif |
(stateChange) | { state, method } | aucune | Le paiement passe à une autre étape, par exemple APPROVAL_REQUIRED pendant que le payeur valide sur son téléphone |
(resize) | { height } | aucune | L'iframe prend une nouvelle hauteur, en pixels |
(redirect) | { url, method } | aucune | Le paiement continue sur la page du prestataire, où Wajub.js emmène votre page juste après |
Les autres outputs sont (change), (focus), (blur), (success), (error), (loadError) et
(methodChange). L'élément hôte est le conteneur où Wajub.js monte l'iframe, en display: block
et pleine largeur : placez-y votre class et votre style. Il ne contient que l'iframe : les
événements DOM qui portent le nom d'un output, change, focus, blur et error, ne
l'atteignent jamais, et (change) n'est que l'output. Le composant expose l'instance montée sous
componentInstance, null avant le montage.
(instance) émet null pendant la destruction du formulaire, en pleine détection des
changements. Gardez l'instance dans un signal, ou dans un champ que seul votre code lit : un simple
champ lu par un binding placé plus haut dans le template déclenche NG0100,
ExpressionChangedAfterItHasBeenChecked, en développement.
beforeSubmit est appelé sans this
Comme tout callback, beforeSubmit est appelé seul : une simple méthode de votre classe passée en
[beforeSubmit]="placeOrder" n'atteint pas votre composant par this. Déclarez-la comme fonction
fléchée dans un champ, readonly placeOrder = async () => …, ou liez la méthode dans votre
classe. Si elle lève une erreur ou rejette, rien n'est débité, et l'erreur part vers
l'ErrorHandler d'Angular avec une indication qui le dit. Une nouvelle fonction donnée après le
chargement du formulaire est celle qui est appelée, sans nouveau montage.
Sans minHeight, la place suit loader, de la même façon dans chaque wrapper : ce que tient
l'hôte au rendu serveur, à l'hydratation et aux vérifications suivantes est décrit dans
Champs de paiement. Il finit à la
hauteur du formulaire : un bloc variant="fields" épouse ses champs sans [minHeight]="0".
Un [appearance]="{ inherit: true }" écrit en ligne, nouvel objet à chaque vérification, n'envoie
rien, et un objet appearance ou fields que vous modifiez sur place compte à la vérification
suivante, comme un nouvel objet. Plusieurs changements d'une même vérification partent dans un seul
update(). Un nouveau montage émet null sur (instance), puis la nouvelle instance.
Ce que les composants de champs ont en commun, chaque input qui atteint le formulaire par
update() ou par un nouveau montage, l'événement redirect et ce dont injectConfirmPayment()
vous protège, est décrit dans
Champs de paiement.
injectConfirmPayment
injectConfirmPayment(sessionId?) est le useConfirmPayment des autres packages. Appelez-le dans
un contexte d'injection, l'initialiseur d'un champ ou un constructeur. Il renvoie
{ confirm(instance, options?), isConfirming }.
| Membre | Fonction |
|---|---|
confirm(instance, options?) | Envoie le composant de champ qu'il reçoit et se résout avec l'issue. Un second appel pendant un paiement en cours renvoie la même promesse. Il rejette avant le chargement de l'environnement d'exécution, et sans session |
isConfirming | Un signal : un paiement est en cours |
La session est lue à chaque confirm() : passez un signal ou une fonction, () => this.sessionId,
pour suivre une session qui change, ou une simple chaîne, le type SessionIdSource. Un sessionId
dans options l'emporte, et un sessionId laissé à undefined ne l'emporte pas.
options.components ne remplace jamais l'instance que vous passez.
Détection des changements
Wajub.js s'exécute hors de la zone d'Angular : les messages de l'iframe, son redimensionnement et
son apparition en fondu ne lancent jamais une vérification de votre application. Les outputs sont
émis de retour dans la zone : une application zone.js se rafraîchit après un événement qu'elle
écoute, et un output que personne n'écoute ne coûte rien. Sans zone.js, avec
provideZonelessChangeDetection(), le listener du template planifie la vérification, comme pour
tout output. injectConfirmPayment() suit lui aussi le paiement hors de la zone, et règle sa
promesse et isConfirming de retour dans celle-ci.
Les composants sont OnPush. Ils prennent en compte un nouvel input, et un objet modifié sur
place, à la vérification suivante de leur parent : le montage, l'update() ou le nouveau montage a
lieu juste après cette vérification, une fois la vue écrite.
Types
Chaque type de @wajub/js est aussi exporté : un seul import sert les deux packages, et
import type { PaymentSuccess } from '@wajub/angular' fonctionne. Les inputs et les outputs sont
typés sur les classes des composants, que lit la vérification des types de templates d'Angular.
Le package ajoute WajubOptions, ce que prend provideWajub(), SessionIdSource, la session que
prend injectConfirmPayment(), et WajubConfirmPayment, ce qu'il renvoie. WajubComponentBase est
la classe que partagent les composants de champs : leurs inputs, leurs outputs et
componentInstance.
Mobile Money et carte dans votre propre checkout
Votre checkout garde son formulaire, son choix de moyen de paiement et son bouton Payer, et chaque
moyen n'affiche que ses champs, dans le style de votre page. Les étapes, les appels serveur et la
raison de chaque option sont dans
Champs de paiement.
Ici, votre serveur a préparé le paiement avec deferred: true à l'ouverture du checkout, et a
transmis à la page son authorization_token et son authorization_url.
import { Component, Input, computed, signal } from '@angular/core';
import {
WajubCardComponent,
WajubMobileMoneyComponent,
injectConfirmPayment,
type ComponentInstance,
type WajubError,
} from '@wajub/angular';
// Le paiement est en cours, ou attend le payeur : rien ne doit changer sur la page.
const RUNNING = [
'PROCESSING', 'VERIFYING', 'PENDING', 'OTP_REQUIRED', 'PIN_REQUIRED', 'PHONE_REQUIRED',
'BIRTHDAY_REQUIRED', 'ADDRESS_REQUIRED', '3DS_REQUIRED', 'USSD_REQUIRED', 'APPROVAL_REQUIRED',
];
@Component({
selector: 'app-payment-step',
standalone: true,
imports: [WajubCardComponent, WajubMobileMoneyComponent],
template: `
<fieldset [disabled]="locked()">
<label><input type="radio" name="method" [checked]="method() === 'mobileMoney'" (change)="method.set('mobileMoney')" /> Mobile Money</label>
<label><input type="radio" name="method" [checked]="method() === 'card'" (change)="method.set('card')" /> Carte</label>
</fieldset>
@if (fallback()) {
<p>Vous finirez sur la page de paiement sécurisée de Wajub.</p>
} @else if (method() === 'card') {
<!-- loader="skeleton" par défaut : le bloc garde sa hauteur pendant le chargement. -->
<wajub-card
[sessionId]="sessionId"
variant="fields"
[appearance]="{ inherit: true }"
[readyTimeout]="10000"
[fields]="{ customer: 'external' }"
[beforeSubmit]="placeOrder"
(instance)="mounted($event)"
(change)="complete.set(!!$event.complete)"
(stateChange)="step.set($event.state)"
(loadError)="loadFailed($event)"
></wajub-card>
} @else {
<wajub-mobile-money
[sessionId]="sessionId"
variant="fields"
[appearance]="{ inherit: true }"
[readyTimeout]="10000"
[fields]="{ customer: 'external', country: 'CM', operator: 'detect' }"
[beforeSubmit]="placeOrder"
(instance)="mounted($event)"
(change)="complete.set(!!$event.complete)"
(stateChange)="step.set($event.state)"
(loadError)="loadFailed($event)"
></wajub-mobile-money>
}
@if (step() === 'APPROVAL_REQUIRED') {
<p role="status">Validez le paiement sur votre téléphone.</p>
}
<button type="button" [disabled]="locked() || (!fallback() && !complete())" (click)="pay()">Payer</button>
`,
})
export class PaymentStepComponent {
// L'authorization_token et l'authorization_url du paiement préparé, transmis par votre serveur.
@Input({ required: true }) sessionId!: string;
@Input({ required: true }) hostedUrl!: string;
readonly method = signal<'mobileMoney' | 'card'>('mobileMoney');
readonly complete = signal(false);
readonly step = signal('INITIATED');
readonly fallback = signal(false);
private readonly payment = injectConfirmPayment(() => this.sessionId);
readonly locked = computed(() => this.payment.isConfirming() || RUNNING.includes(this.step()));
private component: ComponentInstance | null = null;
// Votre serveur vérifie le panier, passe la commande et active le paiement.
// Une fonction fléchée, pour que this soit votre composant quand le champ l'appelle.
readonly placeOrder = async (): Promise<boolean> => {
const response = await fetch('/checkout/place-order', { method: 'POST' });
return response.ok; // false : rien n'est débité
};
// Changer de moyen détruit un formulaire et monte l'autre : null entre les deux.
mounted(instance: ComponentInstance | null): void {
this.component = instance;
this.complete.set(false);
}
loadFailed(error: WajubError | Record<string, unknown>): void {
if (error.code === 'ready_timeout') this.fallback.set(true);
}
async pay(): Promise<void> {
if (this.fallback()) {
// Un paiement préparé n'est débité qu'une fois activé : passez d'abord la commande.
if (await this.placeOrder()) window.location.assign(this.hostedUrl);
return;
}
if (!this.component) return;
try {
await this.payment.confirm(this.component);
window.location.assign('/thanks');
} catch {
// Le formulaire affiche la raison, et le payeur peut y réessayer.
}
}
}Les champs affichent eux-mêmes un paiement refusé, et le payeur y réessaie. S'ils ne s'affichent
pas dans le délai readyTimeout, le bouton Payer passe la commande puis envoie le payeur sur la
page de Wajub : un paiement préparé n'est débité qu'une fois activé. appearance.inherit lit un
champ texte de votre page, son libellé, votre bouton principal et le fond derrière les champs.
Affichez donc les champs de votre propre formulaire avant cette étape, ou nommez-les avec
[appearance]="{ inherit: { input: '#email', button: '#pay' } }". Marquez la commande payée sur
votre serveur, à partir du webhook payment.succeeded, jamais depuis le navigateur.
Angular SSR
La session est créée sur votre serveur. Aucun élément de ce package ne nécessite ni ne doit recevoir de clé secrète.
import express from 'express';
import { Wajub } from '@wajub/node';
const wajub = new Wajub({ secretKey: process.env['WAJUB_API_KEY']! });
// Sur l'application Express que crée server.ts, avant le handler qui affiche Angular.
app.post('/api/checkout/session', express.json(), async (req, res) => {
const { cartId } = req.body;
const payment = await wajub.payments.create(
{
amount: 25000,
currency: 'XAF',
description: `Cart ${cartId}`,
customer: { email: '[email protected]' },
metadata: { cart_id: cartId },
},
{ idempotencyKey: `cart-${cartId}` },
);
res.json({ sessionId: payment.authorization_token });
});src/environments est réservé à la clé publique
Chaque fichier environment.ts est compilé dans le bundle du navigateur. pk. peut y figurer,
jamais sk.. Une clé secrète envoyée depuis un navigateur reçoit une réponse 403 et son
propriétaire est alerté par e-mail.
Angular SSR impose trois autres règles :
- Rien à exclure. Les composants s'affichent sur le serveur, où rien ne lit
windownidocumentet où l'environnement d'exécution n'est pas chargé : l'hôte d'un composant de champ s'affiche vide, en tenant déjà la hauteur estimée du formulaire, la même à l'hydratation. Le formulaire se monte dans le navigateur après le premier rendu :provideClientHydration()ne demande rien de plus, ningSkipHydration, niisPlatformBrowser()autour des composants. - Placez
provideWajub()dansapp.config.ts: le service vit aussi longtemps que l'application, et une navigation dans le flux de checkout ne recharge pas l'environnement d'exécution. - Récupérez la session depuis votre serveur et transmettez le jeton comme input.
Éléments exportés par le package
Les exports sont listés dans la Référence du SDK, les types que le package ajoute dans TypeScript.
Pages associées