Ver Fonte

对接airwallex 卡支付

fogwind há 1 dia atrás
pai
commit
90ca90661d

+ 22 - 8
README.md

@@ -281,16 +281,16 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 3. createCart 接口报错 -- 已解决
 4. 由于apollo client缓存策略以及graphql代理接口问题导致获取不到数据 -- 已解决
-     3和4的问题是相互关联的
+     - 3和4的问题是相互关联的
 
 5. 尝试升级apollo client版本到v4 -- 完成
 6. jwt-cookie 放弃base64编码,尝试改为加密解密
 
 7. 登录状态点击退出登录后GetCartItem接口会报错 -- 待处理
-  错误信息:
-  debugMessage: "Call to a member function load() on null"
-  file: "/var/www/html/nshop/packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php"
-  line: 913
+  - 错误信息:
+  - debugMessage: "Call to a member function load() on null"
+  - file: "/var/www/html/nshop/packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php"
+  - line: 913
 
 8. apollo client 升级后需要用新的方法设置返回数据的类型 -- 已完成
 9. Apollo client的错误处理机制 (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
@@ -309,13 +309,22 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 22. 顶部header 栏 优化:可设置显示标题还是通用
 23. 个人中心取的是localStorage里的email,即使cookie里的token删除了,localStorage里的email会一直存在, 需要废弃localStorage里的email;
 24. 删除老的checkout页面的组件  -- 已完成
-25. resetGuestToken 有问题,改为退出登录后不调resetGuestToken
+25. resetGuestToken 有问题,改为退出登录后不调resetGuestToken -- 已完成
 26. useAddProduct优化
-27. 下单成功页客服工具,分享
+27. 下单成功页客服工具,分享 
+    - (livechat: https://platform.text.com/docs/extending-chat-widget/chat-widget-adapters/react-adapter)
+    - https://github.com/livechat/chat-widget-adapters
 28. cancelOrder接口报错   --  已解决
 29. 购物车数据管理方案 -- 已完成
 30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理;
 31. 当前货币等采用redux管理;
+32. 当前货币切换功能
+33. 登录用户购物车页获取未支付订单
+34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
+    - 通过修改结账页数据初始化逻辑解决
+35. 结账页默认设置运输方式问题(导致结算联动风暴)
+36. 学习set-state-in-effect,然后检查代码,看是否可以优化(特别是CommonModal组件,看能不能不使用useEffect实现)
+37. paypal provider 支付参数根据环境变量配置
 
 ## 关于购物车数据管理方案
 ssr + redux:
@@ -363,4 +372,9 @@ Hook 是 React 在 render 过程中注册状态依赖的函数调用点,而不
 2. https://developer.aliyun.com/article/1688619
 3. https://zh-hans.react.dev/reference/rsc/server-functions
 
-> useFormState 已弃用,重新命名为 useActionState 
+> useFormState 已弃用,重新命名为 useActionState 
+
+## 关于set-state-in-effect
+1. https://yceffort.kr/en/2025/12/react-set-state-in-effect-lint-rule
+2. 使用useReducer规避待验证:https://stackoverflow.com/questions/79856121/calling-setstate-synchronously-within-an-effect-can-trigger-cascading-renders
+3. https://github.com/react/react/issues/34743

+ 1 - 0
package.json

@@ -11,6 +11,7 @@
     "package-version": "npx npm-check-updates"
   },
   "dependencies": {
+    "@airwallex/components-sdk": "^1.32.0",
     "@apollo/client": "^4.1.9",
     "@apollo/client-integration-nextjs": "^0.14.5",
     "@heroicons/react": "^2.2.0",

+ 15 - 0
pnpm-lock.yaml

@@ -12,6 +12,9 @@ importers:
 
   .:
     dependencies:
+      '@airwallex/components-sdk':
+        specifier: ^1.32.0
+        version: 1.32.0
       '@apollo/client':
         specifier: ^4.1.9
         version: 4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
@@ -175,6 +178,12 @@ packages:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
       react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
 
+  '@airwallex/airtracker@3.2.0':
+    resolution: {integrity: sha512-PKE5N38ajTVg6ph9JzLpWsICNjqLtf/wWudNVU3UPX9SVy2I5s5ITc281sMSD8+LIE6RJoGjGTO+VYP/io5kig==}
+
+  '@airwallex/components-sdk@1.32.0':
+    resolution: {integrity: sha512-IRrYOyCIWw/cQcndm/n11zx8b7hj2wYF7TD2YLmds9lXIQoir214JF99i1QJAyMsRR//nIRkNFTB2Ta2/etYGw==}
+
   '@alloc/quick-lru@5.2.0':
     resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
     engines: {node: '>=10'}
@@ -3463,6 +3472,12 @@ snapshots:
       react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       use-sync-external-store: 1.6.0(react@19.2.5)
 
+  '@airwallex/airtracker@3.2.0': {}
+
+  '@airwallex/components-sdk@1.32.0':
+    dependencies:
+      '@airwallex/airtracker': 3.2.0
+
   '@alloc/quick-lru@5.2.0': {}
 
   '@apollo/client-integration-nextjs@0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)':

+ 57 - 45
src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx

@@ -1,68 +1,73 @@
 "use client";
 
-import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { useRouter } from 'next/navigation'
 import PaypalButton from "./PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import { useAppDispatch } from "@/store/hooks";
-import { clearCart } from "@/store/slices/cart-slice";
+import { PlaceOrderFunction } from "@/types/checkout/type";
 export default function CheckoutPlaceOrder({
     paymentMethod,
     clickPlaceOrder, // 校验运输方式,地址 
 }: {
     paymentMethod: string;
-    clickPlaceOrder: () => Promise<boolean>;
+    clickPlaceOrder: PlaceOrderFunction;
 }) {
-    const dispatch = useAppDispatch(); 
-
-    const { createOrder } = usePlaceOrder(); 
+    const router = useRouter();
     
-    const handleCreateOrder = async () => {
-        const res = await createOrder();
-        console.log("createOrder res ---- ", res);
-        if(!res.error && res.data) {
-            //创建订单成功后清空redux 购物车
-            dispatch(clearCart());
+    const paypalCreateOrder = async () => {
+
+        const placeOrderRes = await clickPlaceOrder();
+        if(placeOrderRes.valid && placeOrderRes.data !== null) {
             return {
-                error: res.error,
+                error: false,
                 msg: '',
-                orderId: res.data.gatewayOrderId,
-                webOrderId: res.data.orderId
+                orderId: placeOrderRes.data.gatewayOrderId,
+                webOrderId: placeOrderRes.data.orderId
             };
         } else {
             return {
-                error: res.error,
-                msg: res.msg,
-                orderId: res.data?.gatewayOrderId ?? '',
-                webOrderId: res.data?.orderId ?? ''
+                error: true,
+                msg: '',
+                orderId: '',
+                webOrderId: ''
             };
+
         }
     };
 
     const airwallexPlaceOrder = async () => {
-        overlayLoading.start();
-        const vaild = await clickPlaceOrder();
-        if(vaild) {
-            const res = await createOrder({
-                paymentSuccessUrl: window.location.origin + '/paymentresult/result'
-            });
-            console.log("createOrder res ---- ", res);
-            if(!res.error && res.data) {
-                // 创建订单成功了,跳转支付页面
-                //创建订单成功后清空redux 购物车
-                dispatch(clearCart());
-                window.location.href = res.data.gatewayOrderId;
-            } else {
-                //创建订单出错了
+
+        const placeOrderRes = await clickPlaceOrder({
+            paymentSuccessUrl: window.location.origin + '/paymentresult/result'
+        });
+        overlayLoading.stop();
+        if(placeOrderRes.valid && placeOrderRes.data !== null) {
+            window.location.href = placeOrderRes.data.gatewayOrderId;
+        }
+    };
+
+    const cardAirwallexPlaceOrder = async () => {
+        const placeOrderRes = await clickPlaceOrder();
+        if(placeOrderRes.valid && placeOrderRes.data !== null && placeOrderRes.otherData !== null) {
+            const webOrderId = placeOrderRes.data.orderId;
+            const cardNumberElement = placeOrderRes.otherData;
+            const paymentIntentIdClientSecret = placeOrderRes.data.gatewayOrderId.split(':');
+            const paymentIntentId = paymentIntentIdClientSecret[0];
+            const paymentIntentClientSecret = paymentIntentIdClientSecret[1];
+
+            cardNumberElement.confirm({
+                intent_id: paymentIntentId,
+                client_secret: paymentIntentClientSecret,
+            }).then((result) => { 
                 overlayLoading.stop();
-                confirmDialog({
-                    title: "Warning",
-                    content: res.msg + " Create order failed. Please try again.",
-                    noCancel: true,
-                }).then(() => {});
-            }
-        } else {
-            overlayLoading.stop();
+                console.log('cardNumberElement.confirm result ---- ', result);
+                // 支付成功 跳转页面
+                router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
+            }).catch((error) => {
+                overlayLoading.stop();
+                console.log('cardNumberElement.confirm error ---- ', error);
+                // 支付失败
+                router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=failure');
+            });
         }
     };
 
@@ -71,8 +76,7 @@ export default function CheckoutPlaceOrder({
             <div className="relative w-full">
                 {paymentMethod === 'paypal_smart_button' && 
                     <PaypalButton
-                        createOrder={handleCreateOrder}
-                        onPaymentClick={clickPlaceOrder}
+                        createOrder={paypalCreateOrder}
                     />
                 }
                 {(paymentMethod === 'awxklarna' || paymentMethod === 'awxafterpay') &&
@@ -82,6 +86,14 @@ export default function CheckoutPlaceOrder({
                         Place Order
                     </button>
                 }
+                {paymentMethod === 'airwallex' && 
+                    <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                        onClick={cardAirwallexPlaceOrder}
+                    >
+                        Place Order
+                    </button>
+                }
+                
 
             </div>
         </>

+ 61 - 15
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,16 +1,18 @@
 "use client";
 
-import {useState, useRef} from "react";
-
+import {useState, useRef } from "react";
+import { Payment } from "@airwallex/components-sdk";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 import { useForm, FormProvider } from "react-hook-form";
-
 import { 
     Country,
     ShipAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
+    ExtraMapPlaceOrderResult,
+    PlaceOrderResult,
+    PlaceOrderFunction
 } from "@/types/checkout/type";
 import AddressResultDisplay from "./AddressResultDisplay";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
@@ -30,8 +32,11 @@ import CommonModal from "@/components/theme/ui/CommonModal";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { CartDetail,CartAddress } from "@/types/cart/type";
 import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { clearCart } from "@/store/slices/cart-slice";
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -217,7 +222,7 @@ export default function CheckoutWrapper({
     countries: Country[];
     cartDetailData: CartDetail;
 }) {
-
+    const { createOrder } = usePlaceOrder(); 
     const addressFormDefaultValues = getAddressFormDataFromCart(cartDetailData,loginEmail);
     const dispatch = useAppDispatch();
     
@@ -252,7 +257,7 @@ export default function CheckoutWrapper({
 
     // useForm() 只会在组件初始化时读取一次 defaultValues
     const addressForm = useForm<FullAddressFormData>({
-        mode: "onBlur",          // 或 "onChange"
+        mode: "onChange",          // 或 "onChange"
         reValidateMode: "onChange",
         defaultValues: addressFormDefaultValues 
     });
@@ -351,8 +356,10 @@ export default function CheckoutWrapper({
                 dispatch(updateCart(newCartDetail));
                 setCartData({...newCartDetail});
             }
+            return true;
         } else {
             showToast(saveRes.msg, 'danger');
+            return false;
         }
 
     };
@@ -378,11 +385,9 @@ export default function CheckoutWrapper({
 
     };
 
-    // 下单
-    // 1. 校验地址 运输方式,支付方式;
-    // 2. 根据购物车详情判断是否需要传下单参数: 地址,运输方式,支付方式
-    const handlePlaceOrder = async () => { 
-        // 校验地址
+
+    // 校验地址 运输方式,支付方式;
+    const validateCheckout = async () => { 
         const addressValid = await addressForm.trigger();
 
         if(!addressValid) {
@@ -402,10 +407,16 @@ export default function CheckoutWrapper({
 
         if(methodPaymentRef.current) {
             const paymentMethodValid = await methodPaymentRef.current.validatePaymentMethod();
+            const paymentMethod = methodPaymentRef.current.getSelectPaymentMethod();
             if(!paymentMethodValid) {
                 showToast('Please select a payment method.', 'danger');
                 return false;
             }
+            if(paymentMethod === 'airwallex') {
+                const paymentMethodValid = methodPaymentRef.current.validateAirwallexCard();
+                return paymentMethodValid;
+            }
+
         } else {
             showToast('Please wait for payment method loading.', 'danger');
             return false;
@@ -417,7 +428,7 @@ export default function CheckoutWrapper({
         
         //如果购物车详情没有保存过支付方式,则需要保存支付方式
         if(cartData.paymentMethod === null) {
-            const pmethod = methodPaymentRef?.current.getSelectPaymentMethod();
+            const pmethod = methodPaymentRef.current.getSelectPaymentMethod();
             const saveRes = await saveCheckoutCart({ 
                 shippingMethod: '-1',
                 paymentMethod: pmethod,
@@ -433,7 +444,43 @@ export default function CheckoutWrapper({
         }
 
         return true;
+
     };
+    // 下单
+    const handlePlaceOrder: PlaceOrderFunction = async (params) => { 
+        // 校验地址
+        overlayLoading.start();
+        const validRes = await validateCheckout();
+        if(validRes) {
+            const res = await createOrder(params);
+            if(res.error) {
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: res.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            } else {
+                //创建订单成功后清空redux 购物车
+                dispatch(clearCart());  
+            }
+            const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
+            const resultData:PlaceOrderResult<keyof ExtraMapPlaceOrderResult> = {
+                valid: true, 
+                data: res.data,
+                otherData: null
+            }
+            if(pmethod === 'airwallex' && methodPaymentRef.current) {
+                const airwallexCardElements = methodPaymentRef.current.getAirwallexCardElements();
+                resultData.otherData = airwallexCardElements.cardNumber as Payment.CardNumberElementType;
+            }
+            return resultData;
+        } else {
+            overlayLoading.stop();
+            return {valid: false, data: null, otherData: null};
+        }
+    };                    
+    
     
 
 
@@ -526,12 +573,11 @@ export default function CheckoutWrapper({
 
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium">Payment Method</h3>
-                { paymentMethodLoading ?
-                    <LoadingPaymentMethod />
-                :   
+                { paymentMethodLoading && <LoadingPaymentMethod /> }
+                { (!paymentMethodLoading && paymentMethodError) && <p className="mt-3 text-red-500 text-ly-12">{paymentMethodError}</p>}
+                { (!paymentMethodLoading && !paymentMethodError) && 
                     <PaymentMethodCheckout 
                         ref={methodPaymentRef}
-                        errorMsg={paymentMethodError}
                         paymentMethods={paymentMethodDatas} 
                         selectedPaymentMethod={selectedPaymentMethod}
                         onPaymentMethodChange={handlePaymentMethodChange}

+ 4 - 2
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -2,7 +2,7 @@
 
 import { useState } from "react";
 import Image from "next/image";
-import { redirect, RedirectType } from 'next/navigation';
+import { useRouter } from 'next/navigation';
 import { OrderDetails } from "@/types/customer/type";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import ProductsModalPendingOrder from "./ProductsModalPendingOrder";
@@ -19,6 +19,8 @@ export default function ContinueToPayOrderInfo({
     currencySymbol: string;
 }) { 
     
+    const router = useRouter();
+    
     const [productsModalOpen, setProductsModalOpen] = useState(false);
 
     const countDownEnd = (new Date(orderDetail.createdAt)).getTime() + 24*3600*1000;
@@ -38,7 +40,7 @@ export default function ContinueToPayOrderInfo({
             content: "This order timeout, has been canceled. Will redirect to home page.",
             noCancel: true,
         }).then(() => {
-            redirect('/', RedirectType.replace);
+            router.replace('/');
         });  
     };
 

+ 164 - 20
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,8 +1,12 @@
 "use client";
 
 import Image from "next/image";
-import { use } from "react";
-import { useForm, get } from "react-hook-form";
+import { use,useState } from "react";
+import { useForm, get, useWatch } from "react-hook-form";
+import { useRouter } from 'next/navigation'
+import { useCustomToast } from "@/utils/hooks/useToast";
+import AirwallexCardInput from "../PaymentMethodAdditional/AirwallexCardInput";
+import {useAirwallexCard} from "@/lib/Airwallex/useAirwallexCard";
 import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
 import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
@@ -10,6 +14,7 @@ import PaypalButton from "../PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 
+/**@todo 优化目标:不使用 react-hook-form */
 export function PaymentMethodContinueTpPay({
     paymentMethodsDataPromise,
     defaultPaymentMethod,
@@ -19,14 +24,24 @@ export function PaymentMethodContinueTpPay({
     defaultPaymentMethod: {method: string; methodTitle: string;}
     orderId: string;
 }) {
+    const router = useRouter();
+    const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
     const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
 
     const { paymentRepay } = usePlaceOrder(); 
+
+    const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
+        cardNumber: 'airwallex_cardNumber', 
+        cvc: 'airwallex_cvc', 
+        expiry: 'airwallex_expiry'
+    });
+    const [isPay, setIsPay] = useState(false);
   
     const {
         register,
-        watch,
+        // watch,
+        control,
         formState: { errors }
     } = useForm({
         mode: "onChange",
@@ -38,19 +53,45 @@ export function PaymentMethodContinueTpPay({
 
     // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
     const paymentMethodError = get(errors, "paymentMethod");
-    const selectedPaymentMethod = watch("paymentMethod");
+    // react 19中使用watch 会提示React Hook Form's `useForm()` API returns a `watch()` function which cannot be memoized safely.
+    // const selectedPaymentMethod = watch("paymentMethod");
+    const selectedPaymentMethod = useWatch({
+        control,
+        name:"paymentMethod",
+    });
 
-    const handleCreateOrder = async () => {
-        return await paymentRepay({
+    const paypalRepayOrder = async () => {
+        overlayLoading.start();
+        const res = await paymentRepay({
             orderId: Number(orderId),
             paymentMethod: selectedPaymentMethod
         });
+        overlayLoading.stop();
+        if(!res.error && res.data !== null) {
+            return {
+                error: false,
+                msg: '',
+                orderId: res.data.gatewayOrderId,
+                webOrderId: res.data.orderId
+            };
+        } else {
+            confirmDialog({
+                title: "Warning",
+                content: res.msg + " Create order failed. Please try again.",
+                noCancel: true,
+            }).then(() => {});
+            return {
+                error: true,
+                msg: '',
+                orderId: '',
+                webOrderId: ''
+            };
+        }
 
     };
-    const handleClickPay = async () => {
-        return true;
-    };
+
     const airwallexRepay = async () => {
+        setIsPay(true);
         overlayLoading.start();
         const res = await paymentRepay({
             orderId: Number(orderId),
@@ -58,9 +99,9 @@ export function PaymentMethodContinueTpPay({
             paymentSuccessUrl: window.location.origin + '/paymentresult/result'
         });
         
-        if(!res.error && res.orderId) {
+        if(!res.error && res.data !== null) {
             // 创建订单成功了,跳转支付页面
-            window.location.href = res.orderId;
+            window.location.href = res.data.gatewayOrderId;
         } else {
             //创建订单出错了
             overlayLoading.stop();
@@ -70,17 +111,100 @@ export function PaymentMethodContinueTpPay({
                 noCancel: true,
             }).then(() => {});
         }
+        setIsPay(false);
+    };
+
+
+    const validateAirwallexCard = () => {
+
+        if(!airwallexCardState.cardNumberReady) {
+            showToast('Card number input has not ready. Please wait a minute.', 'danger');
+            return false;
+        }
+        if(!airwallexCardState.expiryReady) {
+            showToast('Card expiry date input has not ready. Please wait a minute.', 'danger');
+            return false;
+        }
+        if(!airwallexCardState.cvcReady) {
+            showToast('Card cvc input has not ready. Please wait a minute.', 'danger');
+            return false;
+        }
+
+        if(!airwallexCardState.cardNumberValid) {
+            showToast('Please input your card number.', 'danger');
+            return false;
+        }
+        if(!airwallexCardState.cvcValid) {
+            showToast('Please input cvc.', 'danger');
+            return false;
+        }
+        if(!airwallexCardState.expiryValid) {
+            showToast('Please input expiry date.', 'danger');
+            return false;
+        }
+        return true;
     };
+
+    const cardAirwallexRepay = async () => { 
+        setIsPay(true);
+        overlayLoading.start();
+        const validRes = validateAirwallexCard();
+        if(validRes) {
+           
+            const res = await paymentRepay({
+                orderId: Number(orderId),
+                paymentMethod: selectedPaymentMethod,
+            });
+            
+            if(!res.error && res.data !== null) {
+                // 创建订单成功
+                const cardNumberElement = getCardElements().cardNumber;
+                const paymentIntentIdClientSecret = res.data.gatewayOrderId.split(':');
+                const paymentIntentId = paymentIntentIdClientSecret[0];
+                const paymentIntentClientSecret = paymentIntentIdClientSecret[1];
+
+                cardNumberElement?.confirm({
+                    intent_id: paymentIntentId,
+                    client_secret: paymentIntentClientSecret,
+                }).then((result) => { 
+                    overlayLoading.stop();
+                    console.log('cardNumberElement.confirm result ---- ', result);
+                    // 支付成功 跳转页面
+                    router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=success');
+                }).catch((error) => {
+                    overlayLoading.stop();
+                    console.log('cardNumberElement.confirm error ---- ', error);
+                    // 支付失败
+                    router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=failure');
+                });
+            } else {
+                //创建订单出错了
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: res.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            }
+        }else {
+            overlayLoading.stop();
+        }
+        setIsPay(false);
+    };
+
+    const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
+
     return (
         <>
             <form name="shipping method" className="w-full box-border px-4 mt-4">
                 <div className="mt-3 w-full">
                     {paymentMethodsData.error && <p className="text-red-500 text-ly-12">{paymentMethodsData.error}</p>}
+                    <ul className="w-full">
                     {
                         paymentMethods.map((item) => {
-                            return (
-                                <label htmlFor={"paymentmethod-" + item.method} key={item.method}
-                                    className="group box-border flex items-center relative bg-ly-lightgray mt-3 p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
+                            return (<li className="w-full mt-3" key={item.method}>
+                                <label htmlFor={"paymentmethod-" + item.method} 
+                                    className="group box-border flex items-center relative bg-ly-lightgray  p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                                 >
                                     <p className="pr-2 flex-none relative">
                                         <Image className="block h-8 w-auto"
@@ -103,10 +227,22 @@ export function PaymentMethodContinueTpPay({
                                             required: 'Please select a shipping method.',
                                         })}
                                     />
-                                </label>
-                            )
+                                </label> 
+                                { item.method === "airwallex" &&
+                                    <div className="w-full mt-3"
+                                        style={{display: airwallexCardElementShow ? 'block' : 'none'}}
+                                    >
+                                        <AirwallexCardInput 
+                                            cardNumberErrorMsg={airwallexCardState.cardNumberErrorMsg}
+                                            expiryErrorMsg={airwallexCardState.expiryErrorMsg}
+                                            cvcErrorMsg={airwallexCardState.cvcErrorMsg}
+                                        />
+                                    </div>
+                                }                          
+                            </li>)
                         })
-                    }      
+                    }  
+                    </ul>    
                     
                     {paymentMethodError && <p className="text-red-500 text-ly-12">{paymentMethodError.message}</p>}  
                 </div> 
@@ -120,16 +256,24 @@ export function PaymentMethodContinueTpPay({
                 <div className="mt-6 w-full">
                     {selectedPaymentMethod === 'paypal_smart_button' && 
                         <PaypalButton
-                            createOrder={handleCreateOrder}
-                            onPaymentClick={handleClickPay}
+                            createOrder={paypalRepayOrder}
                             isRePay={true}
                         />
                     }
                     {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
                         <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
                             onClick={airwallexRepay}
+                            disabled={isPay}
+                        >
+                            Pay
+                        </button>
+                    }
+                    {selectedPaymentMethod === 'airwallex' && 
+                        <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                            onClick={cardAirwallexRepay}
+                            disabled={isPay}
                         >
-                            Place Order
+                            Pay
                         </button>
                     }
 

+ 21 - 82
src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx

@@ -1,8 +1,8 @@
 "use client";
 
 
-import {useRef} from "react";
-import { redirect, RedirectType } from 'next/navigation';
+import {useRef,useState} from "react";
+import { useRouter } from 'next/navigation';
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import {
   usePayPal,
@@ -22,17 +22,17 @@ import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 
 export default function PaypalButton({
     createOrder,
-    onPaymentClick,
     isRePay
 }:{
   createOrder: () => Promise<{orderId: string;webOrderId: string;error: boolean;msg: string;}>;
-  onPaymentClick: () => Promise<boolean>;
   isRePay?: boolean;
 }) { 
-
+    const router = useRouter();
 
     const {createPaymentCallback} = usePlaceOrder();
 
+    const [isPaying, setIsPaying] = useState(false);
+
     const gatewayOrderIdRef = useRef("");
     const webOrderIdRef = useRef("");
     // paypal sdk 是否加载完成
@@ -58,35 +58,22 @@ export default function PaypalButton({
     const handlerPaypalClick = async (buttonHandlerClick: () => Promise<void | {
         redirectURL?: string | undefined;
     }>) => {
-        overlayLoading.start();
-        const vaild = await onPaymentClick();
-        console.log('789----', vaild);
-        if(vaild) {
+
+            setIsPaying(true);
             const res = await createOrder();
-            if(res.error) {
-                overlayLoading.stop();
-                confirmDialog({
-                    title: "Warning",
-                    content: res.msg + " Create order failed. Please try again.",
-                    noCancel: true,
-                }).then(() => {});
-            } else {
+            if(!res.error) {
                 gatewayOrderIdRef.current = res.orderId;
                 webOrderIdRef.current = res.webOrderId;
                 return buttonHandlerClick();
+            } else {
+                setIsPaying(false);
             }
             
-        } else {
-            overlayLoading.stop();
-        }
     }
 
     // paypal支付回调配置
     const paypalOnHandlerConfig = {
         createOrder: async () => {
-            // const res = await createOrder();
-            // gatewayOrderIdRef.current = res.orderId;
-            // webOrderIdRef.current = res.webOrderId;
             const res = {
                 orderId: gatewayOrderIdRef.current, 
                 webOrderId: webOrderIdRef.current
@@ -102,6 +89,7 @@ export default function PaypalButton({
                 gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
                 status: 'success',
             });
+            overlayLoading.stop();
             if(!resCallback.error) {
                 // 支付成功,跳转到成功落地页 
                 confirmDialog({
@@ -109,7 +97,7 @@ export default function PaypalButton({
                     content: "Your payment was successful. Will redirect to success page.",
                     noCancel: true,
                 }).then(() => {
-                    redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success', RedirectType.replace);
+                    router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success');
                 });
             } else {
                 // callback 失败,提示错误
@@ -118,7 +106,7 @@ export default function PaypalButton({
                     content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderIdRef.current,
                     noCancel: true,
                 }).then(() => { 
-                    redirect('/', RedirectType.replace);
+                    router.replace('/');
                 });
                 
             }
@@ -137,33 +125,9 @@ export default function PaypalButton({
             });
             if(!isRePay) {
                 overlayLoading.stop();
-                redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=cancel', RedirectType.replace);
-                /*
-                if(isGuest) {
-                    await cancelOrder({
-                        orderId: Number(webOrderIdRef.current),
-                    });
-                    // 重新获取购物车
-                    dispatch(fetchCartDetail());
-                    confirmDialog({
-                        title: "Order Cancelled",
-                        content: "Your order has been cancelled. Will redirect to home page.",
-                        noCancel: true,
-                    }).then(() => { 
-                        redirect('/' , RedirectType.replace);
-                    });
+                router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=cancel');
                     
-                } else {
-                    // 登录用户 跳转二次支付页面 提醒用户可以二次支付
-                    confirmDialog({
-                        title: "Payment cancelled",
-                        content: "Will redirect to unpaied page.",
-                        noCancel: true,
-                    }).then(() => { 
-                        redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
-                    });
-                }
-                */
+
             } else {
                 // 登录用户在二次支付页面 
                 confirmDialog({
@@ -188,34 +152,8 @@ export default function PaypalButton({
             });
             if(!isRePay) {
                 overlayLoading.stop();
-                redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure', RedirectType.replace);
-                /*
-                if(isGuest) {
-                    
-                    await cancelOrder({
-                        orderId: Number(webOrderIdRef.current),
-                    });
-                    // 重新获取购物车
-                    dispatch(fetchCartDetail());
-                    confirmDialog({
-                        title: "Payment Failed",
-                        content: data.message + " Will redirect to home page.",
-                        noCancel: true,
-                    }).then(() => { 
-                        redirect('/', RedirectType.replace);
-                    });
-                } else {
-                    // 弹出错误提示弹窗:显示错误信息
-                    // 如果是登录用户,跳转二次支付页面 提醒用户可以二次支付
-                    confirmDialog({
-                        title: "Payment Failed",
-                        content: data.message + " Will redirect to unpaied page.",
-                        noCancel: true,
-                    }).then(() => { 
-                        redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
-                    });
-                }
-                */
+                router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure');
+
             } else {
                 
                 // 登录用户 二次支付页面 
@@ -231,6 +169,7 @@ export default function PaypalButton({
 
         onComplete: (data: OnCompleteData) => {
             overlayLoading.stop();
+            setIsPaying(false);
             console.log("Payment session completed");
             console.log("On Complete data:", data);
         },
@@ -284,18 +223,18 @@ export default function PaypalButton({
                         :
                             <paypal-button onClick={() => {
                                 return handlerPaypalClick(paypalButtonHandleClick);
-                            }} type="pay" className="w-full" />
+                            }} type="pay" className="w-full" disabled={isPaying} />
                         }
                         {guestButtonError ?
                             <p className="text-ly-errorcolor text-ly-12">Error: {guestButtonError.message}</p>
                         :
                             <paypal-basic-card-container className="w-full mt-3">
-                                <paypal-basic-card-button ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
+                                <paypal-basic-card-button disabled={isPaying} ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
                             </paypal-basic-card-container>
                         }
                      
                         {isPayLaterEligible && 
-                            <paypal-pay-later-button className="w-full mt-3"
+                            <paypal-pay-later-button className="w-full mt-3" disabled={isPaying}
                                 onClick={() => handlerPaypalClick(paylaterButtonHandleClick)}
                                 countryCode={payLaterDetails?.countryCode}
                                 productCode={payLaterDetails?.productCode}

+ 41 - 0
src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx

@@ -0,0 +1,41 @@
+"use client";
+
+export default function AirwallexCardInput({ 
+    cardNumberErrorMsg,
+    expiryErrorMsg,
+    cvcErrorMsg,
+} : {
+    cardNumberErrorMsg: string;
+    expiryErrorMsg: string;
+    cvcErrorMsg: string;
+}) {
+
+    return (
+
+        <div id="airwallex-dropIn-card-container" className="w-full">
+            <div className="w-full">
+                <div id="airwallex_cardNumber" className="w-full border-1 border-ly-inputborder h-11.5"></div>
+                {cardNumberErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{cardNumberErrorMsg}</p>}
+            </div>
+            <div className="mt-3 flex justify-between gap-4">
+                <div className="w-full">
+                    <div id="airwallex_expiry" className="w-full border-1 border-ly-inputborder h-11.5"></div>
+                    {expiryErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{expiryErrorMsg}</p>}
+                </div>
+                <div className="w-full">
+                    <div id="airwallex_cvc" className="w-full border-1 border-ly-inputborder h-11.5"></div>
+                    {cvcErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{cvcErrorMsg}</p>}
+                </div>
+            </div>
+           
+            <div className="mt-2 w-full flex items-center">
+                <svg className="flex-none w-4.5 h-4.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="1.4em" height="1.4em" viewBox="0 0 24 24" id="pci-svg">
+                    <path d="m18,8l-1,0l0,-2c0,-2.76 -2.24,-5 -5,-5s-5,2.24 -5,5l0,2l-1,0c-1.1,0 -2,0.9 -2,2l0,10c0,1.1 0.9,2 2,2l12,0c1.1,0 2,-0.9 2,-2l0,-10c0,-1.1 -0.9,-2 -2,-2zm-6,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2s-0.9,2 -2,2zm3.1,-9l-6.2,0l0,-2c0,-1.71 1.39,-3.1 3.1,-3.1c1.71,0 3.1,0.92125 3.1,2.84372l0,-0.78122l0,3.0375z"></path>
+                </svg>
+                <p className="text-ly-12 leading-ly-18">
+                    Your card details are protected using PCI DSS v3.2 security standards.      
+                </p>
+            </div>
+        </div>
+    )
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 119 - 64
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx


+ 0 - 2
src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx

@@ -6,8 +6,6 @@ import { useForm, get } from "react-hook-form";
 
 import {CheckoutShippingRate} from "@/types/checkout/type";
 
-import { useAppSelector } from "@/store/hooks";
-
 
 
 /**

+ 3 - 3
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -10,6 +10,7 @@ import { OrderDetailsData } from "@/types/customer/type";
 import { CurrenciesData } from "@/types/currency/type";
 import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
+import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
 
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
@@ -45,8 +46,7 @@ export default async function ContinueToPay({searchParams}: {
     const orderid = query?.orderid;
     const orderDetailResponse = await authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
         id: orderid,// `/api/shop/customer-orders/${orderid}`
-    });
-    console.log("orderDetailPromise ==========",orderDetailResponse); 
+    }); 
     const orderDetailData = orderDetailResponse.data?.customerOrder;
     if(orderDetailData === undefined || orderDetailData?.status !== "pending")  {
         redirect('/', RedirectType.replace);
@@ -77,7 +77,7 @@ export default async function ContinueToPay({searchParams}: {
                 <h3 className="text-ly-24 font-medium">Payment Method</h3>
 
             </div>
-            <Suspense fallback={<div>Loading...</div>}> 
+            <Suspense fallback={<LoadingPaymentMethod/>}> 
                 <PaymentMethodContinueTpPay 
                     orderId={orderDetailData.incrementId}
                     paymentMethodsDataPromise={paymentMethodsResponse}

+ 14 - 2
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {use} from "react";
+import {use, useEffect} from "react";
 import Link from "next/link";
 import Image from "next/image";
 import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
@@ -31,8 +31,10 @@ function getProductAdditionalInfo(productItem: ProductItemAdditional) {
 
 export default function OrderDetailWrapper({
     orderDetailPromise,
+    isGuest
 }: {
-    orderDetailPromise: Promise<GraphqlRequestResult<OrderDetailsData>>,
+    orderDetailPromise: Promise<GraphqlRequestResult<OrderDetailsData>>;
+    isGuest: boolean;
 }) { 
 
     const {currencies} = useCurrencyList();
@@ -49,6 +51,16 @@ export default function OrderDetailWrapper({
     }) || [];
     const currency = currencies?.find(item => item.code === orderDetailRes?.orderCurrencyCode) || null;
 
+    useEffect(() => {
+        // 如果是游客且支付成功,删除cookie里的GUEST_CART_TOKEN GUEST_CART_ID
+        /**Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#options */
+
+        if(isGuest) {
+            fetch("/api/clearCartCookie",{
+                method:"POST"
+            });
+        }
+    }, [isGuest]);
 
     return (
         <>

+ 5 - 9
src/app/(public)/paymentresult/result/page.tsx

@@ -10,7 +10,7 @@ import SuccessPageSkeleton from "../_components/SuccessPageSkeleton";
 import SkeletonCancelOrFailure from "../_components/SkeletonCancelOrFailure";
 import CancelOrFailure from "../_components/CancelOrFailure";
 import OrderDetailWrapper from "../_components/OrderDetailWrapper";
-import { GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
+import { GUEST_CART_TOKEN } from "@/utils/constants";
 
 // 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
 export default async function SuccessPage({searchParams}: {
@@ -37,23 +37,19 @@ export default async function SuccessPage({searchParams}: {
     const orderDetailPromise = authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
         id: orderid,// `/api/shop/customer-orders/${orderid}`
     });
-    // 如果是游客且支付成功,删除cookie里的GUEST_CART_TOKEN GUEST_CART_ID
-    if(isGuest && paymentResult === 'success') {
-        cookieStore.delete(GUEST_CART_TOKEN);
-        cookieStore.delete(GUEST_CART_ID);
-    }
+
     return (
         
         <div className="w-full">
             {paymentResult === 'success' && 
                 <Suspense fallback={<SuccessPageSkeleton />}>
-                    <OrderDetailWrapper orderDetailPromise={orderDetailPromise} />
+                    <OrderDetailWrapper orderDetailPromise={orderDetailPromise} 
+                        isGuest={isGuest}
+                    />
                 </Suspense>
 
             }
             {(paymentResult === 'cancel' || paymentResult === 'failure') &&
-            //http://localhost:3001/paymentresult/result?orderId=2643&awx_return_result=cancel
-            // http://localhost:3001/paymentresult/result?orderId=2644&awx_return_result=failure
                 <Suspense fallback={<SkeletonCancelOrFailure/>}>
                     <CancelOrFailure isGuest={isGuest} orderDetailPromise={orderDetailPromise} paymentStatus={paymentResult} />
                 </Suspense>

+ 20 - 0
src/app/api/clearCartCookie/route.ts

@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import {
+  GUEST_CART_TOKEN,
+  GUEST_CART_ID,
+} from "@/utils/constants";
+
+
+export async function POST() {
+
+    const response = NextResponse.json({
+        success:true
+    });
+
+
+    response.cookies.delete(GUEST_CART_TOKEN);
+    response.cookies.delete(GUEST_CART_ID);
+
+
+    return response;
+}

+ 0 - 1
src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx

@@ -62,7 +62,6 @@ function PhoneNumberInput({
     onHasUserSelectedPhoneCode,
     hasUserSelectedPhoneCode
 }: PhoneNumberInputProps) {
-    console.log(' value ===========', value);
     // const DEFAULT_COUNTRY_CODE = "US";
     const phoneCodeList = COUNTRY_PHONECODE;
 

+ 84 - 0
src/lib/Airwallex/airwallexCardManager.ts

@@ -0,0 +1,84 @@
+import { createElement, Payment } from "@airwallex/components-sdk";
+import { AirwallexCardElements } from "@/types/checkout/type";
+
+type ElementType =
+  | Payment.CardNumberElementType
+  | Payment.CvcElementType
+  | Payment.ExpiryDateElementType;
+
+
+class AirwallexManager {
+  private elements: AirwallexCardElements = {};
+
+  private listeners = new Map<ElementType, Map<Payment.SplitElementEventCode, boolean>>();
+
+  private created = false;
+
+  async init() {
+    if (this.created) return;
+    this.created = true;
+  }
+
+  async createElements() {
+    if (this.elements.cardNumber) return this.elements;
+
+    const cardNumber = await createElement("cardNumber");
+    const cvc = await createElement("cvc");
+    const expiry = await createElement("expiry");
+
+    this.elements = { cardNumber, cvc, expiry };
+
+    return this.elements;
+  }
+
+  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);
+  }
+
+  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>) {
+    let eventMap = this.listeners.get(el);
+    
+    if (!eventMap) {
+      eventMap = new Map<Payment.SplitElementEventCode, boolean>([[event, false]]);
+      this.listeners.set(el, eventMap);
+    }
+    if(eventMap.get(event)) return; // 注册过就返回
+
+    el.on(event, handler);
+
+    eventMap.set(event,true);
+    
+  }
+
+
+
+  destroy() {
+    const { cardNumber, cvc, expiry } = this.elements;
+
+    [cardNumber, cvc, expiry].forEach((el) => {
+      if (!el) return;
+      el.unmount();
+      el.destroy();
+    });
+
+    this.elements = {};
+  }
+
+  getElements() {
+    return this.elements;
+  }
+}
+
+export const airwallexManager = new AirwallexManager();

+ 18 - 0
src/lib/Airwallex/airwallexInit.ts

@@ -0,0 +1,18 @@
+import { init, Env } from '@airwallex/components-sdk';
+
+let initialized = false; // 全局标志,防止重复初始化
+
+const envMap = {
+    "production": "prod",
+    "development": "demo",
+    "test": "demo"
+};
+
+export async function airwallexInit() {
+  if (initialized) return;
+  await init({
+    env: envMap[process.env.NODE_ENV] as Env, // 生产环境为 'prod'
+    enabledElements: ['payments'],
+  });
+  initialized = true;
+}

+ 212 - 0
src/lib/Airwallex/useAirwallexCard.ts

@@ -0,0 +1,212 @@
+import { useEffect, useRef, useReducer, useEffectEvent } from "react";
+import { airwallexManager } from "./airwallexCardManager";
+import { airwallexInit } from "@/lib/Airwallex/airwallexInit";
+import { Payment } from "@airwallex/components-sdk";
+import { AirwallexCardStateType } from "@/types/checkout/type";
+
+interface EventCallback {
+  focusCallback: (e: any) => void;
+  blurCallback: (e: any) => void;
+  changeCallback: (e: any) => void;
+  readyCallback: (e: any) => void;
+
+}
+
+function airwallexCardReducer(state: AirwallexCardStateType, action: Partial<AirwallexCardStateType>) { 
+    // 这里不需要action的type属性,因为这里只需要覆盖state
+    const newState = Object.assign({}, {...state}, {...action});
+    return {...newState};
+}
+
+
+
+export function useAirwallexCard(
+  enabled: boolean, 
+  mountId: { cardNumber: string; cvc: string; expiry: string; },
+  // callback: EventCallback
+) {
+
+  const defaultAirwallexCardState: AirwallexCardStateType = {
+      cardNumberReady: false,
+      cvcReady: false,
+      expiryReady: false,
+
+      cardNumberValid: false,
+      cvcValid: false,
+      expiryValid: false,
+
+      cardNumberErrorMsg: "",
+      cvcErrorMsg: "",
+      expiryErrorMsg: "",
+  };
+  const [airwallexCardState, airwallexCardDispatch] = useReducer(airwallexCardReducer, defaultAirwallexCardState);
+  const readyAirwallexElement = useEffectEvent((event: any): void => {
+      console.log('----------------------------onReady---------------------');
+      const { type } = event.detail;
+      if (type === 'cardNumber') {
+          airwallexCardDispatch({
+              cardNumberReady: true
+          });
+      }
+      if (type === 'cvc') {
+          airwallexCardDispatch({
+              cvcReady: true
+          });
+      }
+      if (type === 'expiry') {
+          airwallexCardDispatch({
+              expiryReady: true
+          });
+      }
+  });
+
+
+    const changeAirwallexElement = useEffectEvent((event: any) => {
+        console.log('----------------------------onChange---------------------');
+        const { type, complete, error } = event.detail;
+        const msg = error?.message ?? '';
+        if (type === 'cardNumber') {
+            airwallexCardDispatch({
+                cardNumberValid: complete,
+                cardNumberErrorMsg: msg
+            });
+        }
+        if (type === 'cvc') {
+            airwallexCardDispatch({
+                cvcValid: complete,
+                cvcErrorMsg: msg
+            });
+        }
+        if (type === 'expiry') {
+            airwallexCardDispatch({
+                expiryValid: complete,
+                expiryErrorMsg: msg
+            });
+        }
+    });
+
+    const focusAirwallexElement = useEffectEvent((event: any) => {
+        console.log('----------------------------onFocus---------------------');
+        const { type } = event.detail;
+        if (type === 'cardNumber') {
+            airwallexCardDispatch({
+                cardNumberErrorMsg: ''
+            });
+        }
+        if (type === 'cvc') {
+            airwallexCardDispatch({
+                cvcErrorMsg: ''
+            });
+        }
+        if (type === 'expiry') {
+            airwallexCardDispatch({
+                expiryErrorMsg: ''
+            });
+        }
+    });
+
+    const blurAirwallexElement = useEffectEvent((event: any) => {
+        console.log('----------------------------onBlur---------------------');
+        const { type, error } = event.detail;
+        const msg = error?.message ?? JSON.stringify(error);
+        if (type === 'cardNumber') {
+            airwallexCardDispatch({
+                cardNumberErrorMsg: msg,
+            });
+        }
+        if (type === 'cvc') {
+            airwallexCardDispatch({
+                cvcErrorMsg: msg,
+            });
+        }
+        if (type === 'expiry') {
+            airwallexCardDispatch({
+                expiryErrorMsg: msg,
+            });
+        }
+    });
+
+
+
+
+
+
+  const mountedRef = useRef(false);
+  const elementRef = useRef<{
+    cardNumber?: Payment.CardNumberElementType;
+    cvc?: Payment.CvcElementType;
+    expiry?: Payment.ExpiryDateElementType;
+  }>({});
+
+  useEffect(() => {
+    if (!enabled) return;
+
+    let destroyed = false;
+
+    const init = async () => {
+      await airwallexInit();
+      await airwallexManager.init();
+
+      const els = await airwallexManager.createElements();
+      elementRef.current = els;
+
+      if (destroyed) return;
+
+      airwallexManager.mount({
+        cardNumber: mountId.cardNumber,
+        cvc: mountId.cvc,
+        expiry: mountId.expiry
+      });
+
+      mountedRef.current = true;
+
+      // register events ONLY ONCE
+      bindEvents(els,{
+        focusCallback: focusAirwallexElement,
+        blurCallback: blurAirwallexElement, 
+        changeCallback: changeAirwallexElement, 
+        readyCallback: readyAirwallexElement
+      });
+    };
+
+    init();
+
+    return () => {
+      destroyed = true;
+      if (mountedRef.current) {
+        airwallexManager.unmount();// 解除挂载
+        mountedRef.current = false;
+      }
+    };
+  }, [enabled]);
+
+
+
+  return {
+      getCardElements:() => elementRef.current,
+      airwallexCardState: airwallexCardState
+  };
+
+}
+
+function bindEvents(els: any,callback:EventCallback) {
+  const { cardNumber, cvc, expiry } = els;
+  const {focusCallback,blurCallback, changeCallback, readyCallback} = callback;
+
+  // ⚠️ 关键:绑定只发生一次(manager内部防重复)
+  cardNumber.on("focus", focusCallback);
+  cardNumber.on("blur", blurCallback);
+  cardNumber.on("change", changeCallback);
+  cardNumber.on("ready", readyCallback);
+
+
+  cvc.on("focus", focusCallback);
+  cvc.on("blur", blurCallback);
+  cvc.on("change", changeCallback);
+  cvc.on("ready", readyCallback);
+
+  expiry.on("focus", focusCallback);
+  expiry.on("blur", blurCallback);
+  expiry.on("change", changeCallback);
+  expiry.on("ready", readyCallback);
+}

+ 1 - 0
src/lib/graphql-fetch.ts

@@ -271,6 +271,7 @@ export async function authorizationGraphqlQuery<
                 },
             }
         };
+        console.log('authorizationGraphqlQuery token ---- ', token);
         const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
         console.log('authorizationGraphqlQuery result ---- ', result);
         const resData = result.data || null;

+ 39 - 0
src/types/checkout/type.ts

@@ -1,4 +1,5 @@
 import {CartDetail} from "@/types/cart/type";
+import { Payment } from '@airwallex/components-sdk';
 
 export type AddressType = "cart_billing" | "cart_shipping";
 
@@ -467,4 +468,42 @@ export interface CreatePaymentReplayVariables {
   paymentSuccessUrl?: string;
   paymentFailureUrl?: string;
   paymentCancelUrl?: string;
+}
+
+
+// 结账页额外用到的类型
+export type ExtraMapPlaceOrderResult = {
+  AirwallexCad: Payment.CardNumberElementType ;
+  
+};
+
+export type ExtraDataPlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> = ExtraMapPlaceOrderResult[Code];
+export interface PlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> {
+    valid: boolean; 
+    data: CreatePaymentInitiate | null;
+    otherData: ExtraDataPlaceOrderResult<Code> | null;
+}
+
+export type PlaceOrderFunction = (params?: CreatePaymentInitiateVariables) => Promise<PlaceOrderResult<keyof ExtraMapPlaceOrderResult>>
+
+export interface AirwallexCardElements{
+  cardNumber?: Payment.CardNumberElementType;
+  cvc?: Payment.CvcElementType;
+  expiry?: Payment.ExpiryDateElementType;
+}
+
+
+
+export interface AirwallexCardStateType {
+    cardNumberReady: boolean;
+    cvcReady: boolean;
+    expiryReady: boolean;
+
+    cardNumberValid: boolean;
+    cvcValid: boolean;
+    expiryValid: boolean;
+
+    cardNumberErrorMsg: string;
+    cvcErrorMsg: string;
+    expiryErrorMsg: string;
 }

+ 6 - 8
src/utils/hooks/usePlaceOrder.ts

@@ -12,10 +12,11 @@ import {
     CreatePaymentInitiate,
     CreatePaymentCallback,
     CreateCancelOrder,
-    CreatePaymentReplayVariables
+    CreatePaymentReplayVariables,
+    CreatePaymentReplay
 } from "@/types/checkout/type";
 
-interface CreateOrderResult {
+export interface CreateOrderResult {
     data: CreatePaymentInitiate | null;
     msg: string;
     error: boolean;
@@ -31,9 +32,8 @@ interface CreateCancelOrderResult {
     error: boolean;
 }
 interface PaymentReplayResult {
-    orderId: string;
-    webOrderId: string;
     error: boolean;
+    data: CreatePaymentReplay | null;
     msg: string;
 }
 export const usePlaceOrder = () => {
@@ -118,16 +118,14 @@ export const usePlaceOrder = () => {
             const result: PaymentReplayResult = {
                 error: false,
                 msg: '',
-                orderId: resData?.gatewayOrderId ?? '',
-                webOrderId: resData?.orderId ?? ''
+                data: resData
             };
             return result;
         }).catch((err) => {
             const result: PaymentReplayResult = {
                 error: true,
                 msg: err.message,
-                orderId: '',
-                webOrderId: ''
+                data: null,
             };
             return result;
         });