zgl 23 часов назад
Родитель
Сommit
bf9d485b9d
48 измененных файлов с 1509 добавлено и 704 удалено
  1. 22 8
      README.md
  2. 1 0
      package.json
  3. 15 0
      pnpm-lock.yaml
  4. 0 70
      src/app/(checkout)/checkout/_components/CheckoutAddressModal.tsx
  5. 72 23
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  6. 225 107
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  7. 4 2
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  8. 192 46
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  9. 0 1
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  10. 12 0
      src/app/(checkout)/checkout/_components/LoadingCheckoutPlaceOrder.tsx
  11. 46 88
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx
  12. 41 0
      src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx
  13. 122 66
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  14. 1 3
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  15. 18 24
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  16. 8 7
      src/app/(checkout)/checkout/continuetopay/page.tsx
  17. 6 5
      src/app/(checkout)/checkout/page.tsx
  18. 101 0
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  19. 20 0
      src/app/(public)/paymentresult/_components/Faqs.tsx
  20. 17 17
      src/app/(public)/success/_components/OrderDetailWrapper.tsx
  21. 34 0
      src/app/(public)/paymentresult/_components/SkeletonCancelOrFailure.tsx
  22. 0 0
      src/app/(public)/paymentresult/_components/SuccessPageSkeleton.tsx
  23. 60 0
      src/app/(public)/paymentresult/result/page.tsx
  24. 0 33
      src/app/(public)/success/page.tsx
  25. 20 0
      src/app/api/clearCartCookie/route.ts
  26. 1 1
      src/app/api/graphql/route.ts
  27. 0 1
      src/components/cart/CartModal.tsx
  28. 1 1
      src/components/common/ConfirmModal.tsx
  29. 10 0
      src/components/common/portal/Portal.tsx
  30. 6 10
      src/components/Portal.tsx
  31. 4 2
      src/components/theme/ui/CommonModal.tsx
  32. 0 1
      src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx
  33. 6 0
      src/graphql/checkout/mutations/CreateCheckoutOrder.ts
  34. 6 0
      src/graphql/checkout/mutations/CreatePaymentReplay.ts
  35. 16 0
      src/graphql/checkout/queries/GetRepayOrderPaymentMethods.ts
  36. 2 1
      src/graphql/checkout/queries/index.ts
  37. 84 0
      src/lib/Airwallex/airwallexCardManager.ts
  38. 18 0
      src/lib/Airwallex/airwallexInit.ts
  39. 212 0
      src/lib/Airwallex/useAirwallexCard.ts
  40. 1 1
      src/lib/ApolloErrorHandler.ts
  41. 1 0
      src/lib/graphql-fetch.ts
  42. 12 8
      src/providers/ReduxProvider.tsx
  43. 52 0
      src/types/checkout/type.ts
  44. 0 163
      src/utils/hooks/useCheckout.ts
  45. 1 1
      src/utils/hooks/useCheckoutAddress.ts
  46. 3 6
      src/utils/hooks/useCheckoutPaymentMethod.ts
  47. 3 6
      src/utils/hooks/useCheckoutShippingMethod.ts
  48. 33 2
      src/utils/hooks/usePlaceOrder.ts

+ 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)':

+ 0 - 70
src/app/(checkout)/checkout/_components/CheckoutAddressModal.tsx

@@ -1,70 +0,0 @@
-"use client";
-
-import {
-  Drawer,
-  DrawerContent,
-  DrawerHeader,
-  DrawerBody,
-  DrawerFooter
-} from "@heroui/drawer";
-
-export default function CheckoutAddressModal({ 
-    isOpen,
-    onClose,
-    // onSave,
-    body,
-    footer
-}: {
-    isOpen: boolean;
-    onClose: (e: boolean) => void;
-    // onSave: () => void;
-    body: React.ReactNode;
-    footer: React.ReactNode;
-}) {
-
-    const openChange = (e: boolean) => {
-        onClose(e);
-        console.log('address modal open change ---- ', e);
-    }
-
-    return (
-   
-            <Drawer
-                backdrop={"blur"}
-                placement="bottom"
-                isDismissable={false}
-                isKeyboardDismissDisabled={true}
-                isOpen={isOpen}
-                hideCloseButton
-                onOpenChange={(e) => openChange(e)}
-            >
-                <DrawerContent className="rounded-none h-17/20 max-h-none">
-                {(onClose) => (
-                    <>
-                        <DrawerHeader className="p-0">
-                            <div className="w-full box-border p-3.75 flex justify-between items-center">
-                                <span>Shipping Address</span>
-                                <button className="w-6 h-6" onClick={onClose}>
-                                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
-                                </button>
-                            </div>
-                        </DrawerHeader>
-                        <DrawerBody className="drawer-body relative p-3.75">
-                            <p className="text-ly-12 leading-ly-20">
-                                Select a billing address from your address book or enter a new address.
-                            </p>
-                            <div>
-                                {body}
-                            </div>
-                            
-                        </DrawerBody>
-                        <DrawerFooter className="box-border border-t border-gray-200 p-3.75">
-                            {footer}
-                        </DrawerFooter>
-                    </>
-                )}
-                </DrawerContent>
-            </Drawer>
-  
-    ); 
-}

+ 72 - 23
src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx

@@ -1,35 +1,73 @@
 "use client";
 
-import { useState } from "react";
-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 { PlaceOrderFunction } from "@/types/checkout/type";
 export default function CheckoutPlaceOrder({
     paymentMethod,
     clickPlaceOrder, // 校验运输方式,地址 
 }: {
     paymentMethod: string;
-    clickPlaceOrder: () => Promise<boolean>;
-}) { 
-    const { createOrder } = usePlaceOrder(); 
+    clickPlaceOrder: PlaceOrderFunction;
+}) {
+    const router = useRouter();
     
-    const handleCreateOrder = async () => {
-        const res = await createOrder();
-        console.log("createOrder res ---- ", res);
-        if(!res.error && res.data) {
+    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 () => {
+
+        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();
+                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');
+            });
         }
     };
 
