Internet declarative shopping with Payment Request API and Angular

How long have you paid on the website in one click using Google Pay, Apple Pay or a predefined card in your browser?

I rarely get it.

Quite the contrary: every new online store offers me another mold. And I must every time dutifully search for my card in order to reprint the data from it to the site. The next day I will want to pay for something in another store and repeat this process.

This is not very convenient. Especially when you know about an alternative: in the last couple of years, the Payment Request API standard makes it easy to solve this problem in modern browsers.

Let's understand why it is not used, and try to simplify the work with it.



What are you talking about?


Almost all modern browsers implement the Payment Request API standard . It allows you to call a modal window in the browser through which the user can make a payment in a matter of seconds. This is how it can look in Chrome with a regular card from the browser:



And here it is - in Safari when paying with a fingerprint via Apple Pay:



It is not only fast, but also functional: the window allows you to display information on the entire order and on individual goods and services inside him, allows you to clarify customer information and delivery details. All this is customized when creating the request, although the convenience of the provided API is rather controversial.

How to use in Angular?


Angular does not provide abstractions for using the Payment Request API. The safest way to use it from the box in Angular is to get the Document from the Dependency Injection mechanism, get the Window object from it and work with 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'),
                     ),
           );
   }
}

If you use the Payment Request directly, then all the problems of implicit dependencies appear: it becomes harder to test the code, the application explodes in the SSR because the Payment Request does not exist. We hope for a global object without any abstractions.

We can take the WINDOW token from @ ng-web-apis / common to safely get the global object from DI. Now add a new PAYMENT_REQUEST_SUPPORT . It will allow you to check the support of the Payment Request API before using it, and now we will never have an accidental API call in an environment that does not support it.

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'),
           );
       } 
      ...
   }

Let's write in Angular style


With the approach described above, we can work with payments quite safely, but the usability still remains at the same level of a bare API browser: we call a method with three parameters, collect a lot of data together and bring them to the desired format to finally call payment method.

But in the world of Angular, we are accustomed to convenient abstractions: a dependency injection mechanism, services, directives and streams. Let's look at a declarative solution that makes using the Payment Request API faster and easier:



In this example, the basket is like this:

<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>

Everything works thanks to three directives:


So we get a simple and convenient interface for opening a payment and processing its result. And it works according to all the canons of Angular Way.

The directives themselves are connected in a rather simple way:

  • The payment directive collects all goods within itself using ContentChildren and implements PaymentDetailsInit - one of the required arguments when working with the Payment Request API.

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

  • The output directive, which tracks clicks on a button and emits the final payment result, pulls out the payment directive from the Dependency Injection tree, as well as payment methods and additional options that are set by DI tokens.

@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));
   }
}

Turnkey solution


We have collected and implemented all the ideas described in the library @ ng-web-apis / payment-request :


This is a turnkey solution that allows you to work with the Payment Request API safely and quickly both through the service and through directives in the format described above.

We have published and supported this library from @ ng-web-apis, an open source group specializing in the implementation of lightweight Angular wrappers for native Web APIs, mainly in a declarative style. There are other API implementations on our site that are not delivered in Angular out of the box, but may interest you.

All Articles