|
|
@@ -1,6 +1,8 @@
|
|
|
import { init,InitResult,createElement, type Payment } from '@airwallex/components-sdk';
|
|
|
|
|
|
-// export {Payment};
|
|
|
+// ============================================================
|
|
|
+// Types
|
|
|
+// ============================================================
|
|
|
export type AirwallexCartNumberElementType = Payment.CardNumberElementType;
|
|
|
export type AirwallexCvcElementType = Payment.CvcElementType;
|
|
|
export type AirwallexExpiryDateElementType = Payment.ExpiryDateElementType;
|
|
|
@@ -16,6 +18,43 @@ type ElementType =
|
|
|
| AirwallexCvcElementType
|
|
|
| AirwallexExpiryDateElementType;
|
|
|
|
|
|
+// ============================================================
|
|
|
+// Snapshot
|
|
|
+// ============================================================
|
|
|
+export interface AirwallexCardSnapshot {
|
|
|
+ cardNumberReady: boolean;
|
|
|
+ cvcReady: boolean;
|
|
|
+ expiryReady: boolean;
|
|
|
+
|
|
|
+ cardNumberValid: boolean;
|
|
|
+ cvcValid: boolean;
|
|
|
+ expiryValid: boolean;
|
|
|
+
|
|
|
+ cardNumberErrorMsg: string;
|
|
|
+ cvcErrorMsg: string;
|
|
|
+ expiryErrorMsg: string;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Initial Snapshot
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+const initialSnapshot: AirwallexCardSnapshot = {
|
|
|
+ cardNumberReady: false,
|
|
|
+ cvcReady: false,
|
|
|
+ expiryReady: false,
|
|
|
+
|
|
|
+ cardNumberValid: false,
|
|
|
+ cvcValid: false,
|
|
|
+ expiryValid: false,
|
|
|
+
|
|
|
+ cardNumberErrorMsg: '',
|
|
|
+ cvcErrorMsg: '',
|
|
|
+ expiryErrorMsg: '',
|
|
|
+};
|
|
|
+// ============================================================
|
|
|
+// Airwallex SDK init
|
|
|
+// ============================================================
|
|
|
let initPromise: Promise<InitResult> | null = null;
|
|
|
|
|
|
|
|
|
@@ -42,11 +81,100 @@ export function airwallexInit() {
|
|
|
return initPromise;
|
|
|
}
|
|
|
|
|
|
-
|
|
|
+// ============================================================
|
|
|
+// Manager
|
|
|
+// ============================================================
|
|
|
+// 创建的cardNumber,cvc,expiry输入框挂载到页面上的某个元素上才能渲染出来
|
|
|
+// 修改购物车数据后,需要重新获取支付方式列表,要想实现销毁-->重建卡支付输入框,必须要在销毁后把挂载的那个元素也删除,否则新创建的输入框上的绑定的事件会重发触发
|
|
|
+// 所以进行此次优化
|
|
|
class AirwallexManager {
|
|
|
+ // --------------------------------------------------------
|
|
|
+ // Airwallex Elements
|
|
|
+ // --------------------------------------------------------
|
|
|
private elements: AirwallexCardElements = {};
|
|
|
+
|
|
|
private createPromise: Promise<AirwallexCardElements> | null = null;
|
|
|
|
|
|
+ // 引入使用者计数(现在是全局单例模式,这里的全局指的是以页面的生命周期为准,对于卡支付输入框,全局只有一个地方可以初始化,每次初始化后,全局只有一个实例)
|
|
|
+ // 所以这里引入的refCount是一个过度防御
|
|
|
+ // 当把airwallex的卡支付初始化与卡支付输入框的状态订阅拆分后,这个refCount已不那么需要(可以删除),但还是保留
|
|
|
+ private refCount = 0;
|
|
|
+
|
|
|
+ // --------------------------------------------------------
|
|
|
+ // React external store
|
|
|
+ // --------------------------------------------------------
|
|
|
+ private snapshot: AirwallexCardSnapshot = {
|
|
|
+ ...initialSnapshot,
|
|
|
+ };
|
|
|
+
|
|
|
+ private listeners = new Set<() => void>(); // 集合类型,用来保存监听器
|
|
|
+
|
|
|
+ // 参数listener是react内部的监听函数
|
|
|
+ // 当把subscribe传给useSyncExternalStore后,react在内部调用subscribe时把自己的监听函数传给subscribe
|
|
|
+ subscribe = (listener: () => void) => {
|
|
|
+ // 把react的监听函数保存到集合
|
|
|
+ this.listeners.add(listener);
|
|
|
+
|
|
|
+ // 取消订阅时(比如组件卸载时),删除保存的监听函数
|
|
|
+ return () => {
|
|
|
+ this.listeners.delete(listener);
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // react传给subscribe的监听函数里会调用getSnapshot重新获取数据快照
|
|
|
+ getSnapshot = () => {
|
|
|
+ return this.snapshot;
|
|
|
+ };
|
|
|
+ private emit() {
|
|
|
+
|
|
|
+ this.listeners.forEach((listener) => {
|
|
|
+ listener();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ private updateSnapshot( patch: Partial<AirwallexCardSnapshot> ) {
|
|
|
+ this.snapshot = {
|
|
|
+ ...this.snapshot,
|
|
|
+ ...patch,
|
|
|
+ };
|
|
|
+ // 修改数据后,触发react的监听器,监听器里重现获取数据快照,数据变了,react重新渲染组件
|
|
|
+ this.emit();
|
|
|
+ }
|
|
|
+
|
|
|
+ private resetSnapshot() {
|
|
|
+ this.snapshot = {
|
|
|
+ ...initialSnapshot,
|
|
|
+ };
|
|
|
+
|
|
|
+ this.emit();
|
|
|
+ }
|
|
|
+ //
|
|
|
+ setRefCount() {
|
|
|
+ this.refCount++;
|
|
|
+ console.log(
|
|
|
+ '[Airwallex] acquire:',
|
|
|
+ this.refCount
|
|
|
+ );
|
|
|
+ }
|
|
|
+ release() {
|
|
|
+ if (this.refCount === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.refCount--;
|
|
|
+
|
|
|
+ console.log(
|
|
|
+ '[Airwallex] release:',
|
|
|
+ this.refCount
|
|
|
+ );
|
|
|
+
|
|
|
+ if (this.refCount === 0) {
|
|
|
+ console.log('_____________________________AirwallexCard destory ___________________________________')
|
|
|
+ this.destroy();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
async createElements() {
|
|
|
|
|
|
if (this.elements.cardNumber) {
|
|
|
@@ -58,9 +186,6 @@ class AirwallexManager {
|
|
|
|
|
|
return this.createPromise;
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
|
|
|
this.createPromise = Promise.all([
|
|
|
createElement("cardNumber"),
|
|
|
@@ -76,11 +201,15 @@ class AirwallexManager {
|
|
|
|
|
|
return this.elements;
|
|
|
|
|
|
- });
|
|
|
+ }).catch((error) => {
|
|
|
|
|
|
+ // 创建失败,允许下一次重新创建
|
|
|
+ this.createPromise = null;
|
|
|
|
|
|
- return this.createPromise;
|
|
|
+ throw error;
|
|
|
+ });
|
|
|
|
|
|
+ return this.createPromise;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -94,18 +223,268 @@ class AirwallexManager {
|
|
|
this.elements.expiry?.mount(containerIds.expiry);
|
|
|
}
|
|
|
|
|
|
- unmount() {
|
|
|
- this.elements.cardNumber?.unmount();
|
|
|
- this.elements.cvc?.unmount();
|
|
|
- this.elements.expiry?.unmount();
|
|
|
+ on<EventCode extends Payment.SplitElementEventCode>(
|
|
|
+ el: ElementType,
|
|
|
+ event: EventCode,
|
|
|
+ handler: Payment.SplitElementEventHandler<EventCode>
|
|
|
+ ) {
|
|
|
+
|
|
|
+ el.on(event, handler);
|
|
|
}
|
|
|
|
|
|
- on<EventCode extends Payment.SplitElementEventCode>(el: ElementType, event: EventCode, handler: Payment.SplitElementEventHandler<EventCode>) {
|
|
|
+ elementsBindHandler() {
|
|
|
+ const { cardNumber, cvc, expiry } = this.elements;
|
|
|
+ if(!cardNumber || !cvc || !expiry) {
|
|
|
+ throw new Error('Airwallex elements have not been created!');
|
|
|
+ }
|
|
|
+ cardNumber.on("focus", (e) => {
|
|
|
+ this.handleFocus(e)
|
|
|
+ });
|
|
|
+ cardNumber.on("blur", (e) => {
|
|
|
+ this.handleBlur(e);
|
|
|
+ });
|
|
|
+ cardNumber.on("change", (e) => {
|
|
|
+ this.handleChange(e);
|
|
|
+ });
|
|
|
+ cardNumber.on("ready", (e) => {
|
|
|
+ this.handleReady(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ cvc.on("focus", (e) => {
|
|
|
+ this.handleFocus(e)
|
|
|
+ });
|
|
|
+ cvc.on("blur", (e) => {
|
|
|
+ this.handleBlur(e);
|
|
|
+ });
|
|
|
+ cvc.on("change", (e) => {
|
|
|
+ this.handleChange(e);
|
|
|
+ });
|
|
|
+ cvc.on("ready", (e) => {
|
|
|
+ this.handleReady(e);
|
|
|
+ });
|
|
|
+
|
|
|
+ expiry.on("focus", (e) => {
|
|
|
+ this.handleFocus(e)
|
|
|
+ });
|
|
|
+ expiry.on("blur", (e) => {
|
|
|
+ this.handleBlur(e);
|
|
|
+ });
|
|
|
+ expiry.on("change", (e) => {
|
|
|
+ this.handleChange(e);
|
|
|
+ });
|
|
|
+ expiry.on("ready", (e) => {
|
|
|
+ this.handleReady(e);
|
|
|
+ });
|
|
|
|
|
|
- el.on(event, handler);
|
|
|
}
|
|
|
|
|
|
+ // ========================================================
|
|
|
+ // Airwallex event handlers
|
|
|
+ // ========================================================
|
|
|
+
|
|
|
+ handleReady(event: any) {
|
|
|
+ console.log('-----------------airwallex card ready-----------------');
|
|
|
+ const { type } = event.detail;
|
|
|
+
|
|
|
+ if (type === 'cardNumber') {
|
|
|
+ this.updateSnapshot({
|
|
|
+ cardNumberReady: true,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (type === 'cvc') {
|
|
|
+ this.updateSnapshot({
|
|
|
+ cvcReady: true,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (type === 'expiry') {
|
|
|
+ this.updateSnapshot({
|
|
|
+ expiryReady: true,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleChange(event: any) {
|
|
|
+
|
|
|
+ const {
|
|
|
+ type,
|
|
|
+ complete,
|
|
|
+ error,
|
|
|
+ } = event.detail;
|
|
|
|
|
|
+ const message = error?.message ?? '';
|
|
|
+
|
|
|
+ if (type === 'cardNumber') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cardNumberValid: complete,
|
|
|
+ cardNumberErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'cvc') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cvcValid: complete,
|
|
|
+ cvcErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'expiry') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ expiryValid: complete,
|
|
|
+ expiryErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ handleFocus(event: any) {
|
|
|
+
|
|
|
+ const { type } = event.detail;
|
|
|
+
|
|
|
+ if (type === 'cardNumber') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cardNumberErrorMsg: '',
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'cvc') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cvcErrorMsg: '',
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'expiry') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ expiryErrorMsg: '',
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ handleBlur(event: any) {
|
|
|
+
|
|
|
+ const {
|
|
|
+ type,
|
|
|
+ error,
|
|
|
+ } = event.detail;
|
|
|
+
|
|
|
+ const message = error?.message ?? JSON.stringify(error);
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'cardNumber') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cardNumberErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'cvc') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ cvcErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (type === 'expiry') {
|
|
|
+
|
|
|
+ this.updateSnapshot({
|
|
|
+ expiryErrorMsg: message,
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // ========================================================
|
|
|
+ // Validation
|
|
|
+ // ========================================================
|
|
|
+
|
|
|
+ validateCard(): {valid: boolean; msg: string} {
|
|
|
+
|
|
|
+ const state = this.snapshot;
|
|
|
+
|
|
|
+ if (!state.cardNumberReady) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: 'Card number input has not ready. Please wait a minute.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!state.expiryReady) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: 'Card expiry date input has not ready. Please wait a minute.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!state.cvcReady) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: 'Card cvc input has not ready. Please wait a minute.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (!state.cardNumberValid) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: state.cardNumberErrorMsg || 'Please input your card number.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!state.expiryValid) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: state.expiryErrorMsg || 'Please input expiry date.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!state.cvcValid) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ msg: state.cvcErrorMsg || 'Please input cvc.'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ valid: true,
|
|
|
+ msg: ''
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
destroy() {
|
|
|
const { cardNumber, cvc, expiry } = this.elements;
|
|
|
@@ -118,12 +497,17 @@ class AirwallexManager {
|
|
|
|
|
|
this.elements = {};
|
|
|
this.createPromise=null;
|
|
|
+ this.resetSnapshot();
|
|
|
|
|
|
}
|
|
|
|
|
|
getElements() {
|
|
|
return this.elements;
|
|
|
}
|
|
|
+
|
|
|
+ getCardNumberElement() {
|
|
|
+ return this.elements.cardNumber;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export const airwallexManager = new AirwallexManager();
|