@@ -38,15 +76,26 @@ export default function CheckoutPlaceOrder({
             <div className="relative w-full">
                 {paymentMethod === 'paypal_smart_button' && 
                     <PaypalButton
-                        createOrder={handleCreateOrder}
-                        onPaymentClick={clickPlaceOrder}
+                        createOrder={paypalCreateOrder}
                     />
                 }
+                {(paymentMethod === 'awxklarna' || paymentMethod === '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={airwallexPlaceOrder}
+                    >
+                        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>
-           
-            {/* <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">
-                Place Order
-            </button> */}
         </>
     );
 }

+ 225 - 107
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,20 +1,20 @@
 "use client";
 
-import {useState, useEffect, 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,
-    BillAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
+    ExtraMapPlaceOrderResult,
+    PlaceOrderResult,
+    PlaceOrderFunction
 } from "@/types/checkout/type";
 import AddressResultDisplay from "./AddressResultDisplay";
-import AddressResultDisplayLoading from "./AddressResultDisplayLoading";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
 import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
@@ -22,17 +22,21 @@ import { useCustomToast } from "@/utils/hooks/useToast";
 import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
 import {useCheckoutPaymentMethod} from  "@/utils/hooks/useCheckoutPaymentMethod";
 import {useCheckoutShippingMethod} from  "@/utils/hooks/useCheckoutShippingMethod";
-import { useAppDispatch,useAppStore  } from "@/store/hooks";
+import { useAppDispatch } from "@/store/hooks";
 import { updateCart } from "@/store/slices/cart-slice";
-import { useCartDetail } from "@utils/hooks/useCartDetail";
 import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
 import LoadingPaymentMethod from "./LoadingPaymentMethod";
+import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
 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 { CartDetail } from "@/types/cart/type";
+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
@@ -41,6 +45,119 @@ import { CartDetail } from "@/types/cart/type";
  * @todo 购物车为空 跳转到空购物车页面
  */
 
+/**
+ * 地址表单默认值
+ */
+function getBillingAddressFormData(billingAddress:CartAddress) {
+    const billingCountry = billingAddress.country || 'US';
+    const billingPhoneNumber =  normalizePhoneForForm(billingAddress.phone, billingCountry) || "";
+    const res = {
+        "billingAddressId": billingAddress.id,
+        "billingEmail": billingAddress.email,
+        "billingFirstName": billingAddress.firstName,
+        "billingLastName": billingAddress.lastName,
+        "billingCompanyName": "",
+        "billingAddress": billingAddress.address,
+        "billingCountry": billingCountry,
+        "billingState": billingAddress.state,
+        "billingCity": billingAddress.city,
+        "billingPostcode": billingAddress.postcode,
+        "billingPhoneNumber": billingPhoneNumber,
+    };
+
+    return res;
+}
+function getShippingAddressFormData(shippingAddress:CartAddress) {
+    const shippingCountry = shippingAddress.country || 'US';
+    const shippingPhoneNumber =  normalizePhoneForForm(shippingAddress.phone, shippingCountry) || "";
+    const res = {
+        "shippingAddressId": shippingAddress.id,
+        "shippingEmail": shippingAddress.email,
+        "shippingFirstName": shippingAddress.firstName,
+        "shippingLastName": shippingAddress.lastName,
+        "shippingCompanyName": "",
+        "shippingAddress": shippingAddress.address,
+        "shippingCountry": shippingCountry,
+        "shippingState": shippingAddress.state,
+        "shippingCity": shippingAddress.city,
+        "shippingPostcode": shippingAddress.postcode,
+        "shippingPhoneNumber": shippingPhoneNumber,
+    };
+
+    return res;
+}
+function getAddressFormDataFromCart(cartDetail:CartDetail, loginEmail?: string) {
+    const shippingAddress = cartDetail.shippingAddress;
+    const billingAddress = cartDetail.billingAddress;
+    const defaultValues = {
+        "shippingAddressId": "",
+        "shippingEmail": loginEmail ?? '',
+        "shippingFirstName": "",
+        "shippingLastName": "",
+        "shippingCompanyName": "",
+        "shippingAddress": "",
+        "shippingCountry": "US",
+        "shippingState": "",
+        "shippingCity": "",
+        "shippingPostcode": "",
+        "shippingPhoneNumber": "",
+        "billingAddressId": "",
+        "billingEmail": loginEmail ?? '',
+        "billingFirstName": "",
+        "billingLastName": "",
+        "billingCompanyName": "",
+        "billingAddress": "",
+        "billingCountry": "US",
+        "billingState": "",
+        "billingCity": "",
+        "billingPostcode": "",
+        "billingPhoneNumber": "",
+        "billingSameAsShipping": true
+    }
+    if(billingAddress) {
+        const bs = getBillingAddressFormData(billingAddress);
+        defaultValues.billingAddressId = bs.billingAddressId;
+        defaultValues.billingEmail = bs.billingEmail;
+        defaultValues.billingFirstName = bs.billingFirstName;
+        defaultValues.billingLastName = bs.billingLastName;
+        defaultValues.billingCompanyName = bs.billingCompanyName;
+        defaultValues.billingAddress = bs.billingAddress;
+        defaultValues.billingCountry = bs.billingCountry;
+        defaultValues.billingState = bs.billingState;
+        defaultValues.billingCity = bs.billingCity;
+        defaultValues.billingPostcode = bs.billingPostcode;
+        defaultValues.billingPhoneNumber = bs.billingPhoneNumber;
+        // defaultValues.billingSameAsShipping = true;
+    }
+    if(shippingAddress) {
+        const ss = getShippingAddressFormData(shippingAddress);
+        defaultValues.shippingAddressId = ss.shippingAddressId;
+        defaultValues.shippingEmail = ss.shippingEmail;
+        defaultValues.shippingFirstName = ss.shippingFirstName;
+        defaultValues.shippingLastName = ss.shippingLastName;
+        defaultValues.shippingCompanyName = ss.shippingCompanyName;
+        defaultValues.shippingAddress = ss.shippingAddress;
+        defaultValues.shippingCountry = ss.shippingCountry;
+        defaultValues.shippingState = ss.shippingState;
+        defaultValues.shippingCity = ss.shippingCity;
+        defaultValues.shippingPostcode = ss.shippingPostcode;
+        defaultValues.shippingPhoneNumber = ss.shippingPhoneNumber;
+    }
+    if( defaultValues.billingEmail === defaultValues.shippingEmail &&
+        defaultValues.billingFirstName === defaultValues.shippingFirstName &&
+        defaultValues.billingLastName === defaultValues.shippingLastName &&
+        defaultValues.billingCompanyName === defaultValues.shippingCompanyName &&
+        defaultValues.billingAddress === defaultValues.shippingAddress &&
+        defaultValues.billingCountry === defaultValues.shippingCountry &&
+        defaultValues.billingState === defaultValues.shippingState &&
+        defaultValues.billingCity === defaultValues.shippingCity &&
+        defaultValues.billingPostcode === defaultValues.shippingPostcode &&
+        defaultValues.billingPhoneNumber === defaultValues.shippingPhoneNumber
+    ) {
+        defaultValues.billingSameAsShipping = true;
+    }
+    return defaultValues;
+}
 /**
  * 生成createCheckoutAddress接口的参数
  */
@@ -99,13 +216,17 @@ function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateC
 export default function CheckoutWrapper({
     loginEmail,
     countries,
+    cartDetailData,
 }: {
     loginEmail: string;
-    countries: Country[],
+    countries: Country[];
+    cartDetailData: CartDetail;
 }) {
-
+    const { createOrder } = usePlaceOrder(); 
+    const addressFormDefaultValues = getAddressFormDataFromCart(cartDetailData,loginEmail);
     const dispatch = useAppDispatch();
-    const { cartData } = useCartDetail();
+    
+    const [cartData, setCartData] = useState<CartDetail>(cartDetailData);
     const {saveCheckoutCart} = useSaveCheckoutCart();
     
     const {
@@ -116,7 +237,8 @@ export default function CheckoutWrapper({
     } = useCheckoutShippingMethod();
 
 
-    const selectedPaymentMethod = cartData?.paymentMethod ?? 'paypal_smart_button';
+    const selectedPaymentMethod = cartData.paymentMethod || 'paypal_smart_button';
+    const selectedShippingRate = cartData.selectedShippingRate;
    
     const { 
         data: paymentMethodDatas,
@@ -131,73 +253,32 @@ export default function CheckoutWrapper({
     const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
     const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
 
+    
 
     // useForm() 只会在组件初始化时读取一次 defaultValues
     const addressForm = useForm<FullAddressFormData>({
-        mode: "onBlur",          // 或 "onChange"
+        mode: "onChange",          // 或 "onChange"
         reValidateMode: "onChange",
-        defaultValues: {
-            "shippingAddressId": "",
-            "shippingEmail": loginEmail,
-            "shippingFirstName": "",
-            "shippingLastName": "jack",
-            "shippingCompanyName": "",
-            "shippingAddress": "",
-            "shippingCountry": "US",
-            "shippingState": "",
-            "shippingCity": "",
-            "shippingPostcode": "",
-            "shippingPhoneNumber": "",
-            "billingAddressId": "",
-            "billingEmail": "",
-            "billingFirstName": "",
-            "billingLastName": "",
-            "billingCompanyName": "",
-            "billingAddress": "",
-            "billingCountry": "US",
-            "billingState": "",
-            "billingCity": "",
-            "billingPostcode": "",
-            "billingPhoneNumber": "",
-            "billingSameAsShipping": true
-        } 
+        defaultValues: addressFormDefaultValues 
     });
 
     const [addressModalOpen, setAddressModalOpen] = useState(false);
-    const [loadingAddress, setLoadingAddress] = useState(true); // 初始加载地址数据时的loading状态
 
-    const serverShippingAddress = useRef<ShipAddressFormData | null>(null);
-    const serverBillingAddress = useRef<BillAddressFormData | null>(null);
 
     const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
 
-    // 开发环境会执行两次 https://zh-hans.react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
-    useEffect(() => { 
-        getCheckoutAddress((shippingAddress,billingAddress)=> {
-            addressForm.reset({
-                ...shippingAddress,
-                ...billingAddress,
-            });
 
-            serverShippingAddress.current = shippingAddress;
-            serverBillingAddress.current = billingAddress;
-         
-            setLoadingAddress(false);
-        }, ()=> {
-            setLoadingAddress(false);
-        });
-    }, []);
 
 
     const openAddressModal = () => {
         setAddressModalOpen(true);
 
-        if(serverShippingAddress.current && serverBillingAddress.current) {
+        if(cartData.billingAddress && cartData.shippingAddress) {
             addressForm.reset({
-                ...serverShippingAddress.current,
-                ...serverBillingAddress.current,
+                ...getAddressFormDataFromCart(cartData)
             });
         }
+
         
     };
     const closeAddressModal = (e:boolean) => {
@@ -217,8 +298,8 @@ export default function CheckoutWrapper({
             const res = await saveCheckoutAddress(saveAddressParam);
             console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
             // 地址保存成功后重新获取运输方式和支付方式 
-            getShippingMethod();
-            getPaymentMethod();
+            await getShippingMethod();
+            await getPaymentMethod();
             
             // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
             const queryAddressRes = await getCheckoutAddress();
@@ -230,12 +311,17 @@ export default function CheckoutWrapper({
                     defaultValue: queryAddressRes.billingAddress.billingAddressId
                 });
 
-                serverShippingAddress.current = queryAddressRes.shippingAddress;
-                serverBillingAddress.current = queryAddressRes.billingAddress;
+      
                 // 同步地址到购物车详情
+                const newBillingAddress = billingAddressToCartAddress(queryAddressRes.billingAddress);
+                const newShippingAddress = shippingAddressToCartAddress(queryAddressRes.shippingAddress);
                 dispatch(updateCart({
-                    billingAddress: billingAddressToCartAddress(queryAddressRes.billingAddress),
-                    shippingAddress: shippingAddressToCartAddress(queryAddressRes.shippingAddress)
+                    billingAddress: newBillingAddress,
+                    shippingAddress: newShippingAddress
+                }));
+                setCartData(Object.assign({...cartData},{
+                    billingAddress: {...newBillingAddress},
+                    shippingAddress: {...newShippingAddress}
                 }));
 
             }
@@ -252,7 +338,7 @@ export default function CheckoutWrapper({
     };
 
 
-    const handlePaymentMethodChange =  async (method: string,title: string) => { 
+    const handlePaymentMethodChange =  async (method: string) => { 
         let shippingMethod = '-1';
         if(methodShiippingRef.current) {
             shippingMethod = methodShiippingRef.current.getSelectShipMethod();
@@ -268,24 +354,14 @@ export default function CheckoutWrapper({
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
                 dispatch(updateCart(newCartDetail));
+                setCartData({...newCartDetail});
             }
+            return true;
         } else {
             showToast(saveRes.msg, 'danger');
+            return false;
         }
 
-        /*
-        const res = await savePaymentMethod({ paymentMethod: method });
-        console.log('savePaymentMethod res ====== ',res);
-        if(!res.error) {
-            // 更新paymentMethod paymentMethodTitle
-            dispatch(updateCart({
-                paymentMethod: method,
-                paymentMethodTitle: title
-            }));
-        } else {
-            showToast(res.msg, 'danger');
-        }
-        */
     };
     const handleShippingMethodChange =  async (method: string) => { 
         overlayLoading.start();
@@ -299,28 +375,19 @@ export default function CheckoutWrapper({
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
                 dispatch(updateCart(newCartDetail));
+                setCartData({...newCartDetail});
             }
             // 保存成功之后需要重新获取支付方式
             getPaymentMethod();
         } else {
             showToast(saveRes.msg, 'danger');
         }
-        /*
-        const res = await saveShippingMethod({ shippingMethod: method });
-        if(!res.error) {
-            // 保存成功之后需要重新获取购物车详情和支付方式
-            getCartDetail();
-            getPaymentMethod();
-        } else {
-            showToast(res.msg, 'danger');
-        }*/
+
     };
 
-    // 下单
-    // 1. 校验地址 运输方式,支付方式;
-    // 2. 根据购物车详情判断是否需要传下单参数: 地址,运输方式,支付方式
-    const handlePlaceOrder = async () => { 
-        // 校验地址
+
+    // 校验地址 运输方式,支付方式;
+    const validateCheckout = async () => { 
         const addressValid = await addressForm.trigger();
 
         if(!addressValid) {
@@ -340,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;
@@ -354,8 +427,8 @@ export default function CheckoutWrapper({
         }
         
         //如果购物车详情没有保存过支付方式,则需要保存支付方式
-        if(cartData?.paymentMethod === null) {
-            const pmethod = methodPaymentRef?.current.getSelectPaymentMethod();
+        if(cartData.paymentMethod === null) {
+            const pmethod = methodPaymentRef.current.getSelectPaymentMethod();
             const saveRes = await saveCheckoutCart({ 
                 shippingMethod: '-1',
                 paymentMethod: pmethod,
@@ -365,12 +438,49 @@ export default function CheckoutWrapper({
                 if(saveRes.data) {
                     const newCartDetail = formatCartDetail(saveRes.data);
                     dispatch(updateCart(newCartDetail));
+                    setCartData({...newCartDetail});
                 }
             }
         }
 
         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};
+        }
+    };                    
+    
     
 
 
@@ -397,13 +507,16 @@ export default function CheckoutWrapper({
         <section className="w-full">
             <div>
             {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
-                    {loadingAddress ? 
+                    {/* {loadingAddress ? 
                         <AddressResultDisplayLoading /> 
                     :
-                        <AddressResultDisplay shippingAddress={cartData?.shippingAddress ?? null} 
+                        <AddressResultDisplay shippingAddress={cartData.shippingAddress || null} 
                             onAddressModalOpenClick={openAddressModal}
                         />
-                    }
+                    } */}
+                    <AddressResultDisplay shippingAddress={cartData.shippingAddress || null} 
+                        onAddressModalOpenClick={openAddressModal}
+                    />
                     <CommonModal
                         isOpen={addressModalOpen}
                         onClose={closeAddressModal}
@@ -451,6 +564,7 @@ export default function CheckoutWrapper({
                         ref={methodShiippingRef}  
                         shipMethods={shippingMethodDatas}
                         errorMsg={shippingMethodError}  
+                        selectedShippingRate={selectedShippingRate}
                         onShipMethodChange={handleShippingMethodChange}  
                     />
                 }
@@ -459,25 +573,29 @@ 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}
                     />
                 }
                 
             </div>
-            <div className="mt-3 box-border px-4">
+            <div className="mt-3 box-border px-4 pb-8">
                 <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
                 <div className="mt-6 w-full">
-                    <CheckoutPlaceOrder 
-                        paymentMethod={selectedPaymentMethod}
-                        clickPlaceOrder={handlePlaceOrder}
-                    />
+                    { paymentMethodLoading ?
+                        <LoadingCheckoutPlaceOrder />
+                    :
+                        <CheckoutPlaceOrder 
+                            paymentMethod={selectedPaymentMethod}
+                            clickPlaceOrder={handlePlaceOrder}
+                        />
+                    }
                 </div>
             </div>
 

+ 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('/');
         });  
     };
 

+ 192 - 46
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,37 +1,47 @@
 "use client";
 
 import Image from "next/image";
-import { use } from "react";
-import { useForm, get } from "react-hook-form";
-import { useApolloClient } from "@apollo/client/react";
-import {CheckoutPaymentMethodsData} from "@/types/checkout/type";
+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 {CREATE_PAYMENT_REPLAY} from "@/graphql";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import PaypalButton from "../PaymentButton/PaypalButton";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 
-interface PaymentReplayResult {
-    orderId: string;
-    webOrderId: string;
-    error: boolean;
-    msg: string;
-}
+/**@todo 优化目标:不使用 react-hook-form */
 export function PaymentMethodContinueTpPay({
     paymentMethodsDataPromise,
     defaultPaymentMethod,
     orderId,
 }: {
-    paymentMethodsDataPromise: Promise<GraphqlRequestResult<CheckoutPaymentMethodsData>>;
+    paymentMethodsDataPromise: Promise<GraphqlRequestResult<RepayOrderPaymentMethodsData>>;
     defaultPaymentMethod: {method: string; methodTitle: string;}
     orderId: string;
 }) {
+    const router = useRouter();
+    const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
-    const paymentMethods =  paymentMethodsData.data?.collectionPaymentMethods || [];
+    const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
+
+    const { paymentRepay } = usePlaceOrder(); 
 
-    const client = useApolloClient();
+    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",
@@ -43,50 +53,158 @@ 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");
-    console.log('selectedPaymentMethod -------- ',selectedPaymentMethod);
-    const handleCreateOrder = async () => {
+    // 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",
+    });
 
-        return client.mutate({
-            mutation: CREATE_PAYMENT_REPLAY,
-            variables: {
-                orderId: Number(orderId),
-                paymentMethod: selectedPaymentMethod
-            }
-        }).then((res) => {
-            const resData = res?.data?.createPaymentReplay?.paymentReplay ?? null; 
-            const result: PaymentReplayResult = {
+    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: resData?.gatewayOrderId ?? '',
-                webOrderId: resData?.orderId ?? ''
+                orderId: res.data.gatewayOrderId,
+                webOrderId: res.data.orderId
             };
-            return result;
-        }).catch((err) => {
-            const result: PaymentReplayResult = {
+        } else {
+            confirmDialog({
+                title: "Warning",
+                content: res.msg + " Create order failed. Please try again.",
+                noCancel: true,
+            }).then(() => {});
+            return {
                 error: true,
-                msg: err.message,
+                msg: '',
                 orderId: '',
                 webOrderId: ''
             };
-            // showToast(err.message,'danger');
-            return result;
-        });
+        }
+
+    };
 
+    const airwallexRepay = async () => {
+        setIsPay(true);
+        overlayLoading.start();
+        const res = await paymentRepay({
+            orderId: Number(orderId),
+            paymentMethod: selectedPaymentMethod,
+            paymentSuccessUrl: window.location.origin + '/paymentresult/result'
+        });
+        
+        if(!res.error && res.data !== null) {
+            // 创建订单成功了,跳转支付页面
+            window.location.href = res.data.gatewayOrderId;
+        } else {
+            //创建订单出错了
+            overlayLoading.stop();
+            confirmDialog({
+                title: "Warning",
+                content: res.msg + " Create order failed. Please try again.",
+                noCancel: true,
+            }).then(() => {});
+        }
+        setIsPay(false);
     };
-    const handleClickPay = async () => {
+
+
+    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"
@@ -109,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> 
@@ -122,14 +252,30 @@ export function PaymentMethodContinueTpPay({
                 <p className="text-ly-12 text-[#666666] leading-4.75">
                     By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.
                 </p>
+
                 <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}
+                        >
+                            Pay
+                        </button>
+                    }
 
                 </div>
             </div>

+ 0 - 1
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -1,6 +1,5 @@
 "use client";
 
-import { useState } from "react";
 import Image from "next/image";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";

+ 12 - 0
src/app/(checkout)/checkout/_components/LoadingCheckoutPlaceOrder.tsx

@@ -0,0 +1,12 @@
+export default function LoadingCheckoutPlaceOrder() { 
+    return (
+       
+        <div className="relative w-full animate-pulse">
+                        
+            <button className="flex items-center justify-center w-full h-12 bg-gray-300 rounded-3xl text-ly-16 font-bold"></button>
+                        
+        
+        </div>
+        
+    );
+}

+ 46 - 88
src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx

@@ -1,11 +1,9 @@
 "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 { getCookie } from "@/utils/cookie-tools";
-import { IS_GUEST } from "@/utils/constants";
 import {
   usePayPal,
   useEligibleMethods,
@@ -20,20 +18,20 @@ import {
 } from "@paypal/react-paypal-js/sdk-v6";
 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";
+
+
 export default function PaypalButton({
     createOrder,
-    onPaymentClick,
     isRePay
 }:{
   createOrder: () => Promise<{orderId: string;webOrderId: string;error: boolean;msg: string;}>;
-  onPaymentClick: () => Promise<boolean>;
   isRePay?: boolean;
 }) { 
-    const dispatch = useAppDispatch();
-    const isGuest = getCookie(IS_GUEST) !== "false";// 'true' 是游客
-    const {createPaymentCallback,cancelOrder} = usePlaceOrder();
+    const router = useRouter();
+
+    const {createPaymentCallback} = usePlaceOrder();
+
+    const [isPaying, setIsPaying] = useState(false);
 
     const gatewayOrderIdRef = useRef("");
     const webOrderIdRef = useRef("");
@@ -47,8 +45,8 @@ export default function PaypalButton({
         isLoading: isEligibilityLoading,
     } = useEligibleMethods({
         payload: {
-        currencyCode: "USD",
-        paymentFlow: "ONE_TIME_PAYMENT",
+            currencyCode: "USD", // @todo 需要根据用户选择的货币走
+            paymentFlow: "ONE_TIME_PAYMENT",
         },
     });
     const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
@@ -60,37 +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 {
-                //创建订单成功后清空redux 购物车
-                dispatch(clearCart());
+            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
@@ -106,6 +89,7 @@ export default function PaypalButton({
                 gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
                 status: 'success',
             });
+            overlayLoading.stop();
             if(!resCallback.error) {
                 // 支付成功,跳转到成功落地页 
                 confirmDialog({
@@ -113,16 +97,16 @@ export default function PaypalButton({
                     content: "Your payment was successful. Will redirect to success page.",
                     noCancel: true,
                 }).then(() => {
-                    redirect('/success?orderid=' + webOrderIdRef.current, RedirectType.replace);
+                    router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success');
                 });
             } else {
                 // callback 失败,提示错误
                 confirmDialog({
-                    title: "Payment failed",
+                    title: "Somthing Wrong",
                     content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderIdRef.current,
                     noCancel: true,
                 }).then(() => { 
-                    redirect('/', RedirectType.replace);
+                    router.replace('/');
                 });
                 
             }
@@ -140,31 +124,17 @@ export default function PaypalButton({
                 status: 'cancel',
             });
             if(!isRePay) {
-                if(isGuest) {
-                    
-                    await cancelOrder({
-                        orderId: Number(webOrderIdRef.current),
-                    });
-                    // deleteCookie(GUEST_CART_TOKEN);
-                    // deleteCookie(GUEST_CART_ID);
-                    confirmDialog({
-                        title: "Order Cancelled",
-                        content: "Your order has been cancelled. Will redirect to home page.",
-                        noCancel: true,
-                    }).then(() => { 
-                        redirect('/' , RedirectType.replace);
-                    });
+                overlayLoading.stop();
+                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({
+                    title: "Payment cancelled",
+                    content: "You canceled the payment.",
+                    noCancel: true,
+                }).then(() => {  });
             }
         },
 
@@ -181,37 +151,25 @@ export default function PaypalButton({
                 status: 'failure',
             });
             if(!isRePay) {
-                if(isGuest) {
-                    
-                    await cancelOrder({
-                        orderId: Number(webOrderIdRef.current),
-                    });
-                    // deleteCookie(GUEST_CART_TOKEN);
-                    // deleteCookie(GUEST_CART_ID);
-                    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);
-                    });
-                }
+                overlayLoading.stop();
+                router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure');
+
+            } else {
+                
+                // 登录用户 二次支付页面 
+                confirmDialog({
+                    title: "Payment Failed",
+                    content: data.message,
+                    noCancel: true,
+                }).then(() => {  });
+            
             }
         
         },
 
         onComplete: (data: OnCompleteData) => {
             overlayLoading.stop();
+            setIsPaying(false);
             console.log("Payment session completed");
             console.log("On Complete data:", data);
         },
@@ -265,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>
+    )
+}

Разница между файлами не показана из-за своего большого размера
+ 122 - 66
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx


+ 1 - 3
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -1,13 +1,11 @@
 "use client";
 
-import { useAppSelector,useAppDispatch } from "@/store/hooks";
+
 
 
 // 金额明细
 export default function PromotionsDetails() { 
 
-    const cartDetail = useAppSelector((state) => state.cartDetail);
-    const {cart, loading, error} = cartDetail;
 
     return (
         <div className="">

+ 18 - 24
src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx

@@ -1,13 +1,11 @@
 "use client";
 
-import clsx from "clsx";
-import { use, useEffect, useMemo, useState, useImperativeHandle, Ref  } from "react";
+
+import { useEffect, useImperativeHandle, Ref  } from "react";
 import { useForm, get } from "react-hook-form";
-import {useCheckoutShippingMethod} from  "@/utils/hooks/useCheckoutShippingMethod";
+
 import {CheckoutShippingRate} from "@/types/checkout/type";
-import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
-import { useAppSelector } from "@/store/hooks";
-import { useCustomToast } from "@/utils/hooks/useToast";
+
 
 
 /**
@@ -25,23 +23,19 @@ export function ShippingMethodCheckout({
     ref,
     errorMsg,
     shipMethods,
+    selectedShippingRate,
     onShipMethodChange
 }: {
     ref: Ref<RefShippingMethodsHandle>;
     errorMsg: string | null;
     shipMethods: CheckoutShippingRate[];
+    selectedShippingRate: string | null;
     onShipMethodChange: (value: string) => void;
 }) {
-    const { showToast } = useCustomToast();
 
 
-    const {cart,loading} = useAppSelector((state) => state.cartDetail);
-    const selectedShippingRate = cart?.selectedShippingRate ?? '';
-    console.log('selectedShippingRate ============================================ ', selectedShippingRate);
-
     const {
         register,
-        handleSubmit,
         reset,
         getValues,
         trigger,
@@ -69,29 +63,29 @@ export function ShippingMethodCheckout({
     },[]);
     // 根据cartDetail 的loading判断,是true 返回,是false时才往下执行
     useEffect(() => { 
-        if(shipMethods.length > 0 && !loading) {
+        if(shipMethods.length > 0 ) {
             let defaultValue = '';
-            let findItem = shipMethods.find((item) => item.method === selectedShippingRate);
-            if(findItem !== undefined) {
-                defaultValue = selectedShippingRate;
-            } else {
+            let findItem: CheckoutShippingRate | undefined;
+            if(selectedShippingRate) {
+                findItem = shipMethods.find((item) => item.method === selectedShippingRate);
+                if(findItem !== undefined) {
+                    defaultValue = selectedShippingRate;
+                }
+            }
+            if(!defaultValue) {
                 findItem = shipMethods.find((item) => item.price === 0) || shipMethods[0];
-                // shipMethods.forEach((method) => {
-                //     if(method.price === 0) {
-                //         defaultValue = method.method;
-                //     }
-                // });
                 defaultValue = findItem.method;
             }
 
-            console.log('ShippingMethodCheckout' + ' useEffect ++++++++++++++++++++++++++++ ', shipMethods,selectedShippingRate);
+
+            console.log('ShippingMethodCheckout' + ' useEffect ++++++++++++++++++++++++++++ ', defaultValue,selectedShippingRate);
             reset({ shippingMethod: defaultValue });
             if(defaultValue !== selectedShippingRate) {
                 onShipMethodChange(defaultValue);
             }
         }
         
-    }, [shipMethods,selectedShippingRate,loading]);
+    }, [shipMethods,selectedShippingRate]);
     
     return (
         <form name="shipping method" className="w-full mt-3">

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

@@ -4,12 +4,13 @@ import { redirect, RedirectType } from 'next/navigation';
 import { authOptions } from "@utils/auth";
 import { authorizationGraphqlQuery, fullCacheGraphqlRequest} from "@/lib/graphql-fetch";
 import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
-import { GET_ORDER_DETAILS, GET_CHECKOUT_PAYMENT_METHODS, GET_CURRENCIES } from "@/graphql";
+import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS, GET_CURRENCIES } from "@/graphql";
 // import { Country } from "@/types/checkout/type";
 import { OrderDetailsData } from "@/types/customer/type";
 import { CurrenciesData } from "@/types/currency/type";
-import { CheckoutPaymentMethodsData } from "@/types/checkout/type";
+import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
+import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
 
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
@@ -45,18 +46,18 @@ 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);
     }
 
     const currencyRes = await fullCacheGraphqlRequest<CurrenciesData>(GET_CURRENCIES);
-    console.log("currencyRes ==========",currencyRes); 
     const currencyList = currencyRes.data?.currencies?.edges.map((item) => item.node) ?? [];
     // 获取支付方式列表
-    const paymentMethodsResponse = authorizationGraphqlQuery<CheckoutPaymentMethodsData>(GET_CHECKOUT_PAYMENT_METHODS);
+    const paymentMethodsResponse = authorizationGraphqlQuery<RepayOrderPaymentMethodsData>(GET_REPAYORDER_PAYMENT_METHODS,{
+        orderId: Number(orderid)
+    });
     
     const currencySymbol = currencyList.find(item => item.code === orderDetailData.orderCurrencyCode)?.symbol ?? '';
     
@@ -76,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}

+ 6 - 5
src/app/(checkout)/checkout/page.tsx

@@ -21,20 +21,21 @@ export const dynamic = "force-dynamic";
 
 export default async function CheckoutPage() {
 
-    const session = await getServerSession(authOptions);
+    const session = await getServerSession(authOptions); // 游客是null
 
     const {data: countriesData} = await fullCacheGraphqlRequest<{
         countries: Country[] 
     }>(GET_COUNTRIES);
 
-    const cartDetails = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
-
-    if(cartDetails.data === null || cartDetails.data.createReadCart === null) {
+    const cartDetailsRes = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
+    if(cartDetailsRes.data === null || cartDetailsRes.data.createReadCart === null) {
         redirect('/', RedirectType.replace);
     }
+    const cartDetails = cartDetailsRes.data.createReadCart.readCart;
     return (
         <>
-            <CheckoutWrapper 
+            <CheckoutWrapper key={cartDetails.id}
+                cartDetailData={cartDetails}
                 loginEmail={session?.user?.email || ""}
                 countries={countriesData?.countries || []}
             />

+ 101 - 0
src/app/(public)/paymentresult/_components/CancelOrFailure.tsx

@@ -0,0 +1,101 @@
+"use client";
+
+import {use, useEffect, useRef} from "react";
+import Link from "next/link";
+import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
+import { OrderDetailsData } from "@/types/customer/type";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import Faqs from "./Faqs";
+import { useAppDispatch } from "@/store/hooks";
+import { fetchCartDetail } from "@/store/slices/cart-slice";
+export default function CancelOrFailure({
+    orderDetailPromise,
+    paymentStatus,
+    isGuest
+}: {
+    orderDetailPromise: Promise<GraphqlRequestResult<OrderDetailsData>>,
+    paymentStatus: string,
+    isGuest: boolean
+}) { 
+    const dispatch = useAppDispatch();
+    const promiseRes = use(orderDetailPromise);
+    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
+    const {cancelOrder} = usePlaceOrder();
+    const orderId = orderDetailRes?.incrementId;
+    const orderStatus = orderDetailRes?.status;
+    const cancelOrderRef = useRef(false);
+    useEffect(() => {
+        console.log('isGuest ---- ',isGuest);
+        if(isGuest && !cancelOrderRef.current) {
+            
+            // 如果是游客,取消订单
+            console.log('游客,取消订单 ',orderId,orderStatus,cancelOrderRef.current);
+            if(orderId && orderStatus !=='canceled' && !cancelOrderRef.current) {
+                console.log('cancelOrder run ');
+                cancelOrder({
+                    orderId: Number(orderId),
+                }).then(() => {
+                    // 重新获取购物车
+                    dispatch(fetchCartDetail());
+                }).catch(() => {});
+            }
+            cancelOrderRef.current = true;
+        }
+    }, [orderId,orderStatus]);
+    return (
+        <div className="box-border px-4 w-full">
+            {paymentStatus === 'failure' &&
+                <>
+                    <div className="w-12 h-12 mx-auto mt-9">
+                        <svg className="failure block w-full h-full" width="200" height="200" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27635"><path d="M736 332.8L556.8 512l179.2 179.2-44.8 44.8L512 556.8l-179.2 179.2-44.8-44.8L467.2 512 288 332.8l44.8-44.8L512 467.2l179.2-179.2 44.8 44.8z" p-id="27636"></path><path d="M512 0a512 512 0 1 1 0 1024A512 512 0 0 1 512 0z m0 64a448 448 0 1 0 0 896A448 448 0 0 0 512 64z" p-id="27637"></path></svg>
+                    </div>
+                    <p className="text-ly-16 text-center mt-6">
+                        {isGuest ?
+                            "payment Failed! Your order has been cancelled."
+                        :
+                            "Payment Failed! You can try again."
+                        }
+                        
+                    </p>
+                </>
+            }
+            {paymentStatus === 'cancel' &&
+                <>
+                    <div className="w-12 h-12 mx-auto mt-9">
+                        <svg className="warning block w-full h-full" width="200" height="200" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24135"><path d="M874.6496 512a362.6496 362.6496 0 1 0-725.3504 0 362.6496 362.6496 0 0 0 725.3504 0z m64 0a426.6496 426.6496 0 1 1-853.3504 0 426.6496 426.6496 0 0 1 853.3504 0z m-458.7008 85.3504V298.7008h64v298.6496h-64z m0 128v-64h64v64h-64z" p-id="24136"></path></svg>
+                    </div>
+                    <p className="text-ly-16 text-center mt-6">
+                        {isGuest ?
+                            "payment cancelled! Your order has been cancelled."
+                        :
+                            "Payment cancelled! You can try again."
+                        }
+                        
+                    </p>
+                </>
+            }
+
+            <p className="text-ly-16 mt-4 flex justify-center">
+                <span className="px-3.75 leading-ly-28 bg-[#f0fffa]">Order #{orderId}</span>
+            </p>
+            {/* <p className="text-ly-12 text-center mt-4 leading-ly-22">
+                Hello, {orderDetailRes?.customerFirstName} <br/>
+                Thank you so much for your order !<br/>
+                We’ll get started on your order very soon!<br/>
+                You’ll receive an order confirmation email with details of your order in an hour, please be sure to check your email and confirm your order, that is very important for us to process your order.
+            </p> */}
+            {isGuest ?
+                <Link href="/" className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                    Continue Shopping
+                </Link>
+            :
+                <Link href={`/checkout/continuetopay?orderid=${orderId}`} className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                    Continue To Pay
+                </Link>
+            }
+            <div className="box-border w-full mt-6">
+                <Faqs />
+            </div>
+        </div>
+    );
+}

Разница между файлами не показана из-за своего большого размера
+ 20 - 0
src/app/(public)/paymentresult/_components/Faqs.tsx


Разница между файлами не показана из-за своего большого размера
+ 17 - 17
src/app/(public)/success/_components/OrderDetailWrapper.tsx


+ 34 - 0
src/app/(public)/paymentresult/_components/SkeletonCancelOrFailure.tsx

@@ -0,0 +1,34 @@
+
+export default function SkeletonCancelOrFailure() { 
+    return (
+        <div className="w-full animate-pulse px-0">
+
+            <div className="w-full">
+                <div className="box-border px-4 w-full">
+                    <div className="w-12 h-12 mx-auto mt-9 bg-gray-300"></div>
+                    <p className="text-ly-16 text-center mt-6">
+                        <span className="text-gray-300 bg-gray-300">Your Order Has Been Received</span>
+                    </p>
+                    <p className="text-ly-16 mt-4 flex justify-center">
+                        <span className="px-3.75 leading-ly-28 text-gray-300 bg-gray-300">Order #600000000</span>
+                    </p>
+              
+                    <div className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                        <span className="text-gray-300 bg-gray-300">Continue Shopping</span>
+                    </div>
+                </div>
+                <div className="box-border px-4 w-full mt-6">
+                    <div className="relative w-full bg-ly-lightgray box-border p-3">
+                        <button className="absolute top-3 right-3 w-6 h-6 bg-gray-300"></button>
+                        <p className="text-ly-13"><span className="text-gray-300 bg-gray-300">Faqs</span></p>
+                        <p className="text-ly-12 text-[#a6a6a6] mt-2">
+                            <span className="text-gray-300 bg-gray-300">Pls contact customer service if need any help</span>
+                        </p>
+                    </div>
+                </div>
+
+
+            </div>
+        </div>
+    );
+}

src/app/(public)/success/_components/SuccessPageSkeleton.tsx → src/app/(public)/paymentresult/_components/SuccessPageSkeleton.tsx


+ 60 - 0
src/app/(public)/paymentresult/result/page.tsx

@@ -0,0 +1,60 @@
+import {Suspense} from "react";
+import { getServerSession } from "next-auth";
+import { cookies } from 'next/headers';
+import { redirect, RedirectType } from 'next/navigation'
+import { authOptions } from "@utils/auth";
+import {authorizationGraphqlQuery} from "@/lib/graphql-fetch";
+import {GET_ORDER_DETAILS} from "@/graphql";
+import { OrderDetailsData } from "@/types/customer/type";
+import SuccessPageSkeleton from "../_components/SuccessPageSkeleton";
+import SkeletonCancelOrFailure from "../_components/SkeletonCancelOrFailure";
+import CancelOrFailure from "../_components/CancelOrFailure";
+import OrderDetailWrapper from "../_components/OrderDetailWrapper";
+import { GUEST_CART_TOKEN } from "@/utils/constants";
+
+// 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
+export default async function SuccessPage({searchParams}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+})  {
+    const cookieStore = await cookies();
+    // 登录用户cookie里没有GUEST_CART_ID
+    if(!cookieStore.get(GUEST_CART_TOKEN)) {
+        redirect('/', RedirectType.replace);
+    }
+    const session = await getServerSession(authOptions);// 游客是null
+    const isGuest = session === null;
+    
+    const query = await searchParams; // awx_return_result
+    const orderid = query?.orderId;
+    const paymentResult = query?.awx_return_result ?? query?.return_result; // 支付状态
+
+
+    if (!orderid) {
+        redirect("/", RedirectType.replace);
+    }
+
+
+    const orderDetailPromise = authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
+        id: orderid,// `/api/shop/customer-orders/${orderid}`
+    });
+
+    return (
+        
+        <div className="w-full">
+            {paymentResult === 'success' && 
+                <Suspense fallback={<SuccessPageSkeleton />}>
+                    <OrderDetailWrapper orderDetailPromise={orderDetailPromise} 
+                        isGuest={isGuest}
+                    />
+                </Suspense>
+
+            }
+            {(paymentResult === 'cancel' || paymentResult === 'failure') &&
+                <Suspense fallback={<SkeletonCancelOrFailure/>}>
+                    <CancelOrFailure isGuest={isGuest} orderDetailPromise={orderDetailPromise} paymentStatus={paymentResult} />
+                </Suspense>
+            }
+            
+        </div>
+    );
+}

+ 0 - 33
src/app/(public)/success/page.tsx

@@ -1,33 +0,0 @@
-import {Suspense} from "react";
-import { cookies } from 'next/headers';
-import { redirect, RedirectType } from 'next/navigation'
-import {authorizationGraphqlQuery} from "@/lib/graphql-fetch";
-import {GET_ORDER_DETAILS} from "@/graphql";
-import { OrderDetailsData } from "@/types/customer/type";
-import SuccessPageSkeleton from "./_components/SuccessPageSkeleton";
-import OrderDetailWrapper from "./_components/OrderDetailWrapper";
-import { GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
-
-// 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
-export default async function SuccessPage({searchParams}: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
-})  {
-    const cookieStore = await cookies();
-    // 登录用户cookie里没有GUEST_CART_ID
-    if(!cookieStore.get(GUEST_CART_TOKEN)) {
-        redirect('/', RedirectType.replace);
-    }
-    const query = await searchParams;
-    const orderid = query?.orderid;
-    const orderDetailPromise = authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
-        id: orderid,// `/api/shop/customer-orders/${orderid}`
-    });
-    return (
-        
-        <div className="w-full">
-            <Suspense fallback={<SuccessPageSkeleton />}>
-                <OrderDetailWrapper orderDetailPromise={orderDetailPromise} />
-            </Suspense>
-        </div>
-    );
-}

+ 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;
+}

+ 1 - 1
src/app/api/graphql/route.ts

@@ -61,7 +61,7 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
     const { operationName, variables } = body;
     const guestToken = getAuthToken(req);
     const query = ALLOWED_OPERATIONS[operationName];
-    let finalVariables = variables;
+    const finalVariables = variables;
 
     /*if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
         finalVariables = { ...variables };

+ 0 - 1
src/components/cart/CartModal.tsx

@@ -4,7 +4,6 @@ import { useDisclosure } from "@heroui/use-disclosure";
 import { AnimatePresence, motion } from "framer-motion";
 import { ShoppingCartIcon } from "@heroicons/react/24/outline";
 import { DEFAULT_OPTION } from "@/utils/constants";
-import { useAppSelector } from "@/store/hooks";
 import OpenCart from "./OpenCart";
 import { Price } from "../theme/ui/Price";
 import CloseCart from "../common/icons/cart/CloseCart";

+ 1 - 1
src/components/common/ConfirmModal.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState} from "react";
+
 import { AnimatePresence , motion } from "framer-motion";
 
 export interface ConfirmModalProps { 

+ 10 - 0
src/components/common/portal/Portal.tsx

@@ -0,0 +1,10 @@
+'use client';
+
+
+import dynamic from 'next/dynamic'
+
+
+const Portal = dynamic(() => import("./PortalBox"), {
+  ssr: false,
+});
+export default Portal;

+ 6 - 10
src/components/Portal.tsx

@@ -27,23 +27,18 @@ export default function Portal({ children }: { children: React.ReactNode }) {
 但是在react v19 中 认为mounted是一个「派生状态(derived state)」,
 完全可以在 render 时计算出来(使用document 来判断),不需要专门声明一个state在副作用里更改状态(引起二次渲染),所以eslint报错。
 */
-import {useRef, useEffect} from 'react';
+
 import { createPortal } from 'react-dom';
 
-export default function Portal({
+export default function PortalBox({
   children,
 }: {
   children: React.ReactNode;
 }) {
-  const mountedRef = useRef(false);
-
-  useEffect(() => {
-    mountedRef.current = true;
-    // 无需任何清理 (cleanup) 函数
-  }, []); // 依赖数组完全为空
+  
 
   // 服务端和客户端首次渲染时都返回 null
-  if (!mountedRef.current) {
+  if (typeof window === "undefined") {
     return null;
   }
 
@@ -54,4 +49,5 @@ export default function Portal({
   }
 
   return createPortal(children, portalRoot);
-}
+}
+

+ 4 - 2
src/components/theme/ui/CommonModal.tsx

@@ -2,7 +2,7 @@
 
 import clsx from "clsx";
 import { useState, useLayoutEffect, useRef } from "react";
-import Portal from "@/components/Portal";
+import Portal from "@/components/common/portal/Portal";
 
 /**
  * 为了弹窗里的表单校验开发整个组件:
@@ -51,11 +51,13 @@ export default function CommonModal({
         } else {
             document.documentElement.style.overflow = "";
         }
-
+        // intent: 为了实现关闭时也有动画效果,告诉eslint忽略这个错误
         if(isOpen) {
+            // eslint-disable-next-line react-hooks/set-state-in-effect
             setRootVisible('show');
         } else {
             if(cssInit.current) {
+                // eslint-disable-next-line react-hooks/set-state-in-effect
                 setRootVisible('outing');
             }
             

+ 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;
 

+ 6 - 0
src/graphql/checkout/mutations/CreateCheckoutOrder.ts

@@ -5,11 +5,17 @@ export const CREATE_PAYMENT_INITIATE: TypedDocumentNode<CreatePaymentInitiateDat
   mutation CreatePaymentInitiate(
       $expressCheckout: Boolean
       $paymentMethod: String
+      $paymentSuccessUrl: String
+      $paymentFailureUrl: String
+      $paymentCancelUrl: String
   ) {
       createPaymentInitiate(
         input: {
           expressCheckout: $expressCheckout
           paymentMethod: $paymentMethod
+          paymentSuccessUrl: $paymentSuccessUrl
+          paymentFailureUrl: $paymentFailureUrl
+          paymentCancelUrl: $paymentCancelUrl
         }
       ) {
           paymentInitiate {

+ 6 - 0
src/graphql/checkout/mutations/CreatePaymentReplay.ts

@@ -5,11 +5,17 @@ export const CREATE_PAYMENT_REPLAY: TypedDocumentNode<CreatePaymentReplayData> =
   mutation CreatePaymentReplay(
       $orderId: Int
       $paymentMethod: String
+      $paymentSuccessUrl: String
+      $paymentFailureUrl: String
+      $paymentCancelUrl: String
   ) {
       createPaymentReplay(
         input: {
           orderId: $orderId
           paymentMethod: $paymentMethod
+          paymentSuccessUrl: $paymentSuccessUrl
+          paymentFailureUrl: $paymentFailureUrl
+          paymentCancelUrl: $paymentCancelUrl
         }
       ) {
           paymentReplay{

+ 16 - 0
src/graphql/checkout/queries/GetRepayOrderPaymentMethods.ts

@@ -0,0 +1,16 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
+
+export const GET_REPAYORDER_PAYMENT_METHODS: TypedDocumentNode<RepayOrderPaymentMethodsData> = gql`
+  query RepayOrderPaymentMethods($orderId: Int!) {
+    collectionRepayOrderPaymentMethods(orderId: $orderId) {
+      id
+      _id
+      method
+      title
+      description
+      icon
+      isAllowed
+    }
+  }
+`;

+ 2 - 1
src/graphql/checkout/queries/index.ts

@@ -2,4 +2,5 @@ export {GET_CHECKOUT_PAYMENT_METHODS} from "./GetCheckoutPaymentMethods" ;
 export {GET_CHECKOUT_SHIPPING_RATES} from "./GetCheckoutShippingRates" ;
 export {GET_CHECKOUT_ADDRESSES} from "./GetCheckoutAddress";
 export {GET_COUNTRIES} from "./GetCountries";
-export {GET_COUNTRY_STATES} from "./GetStates";
+export {GET_COUNTRY_STATES} from "./GetStates";
+export {GET_REPAYORDER_PAYMENT_METHODS} from "./GetRepayOrderPaymentMethods";

+ 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 - 1
src/lib/ApolloErrorHandler.ts

@@ -9,7 +9,7 @@ import {
 
 // about apollo client error handle https://www.apollographql.com/docs/react/data/error-handling
 // Comprehensive error handling example.
-function handleError(error: unknown) {
+export function handleError(error: unknown) {
   if (CombinedGraphQLErrors.is(error)) {
     // Handle GraphQL errors
   } else if (CombinedProtocolErrors.is(error)) {

+ 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;

+ 12 - 8
src/providers/ReduxProvider.tsx

@@ -4,9 +4,9 @@ import { Provider } from "react-redux";
 
 // import { store } from "@/store/store";
 
-import { useRef } from 'react'
+import { useState } from 'react'
 
-import { makeStore, AppStore,PreloadedStateType } from '@/store/store'
+import { makeStore, PreloadedStateType } from '@/store/store'
 
 export const ReduxProvider = ({ 
   children,
@@ -16,12 +16,16 @@ export const ReduxProvider = ({
   preloadedState: PreloadedStateType
 }) => {
   // return <Provider store={store}>{children}</Provider>;
+  
+  // const storeRef = useRef<AppStore>(undefined)
+  // if (!storeRef.current) {
+  //   // Create the store instance the first time this renders
+  //   storeRef.current = makeStore(preloadedState)
+  // }
 
-  const storeRef = useRef<AppStore>(undefined)
-  if (!storeRef.current) {
-    // Create the store instance the first time this renders
-    storeRef.current = makeStore(preloadedState)
-  }
+  // return <Provider store={storeRef.current}>{children}</Provider>
 
-  return <Provider store={storeRef.current}>{children}</Provider>
+  const [store] = useState(() => makeStore(preloadedState));
+
+  return <Provider store={store}>{children}</Provider>
 }

+ 52 - 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";
 
@@ -127,6 +128,9 @@ export interface CheckoutPaymentMethodsOperation {
   variables: Record<string, unknown>;
   data: CheckoutPaymentMethodsData;
 }
+export interface RepayOrderPaymentMethodsData {
+  collectionRepayOrderPaymentMethods: CheckoutPaymentMethod[];
+}
 
 // checkout save payment
 
@@ -235,6 +239,9 @@ export interface CreatePaymentInitiateData {
 export interface CreatePaymentInitiateVariables {
     expressCheckout?: boolean;
     paymentMethod?: string;
+    paymentSuccessUrl?: string;
+    paymentFailureUrl?: string;
+    paymentCancelUrl?: string;
 }
 
 // create payment callback
@@ -454,4 +461,49 @@ export interface CreatePaymentReplayPayload {
 }
 export interface CreatePaymentReplayData {
   createPaymentReplay: CreatePaymentReplayPayload;
+}
+export interface CreatePaymentReplayVariables {
+  orderId: number;
+  paymentMethod: string;
+  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;
 }

+ 0 - 163
src/utils/hooks/useCheckout.ts

@@ -1,163 +0,0 @@
-import { useMutation } from "@apollo/client/react";
-import { useRouter } from "next/navigation";
-import { useCustomToast } from "./useToast";
-import { useDispatch } from "react-redux";
-import {
-  clearCart,
-} from "@/store/slices/cart-slice";
-import { getCartToken } from "@utils/getCartToken";
-
-import { getCookie, setCookie } from "@/utils/cookie-tools";
-import { useGuestCartToken } from "./useGuestCartToken";
-import { ORDER_ID, IS_GUEST } from "@utils/constants";
-import { useCartDetail } from "./useCartDetail";
-import {
-  CREATE_CHECKOUT_ADDRESS,
-  CREATE_CHECKOUT_ORDER,
-  CREATE_CHECKOUT_PAYMENT_METHODS,
-  CREATE_CHECKOUT_SHIPPING_METHODS,
-  GET_CHECKOUT_ADDRESSES,
-} from "@/graphql";
-import { CreateCheckoutShippingMethodData, CreateCheckoutPaymentMethodResponse, CreateCheckoutOrderData } from "@/types/checkout/type";
-
-export const useCheckout = () => {
-  const router = useRouter();
-  const { resetGuestToken } = useGuestCartToken();
-  const { showToast } = useCustomToast();
-  const dispatch = useDispatch();
-  const { getCartDetail } = useCartDetail();
-
-  const handleMutationError = (error: any) => {
-    showToast(error?.message || "An error occurred", "danger");
-  };
-
-  const [saveAddressToCheckout, { loading: isLoadingToSave }] = useMutation(
-    CREATE_CHECKOUT_ADDRESS,
-    {
-      refetchQueries: [{ query: GET_CHECKOUT_ADDRESSES }],
-      awaitRefetchQueries: true,
-      onCompleted: () => {
-        showToast("Address saved successfully", "success");
-        router.replace("/checkout?step=shipping");
-      },
-      onError: handleMutationError,
-    },
-  );
-
-  const saveCheckoutAddress = async (input: any) => {
-    const token = getCartToken();
-    await saveAddressToCheckout({
-      variables: {
-        token: token || "",
-        ...input,
-      },
-    });
-  };
-
-  const [saveShipping, { loading: isSaving }] = useMutation<CreateCheckoutShippingMethodData>(
-    CREATE_CHECKOUT_SHIPPING_METHODS,
-    {
-      onCompleted: (response) => {
-        const responseData =
-          response?.createCheckoutShippingMethod?.checkoutShippingMethod;
-        if (responseData?.success) {
-          getCartDetail();
-          showToast("Shipping method saved successfully", "success");
-          router.replace("/checkout?step=payment");
-        } else {
-          showToast(
-            responseData?.message || "Failed to save shipping method",
-            "warning",
-          );
-        }
-      },
-      onError: handleMutationError,
-    },
-  );
-
-  const saveCheckoutShipping = async (shippingMethod: string) => {
-    const token = getCartToken();
-    await saveShipping({
-      variables: {
-        token: token || "",
-        shippingMethod,
-      },
-    });
-  };
-
-  const [savePayment, { loading: isPaymentLoading }] = useMutation<CreateCheckoutPaymentMethodResponse>(
-    CREATE_CHECKOUT_PAYMENT_METHODS,
-    {
-      onCompleted: (response) => {
-        const responseData =
-          response?.createCheckoutPaymentMethod?.checkoutPaymentMethod;
-        if (responseData?.success) {
-          showToast("Payment method saved successfully", "success");
-          router.replace("/checkout?step=review");
-        } else {
-          showToast(
-            responseData?.message || "Failed to save payment method",
-            "warning",
-          );
-        }
-      },
-      onError: handleMutationError,
-    },
-  );
-
-  const saveCheckoutPayment = async (paymentMethod: string) => {
-    const token = getCartToken();
-    await savePayment({
-      variables: {
-        token: token || "",
-        paymentMethod,
-      },
-    });
-  };
-
-  const [placeOrder, { loading: isPlaceOrder }] = useMutation<CreateCheckoutOrderData>(
-    CREATE_CHECKOUT_ORDER,
-    {
-      onCompleted: (response) => {
-        const responseData = response?.createCheckoutOrder?.checkoutOrder;
-        if (responseData) {
-          showToast("Order placed successfully!", "success");
-          setCookie(ORDER_ID, responseData?.orderId);
-          dispatch(clearCart());
-          router.replace("/success");
-        } else {
-          showToast(
-            "Failed to place order",
-            "warning",
-          );
-        }
-      },
-      onError: handleMutationError,
-    },
-  );
-
-  const savePlaceOrder = async () => {
-    const token = getCartToken();
-    await placeOrder({
-      variables: {
-        token: token || "",
-      },
-    });
-
-    const isGuest = getCookie(IS_GUEST);
-    if (isGuest === "true") {
-      await resetGuestToken();
-    }
-  };
-
-  return {
-    isLoadingToSave,
-    saveCheckoutAddress,
-    isSaving,
-    saveCheckoutShipping,
-    isPaymentLoading,
-    saveCheckoutPayment,
-    isPlaceOrder,
-    savePlaceOrder,
-  };
-};

+ 1 - 1
src/utils/hooks/useCheckoutAddress.ts

@@ -3,7 +3,7 @@
 import { GET_CHECKOUT_ADDRESSES, CREATE_CHECKOUT_ADDRESS } from "@/graphql";
 import {useApolloClient} from "@apollo/client/react";
 import { 
-    CheckoutAddressNode, 
+    CheckoutAddressNode,
     ShipAddressFormData,
     BillAddressFormData,
     CreateCheckoutAddressVariables

+ 3 - 6
src/utils/hooks/useCheckoutPaymentMethod.ts

@@ -17,10 +17,7 @@ export function useCheckoutPaymentMethod() {
     const [error, setError] = useState<string|null>(null);
     const isIntRef = useRef(true);
 
-    const getPaymentMethod = (
-        resolveCallback: (res:PaymentMethodsResult) => void = () => {}, 
-        rejectCallback: (res: PaymentMethodsResult) => void = ()=> {}
-    ) => {
+    const getPaymentMethod = () => {
         if(!isIntRef.current) {
             setLoading(true);
         }
@@ -48,7 +45,7 @@ export function useCheckoutPaymentMethod() {
             setLoading(false);
             
             console.log('GET_CHECKOUT_paymentmethod res ---- ',paymentMethods);
-            resolveCallback(resData);
+            
             return resData;
         
         }).catch((err) => {
@@ -63,7 +60,7 @@ export function useCheckoutPaymentMethod() {
             };
             setError(resData.msg);
             setLoading(false);
-            rejectCallback(resData);
+            
             return resData;
         });
     };

+ 3 - 6
src/utils/hooks/useCheckoutShippingMethod.ts

@@ -17,10 +17,7 @@ export function useCheckoutShippingMethod() {
     const [error, setError] = useState<string|null>(null);
     const isIntRef = useRef(true);
 
-    const getShippingMethod = (
-        resolveCallback: (res:ShippingMethodsResult) => void = () => {}, 
-        rejectCallback: (res: ShippingMethodsResult) => void = ()=> {}
-    ) => {
+    const getShippingMethod = () => {
         if(!isIntRef.current) {
             setLoading(true);
         }
@@ -46,7 +43,7 @@ export function useCheckoutShippingMethod() {
             setData(resData.data);
             setLoading(false);
             console.log('GET_CHECKOUT_shippingmethod res ---- ',shippingMethods);
-            resolveCallback(resData);
+            // resolveCallback(resData);
             return resData;
         
         }).catch((err) => {
@@ -61,7 +58,7 @@ export function useCheckoutShippingMethod() {
             };
             setError(resData.msg);
             setLoading(false);
-            rejectCallback(resData);
+            // rejectCallback(resData);
             return resData;
         });
     };

+ 33 - 2
src/utils/hooks/usePlaceOrder.ts

@@ -3,6 +3,7 @@ import {
     CREATE_PAYMENT_INITIATE,
     CREATE_PAYMENT_CALLBACK,
     CREATE_CANCEL_ORDER,
+    CREATE_PAYMENT_REPLAY
 } from "@/graphql";
 import {
     CreatePaymentInitiateVariables, 
@@ -11,9 +12,11 @@ import {
     CreatePaymentInitiate,
     CreatePaymentCallback,
     CreateCancelOrder,
+    CreatePaymentReplayVariables,
+    CreatePaymentReplay
 } from "@/types/checkout/type";
 
-interface CreateOrderResult {
+export interface CreateOrderResult {
     data: CreatePaymentInitiate | null;
     msg: string;
     error: boolean;
@@ -28,6 +31,11 @@ interface CreateCancelOrderResult {
     msg: string;
     error: boolean;
 }
+interface PaymentReplayResult {
+    error: boolean;
+    data: CreatePaymentReplay | null;
+    msg: string;
+}
 export const usePlaceOrder = () => {
     const client = useApolloClient();
 
@@ -100,10 +108,33 @@ export const usePlaceOrder = () => {
         });
     }
 
+    const paymentRepay = (params: CreatePaymentReplayVariables) => { 
+
+        return client.mutate({
+            mutation: CREATE_PAYMENT_REPLAY,
+            variables: params
+        }).then((res) => {
+            const resData = res?.data?.createPaymentReplay?.paymentReplay ?? null; 
+            const result: PaymentReplayResult = {
+                error: false,
+                msg: '',
+                data: resData
+            };
+            return result;
+        }).catch((err) => {
+            const result: PaymentReplayResult = {
+                error: true,
+                msg: err.message,
+                data: null,
+            };
+            return result;
+        });
+    };
 
   return {
     createOrder,
     createPaymentCallback,
-    cancelOrder
+    cancelOrder,
+    paymentRepay
   };
 };