- PVSM.RU - https://www.pvsm.ru -
Как давно вы платили на веб-сайте в один клик с помощью Google Pay, Apple Pay или заранее заданной в браузере картой?
У меня такое получается редко.
Даже наоборот: каждый новый интернет-магазин предлагает мне очередную формочку. А я должен каждый раз покорно искать свою карту, чтобы перепечатать данные с нее на сайт. На следующий день я захочу оплатить что-нибудь в другом магазине и повторю этот процесс.
Это не очень удобно. Особенно когда знаешь об альтернативе: в последние пару лет стандарт Payment Request API позволяет легко решать эту проблему в современных браузерах.
Давайте разберемся, почему его не используют, и попробуем упростить работу с ним.
Почти [1] во всех современных браузерах реализован стандарт Payment Request API [2]. Он позволяет вызвать модальное окно в браузере, через которое пользователь сможет провести платеж в считанные секунды. Вот так это может выглядеть в Chrome с обычной карточкой из браузера:
А вот так — в Safari при оплате отпечатком пальца через Apple Pay:
Это не только быстро, но и функционально: окно позволяет выводить информацию по всему заказу и по отдельным товарам и услугам внутри него, позволяет уточнить информацию о клиенте и детали по доставке. Все это кастомизируется при создании запроса, хотя и удобство у предоставляемого API довольно спорное.
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 [3], чтобы безопасно получить глобальный объект из 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'),
);
}
...
}
С описанным выше подходом мы можем достаточно безопасно работать с платежами, но удобство работы все еще остается на том же уровне «голого» 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>
Все работает благодаря трем директивам:
Так мы получаем простой и удобный интерфейс для открытия платежа и обработки его результата. Причем работает он по всем канонам Angular Way.
Сами директивы связаны довольно простым образом:
@Directive({
selector: '[waPayment][paymentTotal]',
})
export class PaymentDirective implements PaymentDetailsInit {
...
@ContentChildren(PaymentItemDirective)
set paymentItems(items: QueryList<PaymentItem>) {
this.displayItems = items.toArray();
}
displayItems?: PaymentItem[];
}
@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 [9]:
Это готовое решение, которое позволяет работать с Payment Request API безопасно и быстро как через сервис, так и через директивы в описанном выше формате.
Эту библиотеку мы опубликовали и поддерживаем от @ng-web-apis — опенсорсной группы, специализирующейся на реализации легких Angular-оберток для нативных Web API, преимущественно в декларативном стиле. На нашем сайте есть и другие реализации API [12], которые не поставляются в Angular из коробки, но могут заинтересовать вас.
Автор: MarsiBarsi
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/349992
Ссылки в тексте:
[1] Почти: https://caniuse.com/#feat=payment-request
[2] Payment Request API: https://www.w3.org/TR/payment-request/
[3] @ng-web-apis/common: https://github.com/ng-web-apis/common
[4] waPayment: https://github.com/ng-web-apis/payment-request/blob/master/projects/payment-request/src/directives/payment/payment.directive.ts
[5] PaymentItem: https://www.w3.org/TR/payment-request/#paymentitem-dictionary
[6] waPaymentItem: https://github.com/ng-web-apis/payment-request/blob/master/projects/payment-request/src/directives/payment-item/payment-item.directive.ts
[7] PaymentResponse: https://developer.mozilla.org/en-US/docs/Web/API/PaymentResponse
[8] waPaymentSubmit: https://github.com/ng-web-apis/payment-request/blob/master/projects/payment-request/src/directives/payment-submit/payment-submit.directive.ts
[9] @ng-web-apis/payment-request: https://www.npmjs.com/package/@ng-web-apis/payment-request
[10] Репозиторий с кодом на GitHub.: https://github.com/ng-web-apis/payment-request
[11] Демопример: https://ng-web-apis.github.io/payment-request
[12] есть и другие реализации API: https://ng-web-apis.github.io/
[13] Источник: https://habr.com/ru/post/492700/?utm_campaign=492700&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.