Achats déclaratifs sur Internet avec l'API de demande de paiement et Angular

Depuis combien de temps payez-vous sur le site en un clic en utilisant Google Pay, Apple Pay ou une carte prédéfinie dans votre navigateur?

Je l'obtiens rarement.

Bien au contraire: chaque nouvelle boutique en ligne me propose un autre moule. Et je dois à chaque fois rechercher consciencieusement ma carte afin d'en réimprimer les données sur le site. Le lendemain, je voudrai payer quelque chose dans un autre magasin et répéter ce processus.

Ce n'est pas très pratique. Surtout lorsque vous connaissez une alternative: au cours des deux dernières années, la norme API de demande de paiement facilite la résolution de ce problème dans les navigateurs modernes.

Comprenons pourquoi il n'est pas utilisé et essayons de simplifier le travail avec.



Qu'est-ce que tu racontes?


Presque tous les navigateurs modernes implémentent la norme API de demande de paiement . Il vous permet d'appeler une fenêtre modale dans le navigateur à travers laquelle l'utilisateur peut effectuer un paiement en quelques secondes. Voici à quoi cela peut ressembler dans Chrome avec une carte régulière du navigateur:



Et le voici - dans Safari lorsque vous payez avec une empreinte digitale via Apple Pay:



Il est non seulement rapide, mais aussi fonctionnel: la fenêtre vous permet d'afficher des informations sur l'ensemble de la commande et sur les biens et services individuels à l'intérieur lui, vous permet de clarifier les informations client et les détails de livraison. Tout cela est personnalisé lors de la création de la demande, bien que la commodité de l'API fournie soit plutôt controversée.

Comment utiliser dans Angular?


Angular ne fournit pas d'abstractions pour l'utilisation de l'API de demande de paiement. La façon la plus sûre de l'utiliser à partir de la boîte dans Angular est d'obtenir le document à partir du mécanisme d'injection de dépendances, d'obtenir l'objet Window et de travailler avec window.PaymentRequest.

import {DOCUMENT} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
 
@Injectable()
export class PaymentService {
   constructor(
       @Inject(DOCUMENT)
       private readonly documentRef: Document,
   ) {}
 
   pay(
       methodData: PaymentMethodData[],
       details: PaymentDetailsInit,
       options: PaymentOptions = {},
   ): Promise<PaymentResponse> {
       if (
           this.documentRef.defaultView === null ||
           !('PaymentRequest' in this.documentRef.defaultView)
       ) {
           return Promise.reject(new Error('PaymentRequest is not supported'));
       }
 
       const gateway = new PaymentRequest(methodData, details, options);
 
       return gateway
           .canMakePayment()
           .then(canPay =>
               canPay
                   ? gateway.show()
                   : Promise.reject(
                         new Error('Payment Request cannot make the payment'),
                     ),
           );
   }
}

Si vous utilisez directement la demande de paiement, tous les problèmes de dépendances implicites apparaissent: il devient plus difficile de tester le code, l'application explose dans le SSR car la demande de paiement n'existe pas. Nous espérons un objet global sans aucune abstraction.

Nous pouvons prendre le jeton WINDOW de @ ng-web-apis / common pour obtenir en toute sécurité l'objet global de DI. Ajoutez maintenant un nouveau PAYMENT_REQUEST_SUPPORT . Il vous permettra de vérifier la prise en charge de l'API de demande de paiement avant de l'utiliser, et maintenant nous n'aurons jamais d'appel d'API accidentel dans un environnement qui ne la prend pas en charge.

export const PAYMENT_REQUEST_SUPPORT = new InjectionToken<boolean>(
   'Is Payment Request Api supported?',
   {
       factory: () => !!inject(WINDOW).PaymentRequest,
   },
);

export class PaymentRequestService {
   constructor(
       @Inject(PAYMENT_REQUEST_SUPPORT) private readonly supported: boolean,
       ...
    ) {}
 
