ynkap-payment
Version:
Module de paiement Y-Nkap pour Angular - Intégration simple des paiements mobiles (Orange Money, MTN Mobile Money)
746 lines (599 loc) • 20.5 kB
Markdown
# Y-Nkap Payment Module for Angular
[](https://badge.fury.io/js/ynkap-payment)
[](https://angular.io/)
[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
[](https://github.com/SIEWE-FORTUNE/projet-npm-ykap)
[](../../RAPPORT-DE-TEST.md)
Le module Y-Nkap pour Angular offre une **intégration simple et robuste** des paiements mobiles (Orange Money, MTN Mobile Money) dans vos applications Angular. Développé par **Yaba-In**, ce module fournit une solution complète avec gestion avancée des erreurs, retry automatique, interface utilisateur optimisée, et respect des bonnes pratiques Angular.
## **Fonctionnalités principales**
- **Intégration en une ligne** : Composant `<ynkap-pay-button>` prêt à l'emploi
- **Interface moderne** : Modale responsive avec thèmes light/dark et animations fluides
- **Retry intelligent** : Gestion automatique des erreurs avec backoff exponentiel
- **Sécurisé** : Authentification par clés API, validation stricte
- **Mobile-first** : Optimisé pour Orange Money et MTN Mobile Money
- **Formatage FCFA** : Pipe personnalisé pour affichage correct "1 000 FCFA"
- **TypeScript** : Support complet avec types stricts et IntelliSense
- **Testé** : 87% de couverture de code, 53 tests unitaires
- **Architecture modulaire** : Services séparés et fichiers organisés (HTML, CSS, TS)
- **Bonnes pratiques** : Respect des conventions Angular et séparation des responsabilités
## **Table des matières**
- [Installation](#-installation)
- [Configuration](#-configuration)
- [Utilisation rapide](#-utilisation-rapide)
- [Guide complet](#-guide-complet)
- [API Reference](#-api-reference)
- [Gestion des erreurs](#-gestion-des-erreurs)
- [Exemples avancés](#-exemples-avancés)
- [Migration](#-migration)
- [Support](#-support)
## **Installation**
### Prérequis
- Angular 16+
- TypeScript 5.0+
- Node.js 18+
### Installation via npm
```bash
npm install ynkap-payment --save
```
### Installation via yarn
```bash
yarn add ynkap-payment
```
### Dépendances requises
```bash
npm install @angular/animations --save
```
## **Configuration**
### 1. Import du module
Importez `YnkapModule` dans votre module principal :
```typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { YnkapModule } from 'ynkap-payment';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule, // Requis pour les animations
// Configuration Y-Nkap
YnkapModule.forRoot({
apiKey: 'your-api-key',
apiSecret: 'your-api-secret',
merchantId: 'your-merchant-id',
environment: 'sandbox', // 'sandbox' | 'production'
webhookUrl: 'https://your-app.com/webhook' // Optionnel
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
```
### 2. Configuration des environnements
```typescript
// environments/environment.ts
export const environment = {
production: false,
ynkap: {
apiKey: 'sandbox-api-key',
apiSecret: 'sandbox-api-secret',
merchantId: 'sandbox-merchant-id',
environment: 'sandbox' as const
}
};
// environments/environment.prod.ts
export const environment = {
production: true,
ynkap: {
apiKey: 'prod-api-key',
apiSecret: 'prod-api-secret',
merchantId: 'prod-merchant-id',
environment: 'production' as const
}
};
```
### 3. Configuration dynamique
```typescript
import { ConfigurationService } from '@yaba-in/ynkap-payment';
constructor(private ynkapConfig: ConfigurationService) {}
ngOnInit() {
// Configuration à l'exécution
this.ynkapConfig.setConfig({
apiKey: 'dynamic-api-key',
apiSecret: 'dynamic-api-secret',
merchantId: 'dynamic-merchant-id',
environment: 'production'
});
}
```
## **Utilisation rapide**
### Exemple minimal
```html
<!-- Utilisation la plus simple -->
<ynkap-pay-button
[amount]="1000"
merchantReference="ORDER-123"
description="Achat produit"
(paymentSuccess)="onSuccess($event)"
(paymentError)="onError($event)">
Payer 1000 XAF
</ynkap-pay-button>
```
```typescript
import { Component } from '@angular/core';
import { PaymentResponse, YnkapError } from '@yaba-in/ynkap-payment';
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html'
})
export class CheckoutComponent {
onSuccess(response: PaymentResponse): void {
console.log(' Paiement réussi:', response);
// Rediriger vers page de confirmation
}
onError(error: YnkapError): void {
console.error(' Erreur de paiement:', error);
// Afficher message d'erreur
}
}
```
## **Guide complet**
### 1. Composant Pay Button
Le composant principal `<ynkap-pay-button>` gère tout le processus de paiement :
```html
<ynkap-pay-button
[amount]="orderTotal"
[currency]="'XAF'"
[merchantReference]="orderId"
[description]="orderDescription"
[theme]="'light'"
[primaryColor]="'#007bff'"
[payButtonText]="'Finaliser le paiement'"
(paymentSuccess)="handleSuccess($event)"
(paymentError)="handleError($event)"
(paymentCancel)="handleCancel()"
(paymentStart)="handleStart()">
<!-- Contenu personnalisé du bouton -->
<i class="fas fa-credit-card"></i>
Payer {{ orderTotal | currency:'XAF':'symbol':'1.0-0' }}
</ynkap-pay-button>
```
### 2. Gestion des événements
```typescript
export class PaymentComponent {
orderTotal = 5000;
orderId = 'ORDER-' + Date.now();
orderDescription = 'Commande e-commerce';
handleSuccess(response: PaymentResponse): void {
// Paiement réussi
console.log('Transaction ID:', response.transaction?.id);
console.log('Statut:', response.transaction?.status);
// Sauvegarder en base de données
this.saveTransaction(response.transaction);
// Redirection
this.router.navigate(['/success'], {
queryParams: { transactionId: response.transaction?.id }
});
}
handleError(error: YnkapError): void {
// Gestion des différents types d'erreurs
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
this.showMessage('Solde insuffisant');
break;
case 'NETWORK_ERROR':
this.showMessage('Problème de connexion. Veuillez réessayer.');
break;
case 'PAYMENT_DECLINED':
this.showMessage('Paiement refusé par votre opérateur');
break;
default:
this.showMessage('Une erreur est survenue: ' + error.userMessage);
}
}
handleCancel(): void {
console.log('Paiement annulé par l\'utilisateur');
// Optionnel: analytics, nettoyage, etc.
}
handleStart(): void {
console.log('Processus de paiement démarré');
// Optionnel: analytics, loading state, etc.
}
}
```
## **API Reference**
### YnkapPayButtonComponent
#### Inputs (Propriétés)
| Propriété | Type | Requis | Défaut | Description |
|-----------|------|--------|--------|-------------|
| `amount` | `number` | ✅ | - | Montant de la transaction en centimes |
| `merchantReference` | `string` | ✅ | - | Référence unique de la transaction |
| `description` | `string` | ✅ | - | Description affichée à l'utilisateur |
| `currency` | `string` | ❌ | `'XAF'` | Code devise ISO (XAF, EUR, USD) |
| `payButtonText` | `string` | ❌ | `'Payer {amount}'` | Texte du bouton de paiement |
| `theme` | `'light' \| 'dark'` | ❌ | `'light'` | Thème de l'interface |
| `primaryColor` | `string` | ❌ | `'#007bff'` | Couleur principale (hex) |
| `disabled` | `boolean` | ❌ | `false` | Désactive le bouton |
| `autoRetry` | `boolean` | ❌ | `true` | Active le retry automatique |
| `maxRetries` | `number` | ❌ | `3` | Nombre max de tentatives |
#### Outputs (Événements)
| Événement | Type | Description |
|-----------|------|-------------|
| `paymentSuccess` | `EventEmitter<PaymentResponse>` | Paiement réussi |
| `paymentError` | `EventEmitter<YnkapError>` | Erreur de paiement |
| `paymentCancel` | `EventEmitter<void>` | Paiement annulé |
| `paymentStart` | `EventEmitter<void>` | Début du processus |
| `paymentRetry` | `EventEmitter<number>` | Tentative de retry (numéro) |
### Services disponibles
#### ConfigurationService
```typescript
import { ConfigurationService } from '@yaba-in/ynkap-payment';
// Injection
constructor(private config: ConfigurationService) {}
// Méthodes principales
setConfig(config: ApiConfig): void
getConfig(): ApiConfig | null
setEnvironment(env: 'sandbox' | 'production'): void
clearConfig(): void
// Propriétés
get apiUrl(): string
get environment(): string
get apiKey(): string | null
```
#### PaymentService
```typescript
import { PaymentService } from '@yaba-in/ynkap-payment';
// Injection
constructor(private payment: PaymentService) {}
// Méthodes principales
initiatePayment(request: PaymentRequest): Observable<PaymentResponse>
retryPayment(transactionId: string): Observable<PaymentResponse>
getPaymentStatus(transactionId: string): Observable<PaymentTransaction>
// Observables
availablePaymentMethods$: Observable<PaymentMethod[]>
currentTransaction$: Observable<PaymentTransaction | null>
```
#### RetryService
```typescript
import { RetryService } from '@yaba-in/ynkap-payment';
// Injection
constructor(private retry: RetryService) {}
// Méthodes principales
isRetryable(error: YnkapError): boolean
getRetryInfo(transactionId: string): string
shouldAutoRetry(error: YnkapError): boolean
```
## **Gestion des erreurs**
### Types d'erreurs
Le module Y-Nkap utilise un système d'erreurs typées pour une gestion précise :
```typescript
interface YnkapError extends Error {
code: string; // Code d'erreur technique
userMessage?: string; // Message utilisateur localisé
retryable?: boolean; // Indique si l'erreur est retryable
httpStatus?: number; // Code de statut HTTP
transactionId?: string; // ID de transaction si disponible
}
```
### Codes d'erreur courants
| Code | Description | Retryable | Action recommandée |
|------|-------------|-----------|-------------------|
| `NETWORK_ERROR` | Problème de connexion | ✅ | Retry automatique |
| `SERVER_ERROR` | Erreur serveur (5xx) | ✅ | Retry automatique |
| `INSUFFICIENT_FUNDS` | Solde insuffisant | ❌ | Demander à l'utilisateur de recharger |
| `PAYMENT_DECLINED` | Paiement refusé | ❌ | Vérifier les informations |
| `INVALID_PHONE` | Numéro invalide | ❌ | Corriger le numéro |
| `AUTH_ERROR` | Erreur d'authentification | ❌ | Vérifier les clés API |
| `VALIDATION_ERROR` | Erreur de validation | ❌ | Corriger les données |
### Gestion avancée des erreurs
```typescript
import { YnkapError, ErrorCategory } from '@yaba-in/ynkap-payment';
handleError(error: YnkapError): void {
// Log pour debugging
console.error('Payment Error:', {
code: error.code,
message: error.message,
transactionId: error.transactionId,
retryable: error.retryable
});
// Gestion par catégorie
switch (error.code) {
case ErrorCategory.NETWORK:
this.showRetryableError('Problème de connexion');
break;
case ErrorCategory.PAYMENT_PROCESSING:
this.showPaymentError(error.userMessage || 'Erreur de paiement');
break;
case ErrorCategory.VALIDATION:
this.showValidationError(error.userMessage || 'Données invalides');
break;
default:
this.showGenericError();
}
}
showRetryableError(message: string): void {
// Afficher avec option de retry
this.snackBar.open(message, 'Réessayer', {
duration: 0,
action: () => this.retryPayment()
});
}
```
### Retry automatique
Le module inclut un système de retry intelligent :
```typescript
// Configuration du retry
YnkapModule.forRoot({
// ... autres options
retryOptions: {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
nonRetryableErrors: ['INSUFFICIENT_FUNDS', 'PAYMENT_DECLINED']
}
})
```
## **Exemples avancés**
### 1. E-commerce avec panier
```typescript
@Component({
selector: 'app-checkout',
template: `
<div class="checkout-container">
<div class="order-summary">
<h3>Récapitulatif de commande</h3>
<div *ngFor="let item of cartItems" class="cart-item">
{{ item.name }} - {{ item.price | currency:'XAF' }}
</div>
<div class="total">
Total: {{ orderTotal | currency:'XAF' }}
</div>
</div>
<ynkap-pay-button
[amount]="orderTotal"
[merchantReference]="orderId"
[description]="orderDescription"
[theme]="userPreferences.theme"
(paymentSuccess)="onPaymentSuccess($event)"
(paymentError)="onPaymentError($event)"
class="payment-button">
<div class="button-content">
<i class="fas fa-lock"></i>
Paiement sécurisé {{ orderTotal | currency:'XAF' }}
</div>
</ynkap-pay-button>
</div>
`
})
export class CheckoutComponent {
cartItems = [
{ name: 'T-shirt Premium', price: 15000 },
{ name: 'Jean Slim', price: 25000 }
];
get orderTotal(): number {
return this.cartItems.reduce((sum, item) => sum + item.price, 0);
}
get orderId(): string {
return `ORDER-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
get orderDescription(): string {
return `Commande de ${this.cartItems.length} article(s)`;
}
onPaymentSuccess(response: PaymentResponse): void {
// Vider le panier
this.cartService.clearCart();
// Envoyer confirmation par email
this.emailService.sendOrderConfirmation(response.transaction);
// Analytics
this.analytics.track('purchase', {
transaction_id: response.transaction?.id,
value: this.orderTotal,
currency: 'XAF'
});
// Redirection
this.router.navigate(['/order-success'], {
queryParams: { orderId: response.transaction?.merchantReference }
});
}
}
```
### 2. Abonnement récurrent
```typescript
@Component({
selector: 'app-subscription',
template: `
<div class="subscription-plans">
<div *ngFor="let plan of plans"
class="plan-card"
[class.selected]="selectedPlan?.id === plan.id"
(click)="selectPlan(plan)">
<h3>{{ plan.name }}</h3>
<div class="price">{{ plan.price | currency:'XAF' }}/mois</div>
<ul>
<li *ngFor="let feature of plan.features">{{ feature }}</li>
</ul>
</div>
</div>
<ynkap-pay-button
*ngIf="selectedPlan"
[amount]="selectedPlan.price"
[merchantReference]="subscriptionId"
[description]="'Abonnement ' + selectedPlan.name"
(paymentSuccess)="onSubscriptionSuccess($event)">
S'abonner à {{ selectedPlan.name }}
</ynkap-pay-button>
`
})
export class SubscriptionComponent {
plans = [
{
id: 'basic',
name: 'Basic',
price: 5000,
features: ['10 GB stockage', 'Support email']
},
{
id: 'premium',
name: 'Premium',
price: 15000,
features: ['100 GB stockage', 'Support prioritaire', 'API access']
}
];
selectedPlan: any = null;
get subscriptionId(): string {
return `SUB-${this.selectedPlan?.id}-${Date.now()}`;
}
selectPlan(plan: any): void {
this.selectedPlan = plan;
}
onSubscriptionSuccess(response: PaymentResponse): void {
// Activer l'abonnement
this.subscriptionService.activateSubscription({
userId: this.currentUser.id,
planId: this.selectedPlan.id,
transactionId: response.transaction?.id,
startDate: new Date(),
endDate: this.calculateEndDate()
});
// Notification
this.notificationService.success(
`Abonnement ${this.selectedPlan.name} activé avec succès!`
);
}
}
```
### 3. Intégration avec formulaires réactifs
```typescript
@Component({
selector: 'app-donation',
template: `
<form [formGroup]="donationForm" (ngSubmit)="onSubmit()">
<mat-form-field>
<mat-label>Montant de la donation</mat-label>
<input matInput
type="number"
formControlName="amount"
placeholder="Montant en XAF">
<mat-error *ngIf="donationForm.get('amount')?.hasError('required')">
Le montant est requis
</mat-error>
<mat-error *ngIf="donationForm.get('amount')?.hasError('min')">
Montant minimum: 1000 XAF
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Message (optionnel)</mat-label>
<textarea matInput
formControlName="message"
placeholder="Votre message de soutien"></textarea>
</mat-form-field>
<mat-checkbox formControlName="anonymous">
Donation anonyme
</mat-checkbox>
<ynkap-pay-button
[amount]="donationForm.get('amount')?.value || 0"
[merchantReference]="donationId"
[description]="donationDescription"
[disabled]="donationForm.invalid"
(paymentSuccess)="onDonationSuccess($event)">
Faire un don de {{ donationForm.get('amount')?.value | currency:'XAF' }}
</ynkap-pay-button>
</form>
`
})
export class DonationComponent {
donationForm = this.fb.group({
amount: [null, [Validators.required, Validators.min(1000)]],
message: [''],
anonymous: [false]
});
constructor(private fb: FormBuilder) {}
get donationId(): string {
return `DONATION-${Date.now()}`;
}
get donationDescription(): string {
const amount = this.donationForm.get('amount')?.value;
return `Donation de ${amount} XAF`;
}
onDonationSuccess(response: PaymentResponse): void {
const formValue = this.donationForm.value;
// Enregistrer la donation
this.donationService.recordDonation({
amount: formValue.amount,
message: formValue.message,
anonymous: formValue.anonymous,
transactionId: response.transaction?.id,
donorId: formValue.anonymous ? null : this.currentUser.id
});
// Reset du formulaire
this.donationForm.reset();
// Message de remerciement
this.dialog.open(ThankYouDialogComponent, {
data: { amount: formValue.amount }
});
}
}
```
## **Migration**
### Depuis la version 0.x
```typescript
// Avant (v0.x)
import { YnkapPaymentModule } from '@ynkap/payment-old';
YnkapPaymentModule.forRoot({
apiKey: 'key',
sandbox: true
})
// Après (v1.x)
import { YnkapModule } from '@yaba-in/ynkap-payment';
YnkapModule.forRoot({
apiKey: 'key',
apiSecret: 'secret', // Nouveau: requis
merchantId: 'merchant', // Nouveau: requis
environment: 'sandbox' // Changé: 'sandbox' | 'production'
})
```
### Changements majeurs v1.0
- **Nouveau service RetryService** avec gestion avancée
- **Types TypeScript** stricts pour toutes les APIs
- **Gestion d'erreurs** améliorée avec codes spécifiques
- **Interface utilisateur** redesignée avec thèmes
- **Configuration** simplifiée avec validation
- **Breaking:** `sandbox: boolean` → `environment: 'sandbox' | 'production'`
- **Breaking:** Nouveaux champs requis `apiSecret` et `merchantId`
## **Support**
### Documentation
- [Guide complet](https://docs.ynkap.com/angular)
- [Tutoriels vidéo](https://youtube.com/ynkap-tutorials)
- [Blog technique](https://blog.yaba-in.com/ynkap)
### Communauté
- [Discord](https://discord.gg/ynkap)
- [Forum](https://forum.ynkap.com)
- [Twitter @YnkapDev](https://twitter.com/ynkapdev)
### Support technique
- [Issues GitHub](https://github.com/yaba-in/ynkap-angular/issues)
- Email: support@ynkap.com
- Téléphone: +237 6XX XXX XXX
### Contribution
Nous accueillons les contributions ! Voir [CONTRIBUTING.md](CONTRIBUTING.md)
## **Licence**
MIT © 2025 Yaba-In. Voir [LICENSE](LICENSE) pour plus de détails.
## **Remerciements**
- Équipe de développement Yaba-In
- Communauté Angular
- Contributeurs open source
- Opérateurs mobiles partenaires (Orange, MTN)
**Développé par [Yaba-In](https://yaba-in.com)**