使用Payment Request API和Angular进行Internet声明式购物

您已使用Google Pay,Apple Pay或浏览器中的预定义卡在一次单击中向网站支付了多长时间?

我很少得到它。

恰恰相反:每个新的在线商店都为我提供了另一种模式。而且,我必须每次都尽职地搜索我的卡,以便将其中的数据重新打印到网站上。第二天,我要在另一家商店中付款,然后重复此过程。

这不是很方便。尤其是当您了解其他选择时:最近几年,使用Payment Request API标准可以轻松在现代浏览器中解决此问题。

让我们了解为什么不使用它,并尝试简化它的工作。



你在说什么?


几乎所有现代浏览器都实现了Payment Request API标准它允许您在浏览器中调用一个模式窗口,用户可以通过该窗口在几秒钟内付款。这就是使用浏览器中的普通卡在Chrome中的显示方式:



这就是-在Safari中通过Apple Pay进行指纹支付时:



不仅速度快,而且功能强大:该窗口可让您显示整个订单以及内部单个商品和服务的信息他,可让您澄清客户信息和交货细节。尽管提供的API的便利性颇有争议,但是在创建请求时,所有这些都是自定义的。

如何在Angular中使用?


Angular不提供使用Payment Request API的抽象。从Angular框中使用它的最安全方法是从“依赖注入”机制获取Document,从中获取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'),
                     ),
           );
   }
}

如果直接使用“付款请求”,那么所有隐式依赖项的问题都会出现:测试代码变得更加困难,因为“付款请求”不存在,因此应用程序在SSR中爆炸。我们希望没有任何抽象的全局对象。

我们可以@ ng-web-apis / common中获取WINDOW令牌以安全地从DI获取全局对象。现在添加一个新的PAYMENT_REQUEST_SUPPORT使用它,您可以在使用前检查付款请求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浏览器的同一级别:我们调用具有三个参数的方法,将大量数据收集在一起,并将它们转换为所需的格式以最​​终调用付款方法。

但是在Angular的世界中,我们习惯于方便的抽象:依赖注入机制,服务,指令和流。让我们看一下一种声明式解决方案,该解决方案使使用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的所有规范工作。

指令本身以非常简单的方式连接:

  • 付款指令使用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发布并支持了该库,这是一个开放源代码组,专门致力于为声明性样式的本地Web API实现轻量级Angular包装器。我们网站上还有其他一些API实现,它们不是Angular开箱即用的,但可能会让您感兴趣。

All Articles