    request(...): Promise<PaymentResponse> {
       if (!this.supported) {
           return Promise.reject(
               new Error('Payment Request is not supported in your browser'),
           );
       } 
      ...
   }

Écrivons dans un style angulaire


Avec l'approche décrite ci-dessus, nous pouvons travailler avec les paiements en toute sécurité, mais la convivialité reste au même niveau qu'un navigateur API nu: nous appelons une méthode avec trois paramètres, collectons beaucoup de données ensemble et les amenons au format souhaité pour finalement appeler mode de paiement.

Mais dans le monde d'Angular, nous sommes habitués à des abstractions pratiques: un mécanisme d'injection de dépendances, des services, des directives et des flux. Examinons une solution déclarative qui rend l'utilisation de l'API de demande de paiement plus rapide et plus facile:



dans cet exemple, le panier est comme ceci:

<div waPayment [paymentTotal]="total">
   <div
       *ngFor="let cartItem of shippingCart"
       waPaymentItem
       [paymentLabel]="cartItem.label"
       [paymentAmount]="cartItem.amount"
   >
       {{ cartItem.label }} ({{ cartItem.amount.value }} {{ cartItem.amount.currency }})
   </div>
 
   <b>Total:</b>  {{ totalSum }} ₽
 
   <button
       [disabled]="shippingCart.length === 0"
       (waPaymentSubmit)="onPayment($event)"
       (waPaymentError)="onPaymentError($event)"
   >
       Buy
   </button>
</div>

Tout fonctionne grâce à trois directives:


Nous obtenons donc une interface simple et pratique pour ouvrir un paiement et traiter son résultat. Et cela fonctionne selon tous les canons de la Voie Angulaire.

Les directives elles-mêmes sont reliées de manière assez simple:

  • La directive de paiement collecte toutes les marchandises en elle-même à l'aide de ContentChildren et implémente PaymentDetailsInit - l'un des arguments requis lors de l'utilisation de l'API de demande de paiement.

@Directive({
   selector: '[waPayment][paymentTotal]',
})
export class PaymentDirective implements PaymentDetailsInit {
   ...
   @ContentChildren(PaymentItemDirective)
   set paymentItems(items: QueryList<PaymentItem>) {
       this.displayItems = items.toArray();
   }
 
   displayItems?: PaymentItem[];
}

  • La directive de sortie, qui suit les clics sur un bouton et émet le résultat du paiement final, extrait la directive de paiement de l'arborescence Injection de dépendances, ainsi que les méthodes de paiement et les options supplémentaires définies par les jetons DI.

@Directive({
   selector: '[waPaymentSubmit]',
})
export class PaymentSubmitDirective {
   @Output()
   waPaymentSubmit: Observable<PaymentResponse>;
 
   @Output()
   waPaymentError: Observable<Error | DOMException>;
 
   constructor(
       @Inject(PaymentDirective) paymentHost: PaymentDetailsInit,
       @Inject(PaymentRequestService) paymentRequest: PaymentRequestService,
       @Inject(ElementRef) {nativeElement}: ElementRef,
       @Inject(PAYMENT_METHODS) methods: PaymentMethodData[],
       @Inject(PAYMENT_OPTIONS) options: PaymentOptions,
   ) {
       const requests$ = fromEvent(nativeElement, 'click').pipe(
           switchMap(() =>
               from(paymentRequest.request({...paymentHost}, methods, options)).pipe(
                   catchError(error => of(error)),
               ),
           ),
           share(),
       );
 
       this.waPaymentSubmit = requests$.pipe(filter(response => !isError(response)));
       this.waPaymentError = requests$.pipe(filter(isError));
   }
}

Solution clé en main


Nous avons collecté et mis en œuvre toutes les idées décrites dans la bibliothèque @ ng-web-apis / payment-request :


Il s'agit d'une solution clé en main qui vous permet de travailler avec l'API de demande de paiement en toute sécurité et rapidement à la fois via le service et via des directives au format décrit ci-dessus.

Nous avons publié et pris en charge cette bibliothèque de @ ng-web-apis, un groupe open source spécialisé dans la mise en œuvre de wrappers angulaires légers pour les API Web natives, principalement dans un style déclaratif. Il existe d'autres implémentations d'API sur notre site qui ne sont pas livrées prêtes à l'emploi dans Angular, mais qui peuvent vous intéresser.

All Articles