Декларативный шопинг в интернете с помощью Payment Request API и Angular

от автора

Как давно вы платили на веб-сайте в один клик с помощью Google Pay, Apple Pay или заранее заданной в браузере картой?

У меня такое получается редко.

Даже наоборот: каждый новый интернет-магазин предлагает мне очередную формочку. А я должен каждый раз покорно искать свою карту, чтобы перепечатать данные с нее на сайт. На следующий день я захочу оплатить что-нибудь в другом магазине и повторю этот процесс.

Это не очень удобно. Особенно когда знаешь об альтернативе: в последние пару лет стандарт Payment Request API позволяет легко решать эту проблему в современных браузерах.

Давайте разберемся, почему его не используют, и попробуем упростить работу с ним.


О чем речь?

Почти во всех современных браузерах реализован стандарт Payment Request API. Он позволяет вызвать модальное окно в браузере, через которое пользователь сможет провести платеж в считанные секунды. Вот так это может выглядеть в Chrome с обычной карточкой из браузера:

А вот так — в Safari при оплате отпечатком пальца через Apple Pay:

Это не только быстро, но и функционально: окно позволяет выводить информацию по всему заказу и по отдельным товарам и услугам внутри него, позволяет уточнить информацию о клиенте и детали по доставке. Все это кастомизируется при создании запроса, хотя и удобство у предоставляемого API довольно спорное.

Как использовать в Angular?

Angular не предоставляет абстракций для использования Payment Request API. Самый безопасный путь использования из коробки в Angular: достать Document из механизма Dependency Injection, получить из него объект Window и работать с 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'),                      ),            );    } } 

Если использовать Payment Request напрямую, то появляются все проблемы неявных зависимостей: тестировать код становится тяжелее, в SSR приложение взрывается, потому что Payment Request не существует. Надеемся на глобальный объект без каких-либо абстракций.

Мы можем взять токен WINDOW из @ng-web-apis/common, чтобы безопасно получить глобальный объект из DI. Теперь добавим новый PAYMENT_REQUEST_SUPPORT. Он позволит проверять поддержку Payment Request API перед его использованием, и теперь у нас никогда не произойдет случайного вызова API в среде, которая его не поддерживает.

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

Давайте писать в стиле Angular

С описанным выше подходом мы можем достаточно безопасно работать с платежами, но удобство работы все еще остается на том же уровне «голого» API-браузера: мы вызываем метод с тремя параметрами, собираем множество данных воедино и приводим их к нужному формату, чтобы наконец вызвать метод платежа.

Но в мире Ангуляра мы привыкли к удобным абстракциям: механизму внедрения зависимостей, сервисам, директивам и стримам. Давайте посмотрим на декларативное решение, которое позволяет сделать использование Payment Request API быстрее и проще:

В этом примере корзина представляет собой вот такой код:

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

Все работает благодаря трем директивам:

  • waPayment директива, которая определяет область отдельного платежа в шаблоне и принимает в себя объект PaymentItem с информацией о названии платежа и его итоговой суммой
  • Каждый товар в корзине — директива waPaymentItem. Инпуты этой директивы позволяют собрать объект PaymentItem каждого отдельного товара декларативно.
  • Нажатие на кнопку запускает модальное окно Payment Request API в браузере. Ответом модального окна может быть PaymentResponse или ошибка. Директива waPaymentSubmit позволяет отлавливать оба этих исхода обычными ангуляровскими аутпутами.

Так мы получаем простой и удобный интерфейс для открытия платежа и обработки его результата. Причем работает он по всем канонам Angular Way.

Сами директивы связаны довольно простым образом:

  • Директива платежа собирает все товары внутри себя с помощью ContentChildren и имплементирует PaymentDetailsInit — один из обязательных аргументов при работе с 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[]; } 

  • Директива-аутпут, которая отслеживает клики по кнопке и эмитит итоговый результат платежа, вытаскивает директиву платежа из дерева Dependency Injection, а также методы платежей и дополнительные опции, которые задаются 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));    } } 

Готовое решение

Все описанные идеи мы собрали и реализовали в библиотеке @ng-web-apis/payment-request:

Это готовое решение, которое позволяет работать с Payment Request API безопасно и быстро как через сервис, так и через директивы в описанном выше формате.

Эту библиотеку мы опубликовали и поддерживаем от @ng-web-apis — опенсорсной группы, специализирующейся на реализации легких Angular-оберток для нативных Web API, преимущественно в декларативном стиле. На нашем сайте есть и другие реализации API, которые не поставляются в Angular из коробки, но могут заинтересовать вас.

ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/492700/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *