| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- 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<InitResult> | 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<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) {
- 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<EventCode extends Payment.SplitElementEventCode>(
- el: ElementType,
- event: EventCode,
- handler: Payment.SplitElementEventHandler<EventCode>
- ) {
- 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();
|