import { init, type InitResult,createElement, type Payment } from '@airwallex/components-sdk'; import { env } from "@/env"; // ============================================================ // Types // ============================================================ export type AirwallexCartNumberElementType = Payment.CardNumberElementType; export type AirwallexCvcElementType = Payment.CvcElementType; export type AirwallexExpiryDateElementType = Payment.ExpiryDateElementType; export interface AirwallexCardElements{ cardNumber?: AirwallexCartNumberElementType; cvc?: AirwallexCvcElementType; expiry?: AirwallexExpiryDateElementType; } type ElementType = | AirwallexCartNumberElementType | 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 | null = null; export function airwallexInit() { if (!initPromise) { initPromise = init({ env: env.NEXT_PUBLIC_APP_ENV !== 'production' ? 'demo' : 'prod', enabledElements: ['payments'], }).catch((err) => { initPromise = null; throw err; }); } return initPromise; } // ============================================================ // Manager // ============================================================ // 创建的cardNumber,cvc,expiry输入框挂载到页面上的某个元素上才能渲染出来 // 修改购物车数据后,需要重新获取支付方式列表,要想实现销毁-->重建卡支付输入框,必须要在销毁后把挂载的那个元素也删除,否则新创建的输入框上的绑定的事件会重发触发 // 所以进行此次优化 class AirwallexManager { // -------------------------------------------------------- // Airwallex Elements // -------------------------------------------------------- private elements: AirwallexCardElements = {}; private createPromise: Promise | 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 ) { 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) { return this.elements; } if (this.createPromise) { return this.createPromise; } this.createPromise = Promise.all([ createElement("cardNumber"), createElement("cvc"), createElement("expiry") ]).then(([cardNumber,cvc,expiry]) => { this.elements = { cardNumber, cvc, expiry }; return this.elements; }).catch((error) => { // 创建失败,允许下一次重新创建 this.createPromise = null; throw error; }); return this.createPromise; } mount(containerIds: { cardNumber: string; cvc: string; expiry: string; }) { this.elements.cardNumber?.mount(containerIds.cardNumber); this.elements.cvc?.mount(containerIds.cvc); this.elements.expiry?.mount(containerIds.expiry); } on( el: ElementType, event: EventCode, handler: Payment.SplitElementEventHandler ) { el.on(event, handler); } 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); }); } // ======================================================== // 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; [cardNumber, cvc, expiry].forEach((el) => { if (!el) return; el.unmount(); el.destroy(); }); this.elements = {}; this.createPromise=null; this.resetSnapshot(); } getElements() { return this.elements; } getCardNumberElement() { return this.elements.cardNumber; } } export const airwallexManager = new AirwallexManager();