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

BDFINFO2.0
Оффлайн
Регистрация
14.05.16
Сообщения
11.398
Реакции
501
Репутация
0
Как давно вы платили на веб-сайте в один клик с помощью Google Pay, Apple Pay или заранее заданной в браузере картой?

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

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

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

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

j5l-xuoz7wyv3vzymk_t1txb3yk.png


О чем речь?


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

zpu2m2tovaltoiuinotdhzt4nmc.png


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

dwxhjjwweobgodqsv4clb4epyx8.jpeg


Это не только быстро, но и функционально: окно позволяет выводить информацию по всему заказу и по отдельным товарам и услугам внутри него, позволяет уточнить информацию о клиенте и детали по доставке. Все это кастомизируется при создании запроса, хотя и удобство у предоставляемого 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
{
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 из , чтобы безопасно получить глобальный объект из DI. Теперь добавим новый PAYMENT_REQUEST_SUPPORT. Он позволит проверять поддержку Payment Request API перед его использованием, и теперь у нас никогда не произойдет случайного вызова API в среде, которая его не поддерживает.


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




export class PaymentRequestService {
constructor(
@Inject(PAYMENT_REQUEST_SUPPORT) private readonly supported: boolean,
...
) {}

request(...): Promise
{
if (!this.supported) {
return Promise.reject(
new Error('Payment Request is not supported in your browser'),
);
}
...
}



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


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

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

z62s1ulbagd5qj3rcloaqpnk2sy.gif


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





{{ cartItem.label }} ({{ cartItem.amount.value }} {{ cartItem.amount.currency }})



Total: {{ totalSum }} ₽


Buy






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

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


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

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

  • Директива платежа собирает все товары внутри себя с помощью ContentChildren и имплементирует PaymentDetailsInit — один из обязательных аргументов при работе с Payment Request API.



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

displayItems?: PaymentItem[];
}



  • Директива-аутпут, которая отслеживает клики по кнопке и эмитит итоговый результат платежа, вытаскивает директиву платежа из дерева Dependency Injection, а также методы платежей и дополнительные опции, которые задаются DI-токенами.



@Directive({
selector: '[waPaymentSubmit]',
})
export class PaymentSubmitDirective {
@Output()
waPaymentSubmit: Observable
;

@Output()
waPaymentError: Observable;

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



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


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

  • , с которого сделаны скриншоты и гифки в этой статье.

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

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