У меня такое получается редко.
Даже наоборот: каждый новый интернет-магазин предлагает мне очередную формочку. А я должен каждый раз покорно искать свою карту, чтобы перепечатать данные с нее на сайт. На следующий день я захочу оплатить что-нибудь в другом магазине и повторю этот процесс.
Это не очень удобно. Особенно когда знаешь об альтернативе: в последние пару лет стандарт 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:
- Репозиторий с кодом на GitHub.
- Демопример, с которого сделаны скриншоты и гифки в этой статье.
Это готовое решение, которое позволяет работать с Payment Request API безопасно и быстро как через сервис, так и через директивы в описанном выше формате.
Эту библиотеку мы опубликовали и поддерживаем от @ng-web-apis — опенсорсной группы, специализирующейся на реализации легких Angular-оберток для нативных Web API, преимущественно в декларативном стиле. На нашем сайте есть и другие реализации API, которые не поставляются в Angular из коробки, но могут заинтересовать вас.
ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/492700/

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