16 次代碼提交 7e4fafd6c6 ... ba6f73b0cf

作者 SHA1 備註 提交日期
  zhangzf ba6f73b0cf 代码调整 1 周之前
  zhangzf 723a962484 Merge branch 'zzf-account' 1 周之前
  fogwind 9cc21004e0 airwallex 卡支付 初始化逻辑优化 1 周之前
  fogwind 4f5a980420 airwallex 卡支付输入框样式 1 周之前
  fogwind 89a403f5b5 合并dev-checkoutproduct 1 周之前
  fogwind 67469bd02c eslint 检查修改 1 周之前
  fogwind ddae37d2b8 ToastProvider优化(当组件中的useEffect依赖showToast时,其他组件中调用showToast会触发前者的useEffect) 1 周之前
  fogwind 91029c2e36 结账页产品展示,修改产品数量,删除产品;优惠券使用、取消 1 周之前
  fogwind 4c62c77815 结账页登录用户选择地址功能; 1 周之前
  fogwind f489088636 移除package.json中的pnpm配置 1 周之前
  fogwind aa9911f3cf 合并dev-vipplus 2 周之前
  fogwind 206d63f61b 结账页vip plus 功能 --- v1 2 周之前
  fogwind 09f2213fb0 useCartDetail 优化;airwallex卡支付初始化优化 2 周之前
  fogwind 9f95215d30 添加通用登录弹窗;CommonModal支持创建、销毁模式 2 周之前
  fogwind ab1bb2e1a0 游客和登录用户 token管理修改;删除redux中的user 2 周之前
  fogwind 4c444a7296 订单详情使用订单里的货币码(因为订单详情里的金额是下单时候用户选的货币对应的金额,不是当前货币对应的金额) 3 周之前
共有 81 個文件被更改,包括 3628 次插入1267 次删除
  1. 14 2
      README.md
  2. 0 6
      package.json
  3. 6 0
      pnpm-workspace.yaml
  4. 54 0
      src/actions/mergeCartAction.ts
  5. 0 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplay.tsx
  6. 0 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplayLoading.tsx
  7. 153 23
      src/app/(checkout)/checkout/_components/BillingAddressCheckout.tsx
  8. 448 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx
  9. 173 11
      src/app/(checkout)/checkout/_components/ShippingAddressCheckout.tsx
  10. 110 0
      src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx
  11. 1 1
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  12. 197 0
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  13. 93 385
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  14. 45 20
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  15. 21 42
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  16. 4 0
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  17. 1 4
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx
  18. 24 12
      src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx
  19. 6 62
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  20. 30 8
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  21. 223 0
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  22. 1 1
      src/app/(checkout)/checkout/page.tsx
  23. 1 1
      src/app/(public)/_components/HomeBestSellers.tsx
  24. 37 17
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  25. 0 3
      src/app/api/gift/add/route.ts
  26. 2 5
      src/app/api/gift/lists/route.ts
  27. 0 3
      src/app/api/gift/my-gift-cards/route.ts
  28. 6 9
      src/app/api/graphql/route.ts
  29. 5 0
      src/app/globals.css
  30. 4 3
      src/components/cart/CartModal.tsx
  31. 482 0
      src/components/common/LoginModal/LoginModal.tsx
  32. 4 5
      src/components/common/button/ReviewButton.tsx
  33. 14 13
      src/components/customer/LoginForm.tsx
  34. 9 10
      src/components/customer/credentials/CredentialModal.tsx
  35. 18 9
      src/components/home/HomeImageBanner.tsx
  36. 59 18
      src/components/theme/ui/CommonModal.tsx
  37. 4 2
      src/components/theme/ui/InputText.tsx
  38. 102 0
      src/components/theme/ui/LyDropDown.tsx
  39. 74 0
      src/components/theme/ui/PasswordInput.tsx
  40. 6 6
      src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx
  41. 1 0
      src/graphql/cart/mutations/AddProductToCart.ts
  42. 70 0
      src/graphql/cart/mutations/CreateApplyCoupon.ts
  43. 1 0
      src/graphql/cart/mutations/CreateMergeCart.ts
  44. 66 0
      src/graphql/cart/mutations/CreateRemoveCoupon.ts
  45. 1 0
      src/graphql/cart/mutations/GetCartItem.ts
  46. 2 1
      src/graphql/cart/mutations/RemoveCartItem.ts
  47. 2 1
      src/graphql/cart/mutations/UpdateCartItems.ts
  48. 3 1
      src/graphql/cart/mutations/index.ts
  49. 8 1
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  50. 3 2
      src/graphql/customer/mutations/CustomerRegistration.ts
  51. 44 0
      src/graphql/customer/query/GetCustomerAddress.ts
  52. 2 1
      src/graphql/customer/query/index.ts
  53. 1 0
      src/graphql/index.ts
  54. 394 27
      src/lib/Airwallex/airwallexInit.ts
  55. 68 215
      src/lib/Airwallex/useAirwallexCard.ts
  56. 1 32
      src/lib/ApolloClientBrowser.ts
  57. 1 26
      src/lib/restApiClient.ts
  58. 4 1
      src/providers/GlobalProviders.tsx
  59. 103 0
      src/providers/LoginModalProvider.tsx
  60. 0 2
      src/providers/SessionManager.tsx
  61. 0 21
      src/providers/SessionSync.tsx
  62. 9 8
      src/providers/ToastProvider.tsx
  63. 0 1
      src/providers/index.ts
  64. 0 30
      src/store/slices/user-slice.ts
  65. 0 2
      src/store/store.ts
  66. 6 0
      src/types/cart/type.ts
  67. 0 13
      src/types/checkout/type.ts
  68. 63 1
      src/types/customer/type.ts
  69. 13 9
      src/types/types.ts
  70. 5 1
      src/utils/auth.ts
  71. 56 49
      src/utils/bagisto/index.ts
  72. 1 0
      src/utils/cartDetailTools.ts
  73. 10 0
      src/utils/constants.ts
  74. 33 8
      src/utils/cookie-tools.ts
  75. 0 13
      src/utils/getCartToken.ts
  76. 94 18
      src/utils/hooks/useAddToCart.ts
  77. 10 13
      src/utils/hooks/useCartDetail.ts
  78. 63 0
      src/utils/hooks/useGetCustomerAddress.ts
  79. 14 47
      src/utils/hooks/useGuestCartToken.ts
  80. 45 0
      src/utils/hooks/useUserRegister.ts
  81. 0 42
      src/utils/jwt-cookie.ts

+ 14 - 2
README.md

@@ -256,6 +256,7 @@ apollo client 中的错误分类:
 
 next-auth只负责登录和校验用户
 
+关于next-auth: https://juejin.cn/post/7532344864815022115
 
 
 ## 关于cookie的携带
@@ -339,15 +340,19 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 37. paypal provider 支付参数根据环境变量配置 --- 已完成
 38. CartAndUserActions 组件在支付结果页多次挂载问题
 39. 服务端去掉apollo client。apollo client 只在客户端使用; apollo 错误处理(第9条);
-40. Authorization 写入header优化;
+40. Authorization 写入header优化;-- 已完成
     ---- https://chatgpt.com/share/6a6acb5d-e184-83ea-acfc-839fe0d34caf
 41. 货币切换功能,货币切换后货币符号没有修改问题
 42. 错误处理 https://nextjs.org/docs/app/getting-started/error-handling
-43. redux中的user删除
+43. redux中的user删除   --- 已完成
 44. 关于订单id,管理后台订单列表里显示的是increment_id(数据库里的字段名),前端页面上用的是id(数据库里的字段名)对应接口里的orderId
 
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
+46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
+47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
+    对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
+
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
@@ -435,6 +440,13 @@ public/.well-known/apple-developer-merchantid-domain-association
 ```
 本地开发时使用ngrok测试支付。
 
+## 关于airwallex卡支付
+以页面生命周期为准,只能初始化一个卡支付。
+
+useAirwallexCard -- 负责管理卡支付输入框的生命周期:创建 --> 销毁
+useAirwallexCardState -- 负责获取卡支付输入框状态,使用useSyncExternalStore让react订阅状态变化
+airwallexManager -- 负责提供接口,创建,销毁,绑定事件,暴漏第三方接口等
+
 ## 关于hook
 hook 的定义:
 

+ 0 - 6
package.json

@@ -62,11 +62,5 @@
     "ts-node": "^10.9.2",
     "typescript": "^5"
   },
-  "pnpm": {
-    "overrides": {
-      "@types/react": "19.2.14",
-      "@types/react-dom": "19.2.3"
-    }
-  },
   "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
 }

+ 6 - 0
pnpm-workspace.yaml

@@ -0,0 +1,6 @@
+packages:
+  - '.'
+
+overrides:
+  '@types/react': '19.2.14'
+  '@types/react-dom': '19.2.3'

+ 54 - 0
src/actions/mergeCartAction.ts

@@ -0,0 +1,54 @@
+"use server";
+
+
+import {cookies} from "next/headers";
+import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
+import { bagistoFetch } from "@utils/bagisto";
+import { CREATE_MERGE_CART } from "@/graphql";
+import { CreateMergeCartData } from "@/types/cart/type";
+
+export async function mergeCartAction(){
+
+    const cookieStore = await cookies();
+
+    const guestCartId = cookieStore.get(GUEST_CART_ID)?.value;
+
+    cookieStore.delete(GUEST_CART_ID);
+    cookieStore.delete(GUEST_CART_TOKEN);
+    cookieStore.set(IS_GUEST,"false");
+    if(!guestCartId){
+        
+        return {
+            cartDetail: null,
+            success:true,
+            msg: ''
+        };
+    }
+    try{
+        // 调 Bagisto merge cart mutation
+        const response = await bagistoFetch<{
+            data: CreateMergeCartData,
+            variables: {cartId:number}
+        }>({
+            query: CREATE_MERGE_CART,
+            variables: {
+                cartId: Number(guestCartId)
+            },
+            cache: "no-store",
+        });
+        // cookieStore.delete(GUEST_CART_ID);
+        // cookieStore.delete(GUEST_CART_TOKEN);
+        // cookieStore.set(IS_GUEST,"false");
+        return {
+            cartDetail: response.body.data.createMergeCart.mergeCart,
+            success:true,
+            msg: ''
+        };
+    } catch(e: any) {
+        return {
+            cartDetail: null,
+            success:false,
+            msg: e.message || 'Fail to merge cart.'
+        };
+    }
+}

src/app/(checkout)/checkout/_components/AddressResultDisplay.tsx → src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplay.tsx


src/app/(checkout)/checkout/_components/AddressResultDisplayLoading.tsx → src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplayLoading.tsx


+ 153 - 23
src/app/(checkout)/checkout/_components/BillingAddressCheckout.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useMemo, useState, useEffect } from "react";
+import { useMemo, useState, useEffect, useCallback } from "react";
 import { useQuery } from "@apollo/client/react";
 import clsx from "clsx";
 import { 
@@ -19,12 +19,27 @@ import {
     IS_VALID_PHONECODE,
     IS_VALID_FULL_PHONE 
 } from "@utils/constants";
+import { useConfig } from "@utils/hooks/useConfig";
+import { CustomerAddressItem } from "@/types/customer/type";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
-import { useConfig } from "@utils/hooks/useConfig";
-export default function BillingAddressCheckout () {
+import {LyDropDown} from "@/components/theme/ui/LyDropDown";
+
+export default function BillingAddressCheckout ({
+    noAddress,
+    isGuest,
+    showFormField,
+    onShowFormFieldChange,
+    customerAddressList
+}: {
+    noAddress: boolean;
+    isGuest: boolean;
+    showFormField: boolean;
+    onShowFormFieldChange: (e: boolean) => void;
+    customerAddressList: CustomerAddressItem[];
+}) {
     const {countries} = useConfig();
     const { 
         getValues,
@@ -34,9 +49,37 @@ export default function BillingAddressCheckout () {
         control 
     } = useFormContext() // retrieve all hook methods
 
-    const billingSameAsShipping = useWatch({
+    const countriesOptions = useMemo(() => {
+        return countries.map((country) => ({
+            value: country.code,
+            label: country.name,
+            id: String(country._id),
+        }))
+    },[countries]);
+
+    const [
+        billingSameAsShipping,
+        selectedCountryCode,
+        firstName,
+        lastName,
+        streetAddress,
+        stateProvince,
+        postCode,
+        city,
+        phoneNumber
+    ] = useWatch({
         control,
-        name: 'billingSameAsShipping',
+        name: [
+            'billingSameAsShipping',
+            'billingCountry',
+            'billingFirstName',
+            'billingLastName',
+            'billingAddress',
+            'billingState',
+            'billingPostcode',
+            'billingCity',
+            'billingPhoneNumber'
+        ]
     });
 
     const shippingFields = useWatch({
@@ -82,18 +125,7 @@ export default function BillingAddressCheckout () {
         setValue("billingPhoneNumber", shippingPhoneNumber);
     }, [billingSameAsShipping, shippingFields, setValue]);
     
-    const countriesOptions = useMemo(() => {
-        return countries.map((country) => ({
-            value: country.code,
-            label: country.name,
-            id: String(country._id),
-        }))
-    },[countries]);
-
-    const selectedCountryCode = useWatch({
-        control,
-        name: 'billingCountry',
-    });
+    
 
     const selectedCountry = useMemo(() => {
         return countries.find( (country) => country.code === selectedCountryCode)
@@ -116,7 +148,7 @@ export default function BillingAddressCheckout () {
         }));
     }
 
-   
+    const [showDropDown, setShowDropDown] = useState(false);
     // 是否用户手动选择过区号
     const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] = useState(false);
 
@@ -141,10 +173,33 @@ export default function BillingAddressCheckout () {
     },[selectedCountryCode,hasUserSelectedPhoneCode,getValues,setValue]);
 
 
+    const showMyAddressList = () => {
+        setShowDropDown(true);
+    };
+    const closeMyaddressList = useCallback(() => {
+        setShowDropDown(false);
+    },[]);
 
+    // 从地址列表选择地址
+    const selectAddress = (param: CustomerAddressItem | 'edit') => {
+        if(param === 'edit') {
+            if(!showFormField) onShowFormFieldChange(true);
+        } else {
+            setValue('billingFirstName',param.firstName);
+            setValue('billingLastName',param.lastName);
+            setValue('billingAddress',param.address);
+            setValue('billingCountry',param.country);
+            setValue('billingState',param.state);
+            setValue('billingPostcode',param.postcode);
+            setValue('billingCity',param.city);
+            setValue('billingPhoneNumber',param.phone);
+        }
+        closeMyaddressList();
+    };
     
-    return (
-        <section className="w-full mt-4"> 
+    return (<>
+        <section className="w-full mt-4 pb-8"> 
+
             <div className="box-border w-full p-3 bg-ly-lightgray">
                 <div className="flex justify-between items-center">
                     <span className="text-ly-12 flex-none">Billing Address</span>
@@ -162,11 +217,86 @@ export default function BillingAddressCheckout () {
                 <p className="text-ly-12 mt-1.25">Same As Shipping Address</p>
             </div>
 
+            <div className={clsx("w-full mt-4",{
+                "hidden": billingSameAsShipping,
+            })}>
+                {noAddress ? 
+                    <div onClick={showMyAddressList}
+                        className="box-border w-full h-9 justify-between flex items-center px-4 relative bg-[url(/image/address-bg.webp)] bg-position-[0_-70%] bg-size-[100%_auto]"
+                    >
+                        <span className="text-ly-12">+New Shipping Address</span>
+                        {!isGuest && 
+                            <button className="flex-none" title="change address">
+                                <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
+                            </button>
+                        }
+                    </div>
+                : 
+                    <div onClick={showMyAddressList}
+                        className="w-full box-border px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]"
+                    >
+                        <div className="border-b-1 flex h-11.25 items-center justify-between">
+                            <p className="text-ly-13 font-medium">
+                                {firstName} {lastName}
+                                <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
+                                {phoneNumber}
+                            </p>
+                            {!isGuest && 
+                                <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
+                            }
+                        </div>
+                        <div className="flex items-center h-10">
+                            <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
+                                <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"  d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
+                                <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                                <path d="M11.9983 11.3653C13.1666 11.3653 14.1138 10.4181 14.1138 9.24979C14.1138 8.08144 13.1666 7.13428 11.9983 7.13428C10.83 7.13428 9.88281 8.08144 9.88281 9.24979C9.88281 10.4181 10.83 11.3653 11.9983 11.3653Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                            </svg>
+                            <span className="text-ly-12 ml-2.5">
+                                {streetAddress}, 
+                                {city}, 
+                                {stateProvince}, 
+                                {postCode}, 
+                                {selectedCountryCode}
+                            </span>
+                        </div>
+                    </div>  
+                }
+                <LyDropDown
+                    showDropDown={showDropDown}
+                    onClose={closeMyaddressList}
+                >
+                    <div className="w-full p-3 text-ly-13 font-medium" key={'edit'} onClick={() => {selectAddress('edit')}}>
+                        Edit Address
+                    </div>
+                    {customerAddressList.map((item) => {
+                        return (
+                            <div className="w-full p-3" key={item.id} onClick={() => selectAddress(item)}>
+                        
+                                <p className="text-ly-13 leading-4">
+                                    {item.firstName} {item.lastName}, 
+                                    {item.address}, 
+                                    {item.city}, 
+                                    {item.state}, 
+                                    {item.postcode}, 
+                                    {item.country},
+                                    {item.phone}
+                                </p>
+                            </div>
+                        );
+                    })}
+                    
+                </LyDropDown>
+                
+            </div> 
+
+
             <div className={clsx("box-border w-full mt-4",{
-                    "hidden": billingSameAsShipping,
+                    "hidden": (!showFormField && !noAddress) || billingSameAsShipping
                 })}
             >
-                <div className="w-full">
+                <div className={clsx("w-full",{
+                    "hidden": !isGuest
+                })}>
                     <label className="text-ly-12 block mb-4 font-semibold">
                         Email *
                     </label>
@@ -352,5 +482,5 @@ export default function BillingAddressCheckout () {
 
 
         </section>
-    );
+    </>);
 }

+ 448 - 0
src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx

@@ -0,0 +1,448 @@
+"use client";
+
+import {Ref, useState, useEffect, useCallback, useImperativeHandle } from "react";
+
+import { useForm, FormProvider } from "react-hook-form";
+import { useCustomToast } from "@/utils/hooks/useToast";
+import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
+
+import { useGetCustomerAddress } from "@utils/hooks/useGetCustomerAddress";
+import { 
+    ShipAddressFormData,
+    FullAddressFormData,
+    CreateCheckoutAddressVariables,
+} from "@/types/checkout/type";
+import { CustomerAddressItem } from "@/types/customer/type";
+import { CartDetail,CartAddress } from "@/types/cart/type";
+
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+
+import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
+
+import AddressResultDisplay from "./AddressResultDisplay";
+import ShippingAddressCheckout from "./ShippingAddressCheckout";
+import BillingAddressCheckout from "./BillingAddressCheckout";
+import CommonModal from "@/components/theme/ui/CommonModal";
+
+
+
+export interface RefCheckoutAddressHandle {
+  getAddressFormDate: () => void;
+  validateAddressForm: () => Promise<boolean>;
+}
+
+/***
+ * 不用useForShipping字段了
+ * 以shipping address 为准,根据shippingaddress 设置billing address
+ * 保存完地址之后要重新获取运输方式和支付方式
+ */
+
+/**
+ * 地址表单默认值
+ */
+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接口的参数
+ */
+function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateCheckoutAddressVariables {
+    const formDataShippingAddress:ShipAddressFormData = {
+        shippingAddressId: formData.shippingAddressId,
+        shippingEmail: formData.shippingEmail,
+        shippingFirstName: formData.shippingFirstName,
+        shippingLastName: formData.shippingLastName,
+        shippingCompanyName: formData.shippingCompanyName,
+        shippingAddress: formData.shippingAddress,
+        shippingCountry: formData.shippingCountry,
+        shippingState: formData.shippingState,
+        shippingCity: formData.shippingCity,
+        shippingPostcode: formData.shippingPostcode,
+        shippingPhoneNumber: formData.shippingPhoneNumber,
+    };
+    let formDataBillingAddress = {
+        billingAddressId: formData.billingAddressId,
+        billingEmail: formData.billingEmail,
+        billingFirstName: formData.billingFirstName,
+        billingLastName: formData.billingLastName,
+        billingCompanyName : formData.billingCompanyName,
+        billingAddress: formData.billingAddress,
+        billingCountry: formData.billingCountry,
+        billingState: formData.billingState,
+        billingCity: formData.billingCity,
+        billingPostcode: formData.billingPostcode,
+        billingPhoneNumber: formData.billingPhoneNumber,
+
+        useForShipping: false,
+    };
+    if(formData.billingSameAsShipping) {
+        formDataBillingAddress = {
+            billingAddressId: formData.billingAddressId,
+            billingEmail: formDataShippingAddress.shippingEmail,
+            billingFirstName: formDataShippingAddress.shippingFirstName,
+            billingLastName: formDataShippingAddress.shippingLastName,
+            billingCompanyName : formDataShippingAddress.shippingCompanyName,
+            billingAddress: formDataShippingAddress.shippingAddress,
+            billingCountry: formDataShippingAddress.shippingCountry,
+            billingState: formDataShippingAddress.shippingState,
+            billingCity: formDataShippingAddress.shippingCity,
+            billingPostcode: formDataShippingAddress.shippingPostcode,
+            billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
+            useForShipping: true,
+        }
+    }
+    return {
+       ...formDataShippingAddress,
+       ...formDataBillingAddress
+    };
+    
+}
+const myAddressPerPage = 50; // 每页的个数
+export function CheckoutAddress({
+    ref,
+    loginEmail,
+    cartData,
+    onSaveAddress,
+}: {
+    ref: Ref<RefCheckoutAddressHandle>;
+    loginEmail: string;
+    cartData: CartDetail;
+    onSaveAddress: (saveRes: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => void;
+}) {
+
+    const addressFormDefaultValues = getAddressFormDataFromCart(cartData,loginEmail);
+
+    const {getCustomerAddress} = useGetCustomerAddress();
+    const { showToast } = useCustomToast();
+    const { saveCheckoutAddress } = useCheckoutAddress(loginEmail);
+    
+    // useForm() 只会在组件初始化时读取一次 defaultValues
+    const addressForm = useForm<FullAddressFormData>({
+        mode: "onChange",          // 或 "onChange"
+        reValidateMode: "onChange",
+        defaultValues: addressFormDefaultValues 
+    });
+    const [myAddressList, setMyAddressList] = useState<CustomerAddressItem[]>([]);
+    const [showShipFormField, setShowShipFormField] = useState(false);
+    const [showBillFormField, setShowBillFormField] = useState(false);
+    const [addressFormModalOpen, setAddressFormModalOpen] = useState(false);
+
+
+    useImperativeHandle(ref, () => {
+        return {
+            // 获取用户填写的地址
+            getAddressFormDate: () => {
+                return addressForm.getValues();
+            },
+         
+            // 触发校验
+            validateAddressForm: () => {
+                return addressForm.trigger();
+            },
+        }
+        
+    },[addressForm]);
+
+
+    const showShipFormFieldChange = useCallback((e: boolean) => {
+        setShowShipFormField(e);
+    },[]);
+    const showBillFormFieldChange = useCallback((e: boolean) => {
+        setShowBillFormField(e);
+    },[]);
+
+
+    const openAddressFormModal = () => {
+        setAddressFormModalOpen(true);
+        setShowShipFormField(false);
+        setShowBillFormField(false);
+        if(cartData.billingAddress && cartData.shippingAddress) {
+            addressForm.reset({
+                ...getAddressFormDataFromCart(cartData)
+            });
+        }
+
+        
+    };
+    const closeAddressFormModal = () => {
+        setAddressFormModalOpen(false);
+        setShowShipFormField(false);
+        setShowBillFormField(false);
+        // 重置表单
+        addressForm.reset();
+    };
+    const addressFormOnSubmit = async (formData: FullAddressFormData) => { 
+        // console.log('addressFormOnSubmit ---- ',formData); return;
+        const saveAddressParam = generateSaveCheckoutAddressParam(formData);
+
+        overlayLoading.start();
+        try {
+
+            const saveRes = await saveCheckoutAddress(saveAddressParam);
+            console.log('CREATE_CHECKOUT_ADDRESS res ====== ',saveRes);
+            if(!saveRes.error) {
+                // 地址保存成功后重新获取运输方式和支付方式 
+                // 保存完地址之后,把保存后的地址id同步到表单里;
+                const createAddressData = saveRes.data;
+                if(!createAddressData) {
+                    // 提示用户出错了,刷新页面
+                    showToast('Something wrong. Please refresh the page.', 'danger');
+                    return;
+                }
+
+                addressForm.resetField('shippingAddressId',{
+                    defaultValue: createAddressData.shippingAddressId
+                });
+                addressForm.resetField('billingAddressId',{
+                    defaultValue: createAddressData.billingAddressId
+                });
+
+        
+                // 同步地址到购物车详情
+                const newBillingAddress =  {
+                    id: String(createAddressData.billingAddressId),
+                    firstName: createAddressData.billingFirstName,
+                    lastName: createAddressData.billingLastName,
+                    email: createAddressData.billingEmail,
+                    address: createAddressData.billingAddress,
+                    city: createAddressData.billingCity,
+                    state: createAddressData.billingState,
+                    country: createAddressData.billingCountry,
+                    postcode: createAddressData.billingPostcode,
+                    phone: createAddressData.billingPhoneNumber
+                };
+                const newShippingAddress = {
+                    id: String(createAddressData.shippingAddressId),
+                    firstName: createAddressData.shippingFirstName,
+                    lastName: createAddressData.shippingLastName,
+                    email: createAddressData.shippingEmail,
+                    address: createAddressData.shippingAddress,
+                    city: createAddressData.shippingCity,
+                    state: createAddressData.shippingState,
+                    country: createAddressData.shippingCountry,
+                    postcode: createAddressData.shippingPostcode,
+                    phone: createAddressData.shippingPhoneNumber,
+                };
+   
+                onSaveAddress({
+                    billingAddress: newBillingAddress,
+                    shippingAddress: newShippingAddress
+                });
+               
+            } else {
+                showToast(saveRes.msg, 'danger');
+            }
+            setAddressFormModalOpen(false);
+        } catch(err) {
+            // 错误处理
+            console.error("save address error", err);
+           
+            showToast('Save address failed. Please try again.', 'danger');
+        } finally {
+            overlayLoading.stop();
+        }
+        
+    };
+
+    const noShipAddress = cartData.shippingAddress === null;
+    const noBillAddress = cartData.billingAddress === null;
+
+    
+    useEffect(() => {
+        if(cartData.isGuest) return;
+
+        async function loadAddress() {
+
+            overlayLoading.start();
+            const res = await getCustomerAddress({
+                first: myAddressPerPage
+            });
+            overlayLoading.stop();
+            if(!res.error && res.data !== null) {
+                setMyAddressList(res.data.list);
+            } else {
+                showToast(res.msg,"danger");
+            }  
+        }
+        loadAddress();
+    },[showToast,cartData.isGuest]);
+    
+    // function tt() {
+    //     const arr = [
+    //         ['US','3803800217'], 
+    //         ['CH','+4186043315'], 
+    //         ['US','334-208-6177'], 
+    //         ['AU','+61 404103617'], 
+    //         ['US','1-9166705105'], 
+    //         ['CA','819 384 3221'],
+    //         ['US','(409) 239-9482'],
+    //         ['US','1-(651) 421-2762'],
+    //         ['CH', '1-792930242'],
+    //         ['US', '+1 4570438892'],
+    //         ['US', '1-+1 (404) 276-1068'],
+    //         ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
+    //     ];
+    //     arr.forEach((item) => {
+    //         normalizePhoneForForm(item[1], item[0]);
+    //     });
+    // }
+    return (<>
+        <AddressResultDisplay shippingAddress={cartData.shippingAddress || null} 
+            onAddressModalOpenClick={openAddressFormModal}
+        />
+        <CommonModal
+            isOpen={addressFormModalOpen}
+            onClose={closeAddressFormModal}
+            header={
+                <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
+                    <span>Shipping Address</span>
+                    <button className="w-6 h-6" onClick={closeAddressFormModal}>
+                        <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>
+            }
+            body={
+                <div className="box-border w-full h-full p-3.75">
+                    <div>
+                        <FormProvider {...addressForm}>
+                            <form>
+                                <ShippingAddressCheckout 
+                                    noAddress={noShipAddress}
+                                    isGuest={cartData.isGuest}
+                                    customerAddressList={myAddressList}
+                                    showFormField={showShipFormField}
+                                    onShowFormFieldChange={showShipFormFieldChange}
+                                />
+                                <BillingAddressCheckout 
+                                    noAddress={noBillAddress}
+                                    isGuest={cartData.isGuest}
+                                    customerAddressList={myAddressList}
+                                    showFormField={showBillFormField}
+                                    onShowFormFieldChange={showBillFormFieldChange}
+                                />
+                            </form>
+                        </FormProvider>
+                    </div>
+                </div>
+            }
+            footer={
+                <div className="box-border w-full p-3.75">
+                <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
+                    onClick={addressForm.handleSubmit(addressFormOnSubmit)}
+                >
+                    Save
+                </button>
+                </div>
+            }
+        />
+            
+
+    </>);
+};

+ 173 - 11
src/app/(checkout)/checkout/_components/ShippingAddressCheckout.tsx

@@ -1,6 +1,7 @@
 "use client";
 
-import {useMemo,useEffect, useState} from "react";
+import {useMemo,useEffect, useState, useCallback} from "react";
+import clsx from "clsx";
 import { useQuery } from "@apollo/client/react";
 import { 
     useFormContext, 
@@ -13,22 +14,40 @@ import {
 } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
 import { GET_COUNTRY_STATES } from "@/graphql";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
+import { CustomerAddressItem } from "@/types/customer/type";
+import { useConfig } from "@utils/hooks/useConfig";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
-import { useConfig } from "@utils/hooks/useConfig";
+import {LyDropDown} from "@/components/theme/ui/LyDropDown";
+
+
+
+export default function ShippingAddressCheckout({
+    noAddress,
+    isGuest,
+    showFormField,
+    onShowFormFieldChange,
+    customerAddressList,
+}: {
+    noAddress: boolean;
+    isGuest: boolean;
+    showFormField: boolean;
+    onShowFormFieldChange: (e: boolean) => void;
+    customerAddressList: CustomerAddressItem[];
+}) {
 
-export default function ShippingAddressCheckout () {
     const {countries} = useConfig();
     const { 
         getValues,
         setValue,
         register,
         formState: { errors },
-        control 
+        control,
     } = useFormContext() // retrieve all hook methods
-
+    console.log('errors ----- ', errors);
+    
     const countriesOptions = useMemo(() => {
         return countries.map((country) => ({
             value: country.code,
@@ -37,9 +56,27 @@ export default function ShippingAddressCheckout () {
         }))
     },[countries]);
 
-    const selectedCountryCode = useWatch({
+    const [ 
+        selectedCountryCode,
+        firstName,
+        lastName,
+        streetAddress,
+        stateProvince,
+        postCode,
+        city,
+        phoneNumber
+    ] = useWatch({
         control,
-        name: 'shippingCountry',
+        name: [
+            'shippingCountry',
+            'shippingFirstName',
+            'shippingLastName',
+            'shippingAddress',
+            'shippingState',
+            'shippingPostcode',
+            'shippingCity',
+            'shippingPhoneNumber'
+        ]
     });
     const selectedCountry = useMemo(() => {
         return countries.find( (country) => country.code === selectedCountryCode)
@@ -62,9 +99,12 @@ export default function ShippingAddressCheckout () {
         }));
     }
 
+    const [showDropDown, setShowDropDown] = useState(false);
+
     // 是否用户手动选择过区号
     const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] = useState(false);
 
+
     // 国家变更后 state字段的值需要清空
     // 选择国家时同步电话区号
     useEffect(() => {
@@ -86,10 +126,132 @@ export default function ShippingAddressCheckout () {
         
     },[selectedCountryCode,hasUserSelectedPhoneCode,getValues,setValue]);
 
+    const showMyAddressList = () => {
+        setShowDropDown(true);
+    };
+    const closeMyaddressList = useCallback(() => {
+        setShowDropDown(false);
+    },[]);
+    // 从地址列表选择地址
+    const selectAddress = (param: CustomerAddressItem | 'edit') => {
+        if(param === 'edit') {
+            if(!showFormField) onShowFormFieldChange(true);
+        } else {
+            setValue('shippingFirstName',param.firstName);
+            setValue('shippingLastName',param.lastName);
+            setValue('shippingAddress',param.address);
+            setValue('shippingCountry',param.country);
+            setValue('shippingState',param.state);
+            setValue('shippingPostcode',param.postcode);
+            setValue('shippingCity',param.city);
+            setValue('shippingPhoneNumber',param.phone);
+        }
+        closeMyaddressList();
+    };
 
-    return (
-        <div className="box-border w-full">
-            <div className="w-full">
+    /**
+     * 游客:
+     * 1. 购物车没地址 -- 进来显示填写新地址,地址表单;填完地址,保存,关闭弹窗,购物车地址更新;再次进来显示填写的地址,隐藏表单;如果想修改,就点击修改,显示地址表单;
+     * 2. 购物车有地址 -- 进来显示已有的地址,隐藏表单;如果想修改,就点击修改,显示地址表单;
+     * 
+     * 登录用户:
+     * 1. 购物车没地址 -- 进来显示填写新地址,地址表单;
+     *    如果想选已有地址,点击箭头,弹出地址列表,选择地址,选完地址更新到表单,关闭地址列表(打上选中状态);如果地址列表没数据,显示没有地址供选择;
+     *    如果不想选地址,就填写地址表单。
+     *    保存地址,关闭弹窗;
+     *    再次进来,显示购物车中的地址;
+     * 
+     * 2. 购物车有地址 -- 进来显示购物车中的地址;
+     *    如果想修改地址,点击箭头,出现地址列表
+     *    
+     * 地址列表数据直接在 父组件获取
+     */
+    return (<>
+        
+        <div className="w-full">
+            {noAddress ? 
+                <div onClick={showMyAddressList}
+                    className="box-border w-full h-9 justify-between flex items-center px-4 relative bg-[url(/image/address-bg.webp)] bg-position-[0_-70%] bg-size-[100%_auto]"
+                >
+                    <span className="text-ly-12">+New Shipping Address</span>
+                    {!isGuest && 
+                        <button className="flex-none" title="change address">
+                            <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
+                        </button>
+                    }
+                </div>
+            : 
+                <div onClick={showMyAddressList}
+                    className="w-full box-border px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]"
+                >
+                    <div className="border-b-1 flex h-11.25 items-center justify-between">
+                        <p className="text-ly-13 font-medium">
+                            {firstName} {lastName}
+                            <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
+                            {phoneNumber}
+                        </p>
+                        {!isGuest && 
+                            <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
+                        }
+                    </div>
+                    <div className="flex items-center h-10">
+                        <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"  d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
+                            <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                            <path d="M11.9983 11.3653C13.1666 11.3653 14.1138 10.4181 14.1138 9.24979C14.1138 8.08144 13.1666 7.13428 11.9983 7.13428C10.83 7.13428 9.88281 8.08144 9.88281 9.24979C9.88281 10.4181 10.83 11.3653 11.9983 11.3653Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                        </svg>
+                        <span className="text-ly-12 ml-2.5">
+                            {streetAddress}, 
+                            {city}, 
+                            {stateProvince}, 
+                            {postCode}, 
+                            {selectedCountryCode}
+                        </span>
+                    </div>
+                </div>  
+            }
+            <LyDropDown
+                showDropDown={showDropDown}
+                onClose={closeMyaddressList}
+            >
+                <div className="w-full p-3 text-ly-13 font-medium" key={'edit'} onClick={() => {selectAddress('edit')}}>
+                    Edit Address
+                </div>
+                {customerAddressList.map((item) => {
+                    return (
+                        <div className="w-full p-3" key={item.id} onClick={() => selectAddress(item)}>
+                    
+                            <p className="text-ly-13 leading-4">
+                                {item.firstName} {item.lastName}, 
+                                {item.address}, 
+                                {item.city}, 
+                                {item.state}, 
+                                {item.postcode}, 
+                                {item.country},
+                                {item.phone}
+                            </p>
+                        </div>
+                    );
+                })}
+                
+            </LyDropDown>
+            
+        </div>     
+            
+        
+        <p className="text-ly-12 leading-ly-20 mt-2 mb-2">
+            Select a shipping address from your address book or enter a new address.
+        </p>
+        {/**
+         * 登录用户: noAddres=false and showFormField = false 时 隐藏;  noAddres=true 显示
+         * 游客: noAddres=false and showFormField = false 时 隐藏; noAddres=true 显示
+         */}
+        <div className={clsx("box-border w-full", {
+            "hidden": !showFormField && !noAddress
+        })}>
+            <div className={clsx("w-full",{
+                "hidden": !isGuest
+            })}>
                 <label className="text-ly-12 block mb-4 font-semibold">
                     Email *
                 </label>
@@ -273,5 +435,5 @@ export default function ShippingAddressCheckout () {
             </div>
 
         </div>
-    );
+    </>);
 };

文件差異過大導致無法顯示
+ 110 - 0
src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx


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

@@ -1,7 +1,7 @@
 "use client";
 
 import clsx from 'clsx';
-import { useRouter } from 'next/navigation'
+import { useRouter } from 'next/navigation';
 import {AirwallexCartNumberElementType} from "@/lib/Airwallex/airwallexInit";
 import PaypalButton from "./PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";

文件差異過大導致無法顯示
+ 197 - 0
src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx


+ 93 - 385
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -2,9 +2,7 @@
 
 import {useState, useRef, useEffect, useCallback } from "react";
 import clsx from "clsx";
-import { useForm, FormProvider } from "react-hook-form";
 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 } from "@/store/hooks";
@@ -12,10 +10,8 @@ import { updateCart, clearCart } from "@/store/slices/cart-slice";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
+import {airwallexManager} from "@/lib/Airwallex/useAirwallexCard";
 import { 
-    ShipAddressFormData,
-    FullAddressFormData,
-    CreateCheckoutAddressVariables,
     PlaceOrderResult,
     PlaceOrderFunction,
     CreatePaymentInitiateVariables
@@ -24,197 +20,24 @@ import { CartDetail,CartAddress } from "@/types/cart/type";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
-import { LoadingSpinner } from "@components/common/LoadingSpinner";
-
-import AddressResultDisplay from "./AddressResultDisplay";
-import ShippingAddressCheckout from "./ShippingAddressCheckout";
-import BillingAddressCheckout from "./BillingAddressCheckout";
 import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
 import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
-
+import LoadingPaymentMethod from "./LoadingPaymentMethod";
+import {CheckoutAddress, RefCheckoutAddressHandle} from "./CheckoutAddress/CheckoutAssress";
 import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
-import CommonModal from "@/components/theme/ui/CommonModal";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
 import CheckoutGiftCardGate from "./GiftCard/CheckoutGiftCardGate";
 import PromotionsDetails from "./PromotionsDetails";
-
+import CheckoutVipPlus from "./VipPlus/CheckoutVipPlus";
+import CheckoutProducts from "./CheckoutProducts/CheckoutProducts";
+import CheckoutCoupon from "./CheckoutCoupon/CheckoutCoupon";
 /***
- * 不用useForShipping字段了
- * 以shipping address 为准,根据shippingaddress 设置billing address
  * 保存完地址之后要重新获取运输方式和支付方式
  * 变更运输方式后重新获取支付方式
  * @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接口的参数
- */
-function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateCheckoutAddressVariables {
-    const formDataShippingAddress:ShipAddressFormData = {
-        shippingAddressId: formData.shippingAddressId,
-        shippingEmail: formData.shippingEmail,
-        shippingFirstName: formData.shippingFirstName,
-        shippingLastName: formData.shippingLastName,
-        shippingCompanyName: formData.shippingCompanyName,
-        shippingAddress: formData.shippingAddress,
-        shippingCountry: formData.shippingCountry,
-        shippingState: formData.shippingState,
-        shippingCity: formData.shippingCity,
-        shippingPostcode: formData.shippingPostcode,
-        shippingPhoneNumber: formData.shippingPhoneNumber,
-    };
-    let formDataBillingAddress = {
-        billingAddressId: formData.billingAddressId,
-        billingEmail: formData.billingEmail,
-        billingFirstName: formData.billingFirstName,
-        billingLastName: formData.billingLastName,
-        billingCompanyName : formData.billingCompanyName,
-        billingAddress: formData.billingAddress,
-        billingCountry: formData.billingCountry,
-        billingState: formData.billingState,
-        billingCity: formData.billingCity,
-        billingPostcode: formData.billingPostcode,
-        billingPhoneNumber: formData.billingPhoneNumber,
-
-        useForShipping: false,
-    };
-    if(formData.billingSameAsShipping) {
-        formDataBillingAddress = {
-            billingAddressId: formData.billingAddressId,
-            billingEmail: formDataShippingAddress.shippingEmail,
-            billingFirstName: formDataShippingAddress.shippingFirstName,
-            billingLastName: formDataShippingAddress.shippingLastName,
-            billingCompanyName : formDataShippingAddress.shippingCompanyName,
-            billingAddress: formDataShippingAddress.shippingAddress,
-            billingCountry: formDataShippingAddress.shippingCountry,
-            billingState: formDataShippingAddress.shippingState,
-            billingCity: formDataShippingAddress.shippingCity,
-            billingPostcode: formDataShippingAddress.shippingPostcode,
-            billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
-            useForShipping: true,
-        }
-    }
-    return {
-       ...formDataShippingAddress,
-       ...formDataBillingAddress
-    };
-    
-}
 
 export default function CheckoutWrapper({
     loginEmail,
@@ -228,7 +51,7 @@ export default function CheckoutWrapper({
     const {loadSdk} = usePaymentSDKContext();
 
     const { createOrder } = usePlaceOrder(); 
-    const addressFormDefaultValues = getAddressFormDataFromCart(cartDetailData,loginEmail);
+  
     const dispatch = useAppDispatch();
     
     const [cartData, setCartData] = useState<CartDetail>(cartDetailData);
@@ -253,24 +76,14 @@ export default function CheckoutWrapper({
     } = useCheckoutPaymentMethod();
 
     const { showToast } = useCustomToast();
-    const { saveCheckoutAddress } = useCheckoutAddress(loginEmail);
 
+
+    const checkoutAddressRef = useRef<RefCheckoutAddressHandle>(null);
     const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
     const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
 
     
 
-    // useForm() 只会在组件初始化时读取一次 defaultValues
-    const addressForm = useForm<FullAddressFormData>({
-        mode: "onChange",          // 或 "onChange"
-        reValidateMode: "onChange",
-        defaultValues: addressFormDefaultValues 
-    });
-
-    const [addressModalOpen, setAddressModalOpen] = useState(false);
-
-
-    const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
 
     const changePaymentMethodToloadSdk = (method:string) => {
         if(method === 'klarna' || 
@@ -295,110 +108,23 @@ export default function CheckoutWrapper({
         }
     },[dispatch]);
 
-    const openAddressModal = () => {
-        setAddressModalOpen(true);
-
-        if(cartData.billingAddress && cartData.shippingAddress) {
-            addressForm.reset({
-                ...getAddressFormDataFromCart(cartData)
-            });
-        }
-
-        
-    };
-    const closeAddressModal = (e:boolean) => {
-        setAddressModalOpen(e);
-        // 重置表单
-        addressForm.reset();
+    const saveAddressCallback = async (resData: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => {
+        await Promise.all([getShippingMethod(),getPaymentMethod()]);
+        dispatchCartStore('update',{
+            billingAddress: resData.billingAddress,
+            shippingAddress: resData.shippingAddress
+        });
+        setCartData(Object.assign({...cartData},{
+            billingAddress: resData.billingAddress,
+            shippingAddress: resData.shippingAddress
+        }));
     };
-    const addressFormOnSubmit = async (formData: FullAddressFormData) => { 
-        // console.log('addressFormOnSubmit ---- ',formData); return;
-        if(addressSaving) {
-            return;
-        }
-        const saveAddressParam = generateSaveCheckoutAddressParam(formData);
-        setAddressSaving(true);
-        try {
 
-            const saveRes = await saveCheckoutAddress(saveAddressParam);
-            console.log('CREATE_CHECKOUT_ADDRESS res ====== ',saveRes);
-            if(!saveRes.error) {
-                // 地址保存成功后重新获取运输方式和支付方式 
-                // await getShippingMethod();
-                // await getPaymentMethod();
-                await Promise.all([getShippingMethod(),getPaymentMethod()]);
-                
-                // 保存完地址之后,把保存后的地址id同步到表单里;
-                const createAddressData = saveRes.data;
-                if(createAddressData !== null) {
-                    addressForm.resetField('shippingAddressId',{
-                        defaultValue: createAddressData.shippingAddressId
-                    });
-                    addressForm.resetField('billingAddressId',{
-                        defaultValue: createAddressData.billingAddressId
-                    });
-
-        
-                    // 同步地址到购物车详情
-                    const newBillingAddress =  {
-                        id: String(createAddressData.billingAddressId),
-                        firstName: createAddressData.billingFirstName,
-                        lastName: createAddressData.billingLastName,
-                        email: createAddressData.billingEmail,
-                        address: createAddressData.billingAddress,
-                        city: createAddressData.billingCity,
-                        state: createAddressData.billingState,
-                        country: createAddressData.billingCountry,
-                        postcode: createAddressData.billingPostcode,
-                        phone: createAddressData.billingPhoneNumber
-                    };
-                    const newShippingAddress = {
-                        id: String(createAddressData.shippingAddressId),
-                        firstName: createAddressData.shippingFirstName,
-                        lastName: createAddressData.shippingLastName,
-                        email: createAddressData.shippingEmail,
-                        address: createAddressData.shippingAddress,
-                        city: createAddressData.shippingCity,
-                        state: createAddressData.shippingState,
-                        country: createAddressData.shippingCountry,
-                        postcode: createAddressData.shippingPostcode,
-                        phone: createAddressData.shippingPhoneNumber,
-                    };
-                    dispatchCartStore('update',{
-                        billingAddress: newBillingAddress,
-                        shippingAddress: newShippingAddress
-                    });
-                    setCartData(Object.assign({...cartData},{
-                        billingAddress: {...newBillingAddress},
-                        shippingAddress: {...newShippingAddress}
-                    }));
-
-                } else {
-                    // 提示用户出错了,刷新页面
-                    showToast('Something wrong. Please refresh the page.', 'danger');
-                }
-            } else {
-                showToast(saveRes.msg, 'danger');
-            }
-            setAddressModalOpen(false);
-        } catch(err) {
-            // 错误处理
-            console.error("save address error", err);
-           
-            showToast('Save address failed. Please try again.', 'danger');
-        } finally {
-            setAddressSaving(false);
-        }
-        
-    };
 
 
     const handlePaymentMethodChange =  async (method: string) => { 
         changePaymentMethodToloadSdk(method);
-        // let shippingMethod = '-1';
-        // if(methodShiippingRef.current) {
-        //     shippingMethod = methodShiippingRef.current.getSelectShipMethod();
-        // }
+   
         overlayLoading.start();
         const saveRes = await saveCheckoutCart({ 
             shippingMethod: '-1',
@@ -448,10 +174,15 @@ export default function CheckoutWrapper({
 
     // 校验地址 运输方式,支付方式;
     const validateCheckout = useCallback(async () => { 
-        const addressValid = await addressForm.trigger();
-
-        if(!addressValid) {
-            showToast('Please fill in the address information correctly.', 'danger');
+        
+        if(checkoutAddressRef.current) {
+            const addressValid = await checkoutAddressRef.current.validateAddressForm();
+            if(!addressValid) {
+                showToast('Please check your address.', 'danger');
+                return false;
+            }
+        } else {
+            showToast('Please wait for address form loading.', 'danger');
             return false;
         }
         if(methodShiippingRef.current) {
@@ -473,8 +204,11 @@ export default function CheckoutWrapper({
                 return false;
             }
             if(paymentMethod === 'airwallex') {
-                const paymentMethodValid = methodPaymentRef.current.validateAirwallexCard();
-                return paymentMethodValid;
+                const airwallexCardValid = airwallexManager.validateCard();
+                if(airwallexCardValid.msg) {
+                    showToast(airwallexCardValid.msg,'danger');
+                }
+                return airwallexCardValid.valid;
             }
 
         } else {
@@ -509,7 +243,7 @@ export default function CheckoutWrapper({
 
         return true;
 
-    },[addressForm,showToast,saveCheckoutCart,dispatchCartStore]);
+    },[showToast,saveCheckoutCart,dispatchCartStore]);
     // 苹果支付下单
     const applepayPlaceOrder = useCallback(async () => { 
         const res = await createOrder();
@@ -541,9 +275,8 @@ export default function CheckoutWrapper({
             }
             const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
             let resultData: PlaceOrderResult<T>;
-            if(pmethod === 'airwallex' && methodPaymentRef.current) {
-                const airwallexCardElements = methodPaymentRef.current.getAirwallexCardElements();
-                const airCardNumber = airwallexCardElements.cardNumber as T;
+            if(pmethod === 'airwallex') {
+                const airCardNumber = airwallexManager.getCardNumberElement() as T;
                 resultData = {
                     data: res.data, 
                     otherData: airCardNumber
@@ -566,91 +299,49 @@ export default function CheckoutWrapper({
             return resultData;
     }
 
-    const onChangeGiftcard = async (paylod: CartDetail) => {
-        dispatchCartStore('update',paylod);
+    const onCartDataChange = async (paylod: CartDetail, dispatch: boolean = true) => {
+        if(dispatch) {
+            dispatchCartStore('update',paylod);
+        }
         setCartData({...paylod});
-        // @todo 还要请求支付方式和运输方式
-        // await getShippingMethod();
-        //     await getPaymentMethod();
+        // 还要请求支付方式和运输方式
         await Promise.all([getShippingMethod(),getPaymentMethod()]);
     };
 
-    // function tt() {
-    //     const arr = [
-    //         ['US','3803800217'], 
-    //         ['CH','+4186043315'], 
-    //         ['US','334-208-6177'], 
-    //         ['AU','+61 404103617'], 
-    //         ['US','1-9166705105'], 
-    //         ['CA','819 384 3221'],
-    //         ['US','(409) 239-9482'],
-    //         ['US','1-(651) 421-2762'],
-    //         ['CH', '1-792930242'],
-    //         ['US', '+1 4570438892'],
-    //         ['US', '1-+1 (404) 276-1068'],
-    //         ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
-    //     ];
-    //     arr.forEach((item) => {
-    //         normalizePhoneForForm(item[1], item[0]);
-    //     });
-    // }
+    
     const testEruda = () => {
         erudaCount.current++
         if(window.eruda && erudaCount.current > 10) {
             window.eruda.init();
         }
     };
-    return (
+    useEffect(() => {
+        console.log('checkoutWrapper mout==========================');
+        return () => {
+            console.log('checkoutWrapper unmout==========================');
+        }
+    },[]);
+    return (<>
         <section className="w-full">
             <div>
-            {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
-                    {/* {loadingAddress ? 
-                        <AddressResultDisplayLoading /> 
-                    :
-                        <AddressResultDisplay shippingAddress={cartData.shippingAddress || null} 
-                            onAddressModalOpenClick={openAddressModal}
-                        />
-                    } */}
-                    <AddressResultDisplay shippingAddress={cartData.shippingAddress || null} 
-                        onAddressModalOpenClick={openAddressModal}
-                    />
-                    <CommonModal
-                        isOpen={addressModalOpen}
-                        onClose={closeAddressModal}
-                        header={
-                            <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
-                                <span>Shipping Address</span>
-                                <button className="w-6 h-6" onClick={() => closeAddressModal(false)}>
-                                    <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>
-                        }
-                        body={
-                            <div className="box-border w-full h-full 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>
-                                    <FormProvider {...addressForm}>
-                                        <form>
-                                            <ShippingAddressCheckout />
-                                            <BillingAddressCheckout />
-                                        </form>
-                                    </FormProvider>
-                                </div>
-                            </div>
-                        }
-                        footer={
-                            <div className="box-border w-full p-3.75">
-                            <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
-                                onClick={addressForm.handleSubmit(addressFormOnSubmit)}
-                            >
-                                {addressSaving ? <LoadingSpinner /> : 'Save'}
-                            </button>
-                            </div>
-                        }
-                    />
+                <CheckoutAddress 
+                    ref={checkoutAddressRef}
+                    loginEmail={loginEmail}
+                    cartData={cartData}
+                    onSaveAddress={saveAddressCallback}
+                />
+ 
+                  
             </div>
+
+
+            <div className="mt-6 box-border px-4">
+                <CheckoutProducts 
+                    cartData={cartData}
+                    onCartChange={onCartDataChange}
+                />
+            </div>
+
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium" onClick={testEruda}>Shipping Method</h3>
            
@@ -663,12 +354,28 @@ export default function CheckoutWrapper({
                     onShipMethodChange={handleShippingMethodChange}  
                 />
             </div>
+            <div className="mt-6 box-border px-4">
+                <CheckoutCoupon 
+                    couponCode={cartData.couponCode || ''}
+                    discountAmount={cartData.formattedDiscountAmount}
+                    onCouponChange={onCartDataChange}
+                />
+            </div>
+            
 
+            {!cartData.isGuest &&
+                <div className="mt-6 box-border px-4">
+                    <CheckoutGiftCardGate 
+                        giftcardNumber={cartData.giftcardNumber || ''}
+                        formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
+                        onChangeGiftcard={onCartDataChange}
+                    />
+                </div>
+            }
             <div className="mt-6 box-border px-4">
-                <CheckoutGiftCardGate 
-                    giftcardNumber={cartData.giftcardNumber || ''}
-                    formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
-                    onChangeGiftcard={onChangeGiftcard}
+                <CheckoutVipPlus 
+                    cartData={cartData} 
+                    onSwitchVip={onCartDataChange}
                 />
             </div>
             
@@ -681,14 +388,16 @@ 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 && paymentMethodError) && <p className="mt-3 text-red-500 text-ly-12">{paymentMethodError}</p>}
+                {(!paymentMethodLoading && !paymentMethodError) &&
                 <PaymentMethodCheckout 
                     ref={methodPaymentRef}
                     paymentMethods={paymentMethodDatas} 
                     selectedPaymentMethod={selectedPaymentMethod}
-                    paymentMethodLoading={paymentMethodLoading}
-                    loadError={paymentMethodError}
                     onPaymentMethodChange={handlePaymentMethodChange}
                 />
+                }
                 
                 
             </div>
@@ -721,7 +430,6 @@ export default function CheckoutWrapper({
                 </div>
             </div>
 
-        </section> 
-        
-    );
+        </section>  
+    </>);
 };

+ 45 - 20
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -20,9 +20,13 @@ export default function ContinueToPayOrderInfo({
 }) { 
     
     const router = useRouter();
-    const {getCurrentCurrencyItem} = useConfig();
+    const {currencies,getCurrentCurrencyItem} = useConfig();
     const currentCurrency = getCurrentCurrencyItem();
     const currencySymbol = currentCurrency.symbol;
+    const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
+    const orderSymbol = orderCurrency?.symbol || currencySymbol;
+    
+    
     
     const [productsModalOpen, setProductsModalOpen] = useState(false);
 
@@ -158,31 +162,52 @@ export default function ContinueToPayOrderInfo({
                 <h3 className="text-ly-24 font-medium">Promotion Details</h3>
                 <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">Subtotal</p>
-                    <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.subTotal}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Shipping</p>
-                    <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.shippingAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Tax</p>
-                    <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.taxAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Discount</p>
-                    <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.discountAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
-                    <p className="text-ly-12 leading-ly-20">-{currencySymbol}{orderDetail.giftcardAmount}</p>
+                    <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.subTotal}</p>
                 </div>
+                {orderDetail.shippingAmount !== 0 &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Shipping</p>
+                        <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.shippingAmount}</p>
+                    </div>
+                }
+                {orderDetail.taxAmount !== 0 &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Tax</p>
+                        <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.taxAmount}</p>
+                    </div>
+                }
+                {(orderDetail.vipPlusAmount !== '0.00' && orderDetail.vipPlusAmount !== null)  &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Plus Fee</p>
+                        <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.vipPlusAmount}</p>
+                    </div>
+                }
+                {orderDetail.discountAmount !== 0 &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Discount</p>
+                        <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.discountAmount}</p>
+                    </div>
+                }
+                {(orderDetail.giftcardAmount !== '0.00' && orderDetail.giftcardAmount !== null) &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                        <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.giftcardAmount}</p>
+                    </div>
+                }
+                {(orderDetail.vipDiscountAmount !== '0.00' && orderDetail.vipDiscountAmount !== null) &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Plus Save</p>
+                        <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.vipDiscountAmount}</p>
+                    </div>
+                }
+
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>
                 </div> */}
                 <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-16 leading-ly-24">Total</p>
-                    <p className="text-ly-16 leading-ly-24 font-bold">{currencySymbol}{orderDetail.grandTotal}</p>
+                    <p className="text-ly-16 leading-ly-24 font-bold">{orderSymbol}{orderDetail.grandTotal}</p>
                 </div>
             </div>
 
@@ -191,7 +216,7 @@ export default function ContinueToPayOrderInfo({
             <ProductsModalPendingOrder 
                 isOpen={productsModalOpen}
                 products={productItems}
-                currencySymbol={currencySymbol}
+                currencySymbol={orderSymbol}
                 onClose={() => closeProductsModal()}
             />
         </>

+ 21 - 42
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -7,7 +7,10 @@ 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 {
+    airwallexManager,
+    useAirwallexCardState
+} from "@/lib/Airwallex/useAirwallexCard";
 import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
 import { OrderDetails } from "@/types/customer/type";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
@@ -41,14 +44,14 @@ export function PaymentMethodContinueTpPay({
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
     const {loadSdk} = usePaymentSDKContext();
-    const {store} = useConfig();
+    const {store,currencies} = useConfig();
     const currentCurrencyCode  = store.currentCurrency;
 
-    const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
-        cardNumber: 'airwallex_cardNumber', 
-        cvc: 'airwallex_cvc', 
-        expiry: 'airwallex_expiry'
-    });
+    const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
+    const orderCurrencyCode = orderCurrency?.code || currentCurrencyCode;
+
+
+    const airwallexCardState = useAirwallexCardState();
     const [isPay, setIsPay] = useState(false);
     const shippingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
     const billingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
@@ -132,32 +135,12 @@ export function PaymentMethodContinueTpPay({
 
     const validateAirwallexCard = () => {
 
-        if(!airwallexCardState.cardNumberReady) {
-            showToast('Card number input has not ready. Please wait a minute.', 'danger');
-            return false;
+        const res = airwallexManager.validateCard();
+        if(res.msg) {
+            showToast(res.msg, 'danger');
         }
-        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;
+        return res.valid;
+       
     };
 
     const cardAirwallexRepay = async () => { 
@@ -173,7 +156,7 @@ export function PaymentMethodContinueTpPay({
             
             if(!res.error && res.data !== null) {
                 // 创建订单成功
-                const cardNumberElement = getCardElements().cardNumber;
+                const cardNumberElement = airwallexManager.getCardNumberElement();
                 const paymentIntentIdClientSecret = res.data.gatewayOrderId.split(':');
                 const paymentIntentId = paymentIntentIdClientSecret[0];
                 const paymentIntentClientSecret = paymentIntentIdClientSecret[1];
@@ -223,13 +206,13 @@ export function PaymentMethodContinueTpPay({
                 const afterpayToken = res.data.gatewayOrderId;
                 const webOrderId = res.data.orderId;
                 let countryCode = "US";
-                if(currentCurrencyCode === "CAD") {
+                if(orderCurrencyCode === "CAD") {
                     countryCode = "CA";
-                } else if(currentCurrencyCode === "AUD") {
+                } else if(orderCurrencyCode === "AUD") {
                     countryCode = "AU";
-                } else if(currentCurrencyCode === "NZD") {
+                } else if(orderCurrencyCode === "NZD") {
                     countryCode = "NZ";
-                } else if(currentCurrencyCode === "GBP") {
+                } else if(orderCurrencyCode === "GBP") {
                     countryCode = "GB";
                 }
                 window.AfterPay.initialize({countryCode: countryCode});
@@ -484,11 +467,7 @@ export function PaymentMethodContinueTpPay({
                                     <div className="w-full mt-3"
                                         style={{display: airwallexCardElementShow ? 'block' : 'none'}}
                                     >
-                                        <AirwallexCardInput 
-                                            cardNumberErrorMsg={airwallexCardState.cardNumberErrorMsg}
-                                            expiryErrorMsg={airwallexCardState.expiryErrorMsg}
-                                            cvcErrorMsg={airwallexCardState.cvcErrorMsg}
-                                        />
+                                        <AirwallexCardInput />
                                     </div>
                                 }                          
                             </li>)

+ 4 - 0
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx

@@ -31,6 +31,8 @@ export default function CheckoutGiftCardGate({
     const { showToast } = useCustomToast();
     const {saveCheckoutCart} = useSaveCheckoutCart();
 
+    const amount = Number(formattedGiftcardAmount.replace(/^\D+/ig,''));
+
     const getMyGiftCard = useCallback(async (page: number) => {
         // overlayLoading.start();
         setLoadingList(true);
@@ -109,9 +111,11 @@ export default function CheckoutGiftCardGate({
                
                 
                 <div className="flex items-center">
+                    {amount > 0 &&
                     <span className="flex items-center text-ly-14 px-2 flex-none h-6 bg-ly-gold mr-2">
                         -{formattedGiftcardAmount}
                     </span>
+                    }
                     <button className="w-6 h-6 flex-none" onClick={() => {toggleModal(true)}}>
                         <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="0" y="0" width="16" height="16"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M6.32031 4.27051L10.4202 8.37036L6.32031 12.4702"></path></svg>
                     </button>

+ 1 - 4
src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx

@@ -231,10 +231,7 @@ export default function PaypalApplepayButton({
 
     const tips = isLoading || !isHydrated  ? (
             <p className="text-ly-12 text-center">
-                Applepay is loading1...
-                {String(isLoading)}
-
-                /{String(isHydrated)}
+                Applepay is loading...
             </p>
         ) : eligibilityError ? (
             <p className="text-ly-errorcolor text-ly-12 text-center">

+ 24 - 12
src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx

@@ -1,29 +1,41 @@
 "use client";
 
-export default function AirwallexCardInput({ 
-    cardNumberErrorMsg,
-    expiryErrorMsg,
-    cvcErrorMsg,
-} : {
-    cardNumberErrorMsg: string;
-    expiryErrorMsg: string;
-    cvcErrorMsg: string;
-}) {
+import { useId } from "react";
+import {
+    useAirwallexCard,
+    useAirwallexCardState
+} from "@/lib/Airwallex/useAirwallexCard";
+
+export default function AirwallexCardInput() {
+    const id = useId();
+    const {
+        cardNumberErrorMsg,
+        expiryErrorMsg,
+        cvcErrorMsg
+    } = useAirwallexCardState();
+    const cardNumberId = `airwallex_cardNumber_${id}`;
+    const cvcId = `airwallex_cvc_${id}`;
+    const expiryId = `airwallex_expiry_${id}`;
+    useAirwallexCard({
+        cardNumber: cardNumberId, 
+        cvc: cvcId, 
+        expiry: expiryId
+    });
 
     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>
+                <div id={cardNumberId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></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>
+                    <div id={expiryId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></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>
+                    <div id={cvcId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></div>
                     {cvcErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{cvcErrorMsg}</p>}
                 </div>
             </div>

+ 6 - 62
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,15 +1,11 @@
 "use client";
 
-import clsx from "clsx";
 import Image from "next/image";
 import { Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import { CheckoutPaymentMethod } from "@/types/checkout/type";
-import { AirwallexCardElements } from "@/lib/Airwallex/airwallexInit";
-import { useCustomToast } from "@/utils/hooks/useToast";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
-import {useAirwallexCard} from "@/lib/Airwallex/useAirwallexCard";
-import LoadingPaymentMethod from "./LoadingPaymentMethod";
+import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
 
 /**
  * 关于如何确定默认选中哪个支付方式:
@@ -19,33 +15,21 @@ export interface RefPaymentMethodsHandle {
   getSelectPaymentMethod: () => string;
   getSelectPaymentTitle: () => string;
   validatePaymentMethod: () => Promise<boolean>;
-  getAirwallexCardElements: () => AirwallexCardElements;
-  validateAirwallexCard: () => boolean;
 }
 export function PaymentMethodCheckout({
     ref,
     paymentMethods,
     selectedPaymentMethod,
-    paymentMethodLoading,
-    loadError,
     onPaymentMethodChange,
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
-    paymentMethodLoading: boolean;
-    loadError: string | null;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 
-    const { showToast } = useCustomToast();
 
-
-    const {airwallexCardState,getCardElements} = useAirwallexCard(!paymentMethodLoading,{
-        cardNumber: 'airwallex_cardNumber', 
-        cvc: 'airwallex_cvc', 
-        expiry: 'airwallex_expiry'
-    });
+    const airwallexCardState = useAirwallexCardState();
   
     const {
         register,
@@ -100,54 +84,18 @@ export function PaymentMethodCheckout({
             // 触发校验
             validatePaymentMethod: () => {
                 return trigger();
-            },
-            // 获取airwallex card 支付创建的elements
-            getAirwallexCardElements: () => {
-                return getCardElements();
-            },
-            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;
             }
         }
         
-    },[getCardElements,paymentMethods,airwallexCardState]);
+    },[paymentMethods]);
 
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
     
     return (<>
 
-        { paymentMethodLoading && <LoadingPaymentMethod /> }
-        { (!paymentMethodLoading && loadError) && <p className="mt-3 text-red-500 text-ly-12">{loadError}</p>}
-        <form name="shipping method" className={clsx("w-full", {
-            "hidden": (paymentMethodLoading || loadError)
-        })}>
+   
+        <form name="shipping method" className="w-full">
         
             <div className="mt-3 w-full">
                 <ul className="w-full">
@@ -183,11 +131,7 @@ export function PaymentMethodCheckout({
                                     <div className="w-full mt-3"
                                         style={{display: airwallexOtherInfoShow ? 'block' : 'none'}}
                                     >
-                                        <AirwallexCardInput 
-                                            cardNumberErrorMsg={airwallexCardState.cardNumberErrorMsg}
-                                            expiryErrorMsg={airwallexCardState.expiryErrorMsg}
-                                            cvcErrorMsg={airwallexCardState.cvcErrorMsg}
-                                        />
+                                        <AirwallexCardInput />
                                     </div>
                                 }
                             </li>

+ 30 - 8
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -16,14 +16,36 @@ export default function PromotionsDetails({cartData}:{
                     <span>Subtotal</span>
                     <span>{cartData.formattedSubtotal}</span>
                 </li>
-                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                    <span>Shipping Method</span>
-                    <span>{cartData.formattedShippingAmount}</span>
-                </li>
-                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                    <span>Gift Card</span>
-                    <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
-                </li>
+                {cartData.shippingAmount !== 0 &&
+                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                        <span>Shipping Method</span>
+                        <span>{cartData.formattedShippingAmount}</span>
+                    </li>
+                }
+                {cartData.vipPlusAmount !== 0 &&
+                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                        <span>Vip Plus Fee</span>
+                        <span>{cartData.formattedVipPlusAmount}</span>
+                    </li>
+                }
+                {cartData.discountAmount !== 0 &&
+                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                        <span>Coupon</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedDiscountAmount}</span>
+                    </li>
+                }
+                {cartData.vipDiscountAmount !== 0 &&
+                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                        <span>Plus</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedVipDiscountAmount}</span>
+                    </li>
+                }
+                {cartData.giftcardAmount !== 0 &&
+                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                        <span>Gift Card</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                    </li>
+                }
                 <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
                     <span>Grand Total</span>
                     <span>{cartData.formattedGrandTotal}</span>

文件差異過大導致無法顯示
+ 223 - 0
src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx


+ 1 - 1
src/app/(checkout)/checkout/page.tsx

@@ -33,7 +33,7 @@ export default async function CheckoutPage() {
     const cartDetails = cartDetailsRes.createReadCart.readCart;
     return (
         <>
-            <CheckoutWrapper key={cartDetails.id}
+            <CheckoutWrapper key={cartDetails.grandTotal}
                 cartDetailData={cartDetails}
                 loginEmail={session?.user?.email || ""}
             />

+ 1 - 1
src/app/(public)/_components/HomeBestSellers.tsx

@@ -54,7 +54,7 @@ const HomeBestSellers = ({ bestSellers }: HomeBestSellersProps) => {
                     className="w-full h-auto object-cover rounded-sm"
                   />
                   {/* 产品标题 */}
-                  <Link href={"/aaaa"||""} className="text-sm text-gray-800 truncate">
+                  <Link href={"/wiggins-ready-go-250-density-popping-curly-hd-lace-front-wigs-medium-length-human-hair-wigs.html"} className="text-sm text-gray-800 truncate">
                     {product.name}
                   </Link>
                   {/* 评分 + 评论数 */}

+ 37 - 17
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -52,7 +52,7 @@ export default function OrderDetailWrapper({
     const productItems = orderDetailRes?.items?.edges.map(item => {
         return item.node;
     }) || [];
-    const currency = currencies?.find(item => item.code === orderDetailRes?.orderCurrencyCode) || null;
+    const currency = currencies.find(item => item.code === orderDetailRes?.orderCurrencyCode) || null;
 
     useEffect(() => {
         // 如果是游客且支付成功,删除cookie里的GUEST_CART_TOKEN GUEST_CART_ID
@@ -176,22 +176,42 @@ export default function OrderDetailWrapper({
                     <p className="text-ly-12 leading-ly-20">Subtotal</p>
                     <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.subTotal}</p>
                 </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Shipping</p>
-                    <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.shippingAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Tax</p>
-                    <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.taxAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Discount</p>
-                    <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.discountAmount}</p>
-                </div>
-                <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
-                    <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.giftcardAmount}</p>
-                </div>
+                {!!orderDetailRes?.shippingAmount &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Shipping</p>
+                        <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.shippingAmount}</p>
+                    </div>
+                }
+                {!!orderDetailRes?.taxAmount &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Tax</p>
+                        <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.taxAmount}</p>
+                    </div>
+                }
+                {(orderDetailRes?.vipPlusAmount !== '0.00' && orderDetailRes?.vipPlusAmount !== null) &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Plus Fee</p>
+                        <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.vipPlusAmount}</p>
+                    </div>
+                }
+                {!!orderDetailRes?.discountAmount &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Discount</p>
+                        <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.discountAmount}</p>
+                    </div>
+                }
+                {(orderDetailRes?.giftcardAmount !== '0.00' && orderDetailRes?.giftcardAmount !== null) &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                        <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.giftcardAmount}</p>
+                    </div>
+                }
+                {(orderDetailRes?.vipDiscountAmount !== '0.00' && orderDetailRes?.vipDiscountAmount !== null) &&
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Plus Save</p>
+                        <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.vipDiscountAmount}</p>
+                    </div>
+                }
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>

+ 0 - 3
src/app/api/gift/add/route.ts

@@ -1,10 +1,8 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
 
 export async function POST(req: NextRequest) {
     try {
-        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
         const params = await req.json();
         const response = await restApiFetch<{
             data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
@@ -16,7 +14,6 @@ export async function POST(req: NextRequest) {
             method:'POST',
             cache:'no-store',
             variables: params,
-            guestToken: authorizationToken,
         });
         // 打印后端原始返回结构
         console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));

+ 2 - 5
src/app/api/gift/lists/route.ts

@@ -1,15 +1,12 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
 import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
-export async function GET(req: NextRequest) {
+export async function GET() {
     try {
-        const guestToken = getAuthToken(req);
         const response = await restApiFetch<FetchWrap<GiftListBody>>({
             api: `/gift/lists`,
             method:'GET',
             cache:'no-store',
-            guestToken,
         });
         return NextResponse.json(response.body,{
             status: response.status,

+ 0 - 3
src/app/api/gift/my-gift-cards/route.ts

@@ -1,10 +1,8 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
 import type { GiftCardRespBody } from '@/types/api/gift/my-gift-cards';
 export async function GET(req: NextRequest) {
     try {
-        const guestToken = getAuthToken(req);
 
         const searchParams = req.nextUrl.searchParams;
         const page = searchParams.get('page') || '1';
@@ -21,7 +19,6 @@ export async function GET(req: NextRequest) {
             api: `/gift/my-gift-cards`,
             method:'GET',
             cache:'no-store',
-            guestToken,
         });
         return NextResponse.json(response.body,{
             status: response.status,

+ 6 - 9
src/app/api/graphql/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { bagistoFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
 import {
     CREATE_ADD_PRODUCT_IN_CART,
     REMOVE_CART_ITEM,
@@ -56,9 +55,8 @@ interface FetchOption  {
 }
 
 // 需要authorization的operation
-function authorizationOperations(body: Record<string, any>,req:NextRequest): FetchOption {
+function authorizationOperations(body: Record<string, any>): FetchOption {
     const { operationName, variables } = body;
-    const guestToken = getAuthToken(req);
     const query = ALLOWED_OPERATIONS[operationName];
     const finalVariables = variables;
  
@@ -66,21 +64,20 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
         query,
         variables: finalVariables,
         cache: "no-store",
-        guestToken,
-        operationName
+        // operationName
     }
 }
 
 
 // 不需要authorization的operation
 function notAuthorizationOperations(body: Record<string, any>): FetchOption {
-    const { operationName, query: bodyGraphqlQuery, variables } = body;
-    const query = bodyGraphqlQuery;
+    const { query, variables } = body;
+    // const query = bodyGraphqlQuery;
     return {
         query,
         variables,
         cache: "no-store",
-        operationName
+        // operationName
     }
 }
 
@@ -97,7 +94,7 @@ export async function POST(req: NextRequest) {
         }
         let fetchOption: FetchOption = notAuthorizationOperations(body);
         if(ALLOWED_OPERATIONS[operationName]) {
-            fetchOption = authorizationOperations(body,req);
+            fetchOption = authorizationOperations(body);
         }
 
         

+ 5 - 0
src/app/globals.css

@@ -77,6 +77,8 @@ html {
   --color-ly-lightgray: #f9f9f9;
   --color-ly-middlegray: #f2f2f2;
   --color-ly-gray: #d3d3d3; /*可被映射为text-ly-gray,bg-ly-gray等相关的颜色类目*/
+  --color-ly-deepgray: #ACACAC;
+  --color-ly-gold: #ffd700;
   --color-ly-gold: #ffd700;
   --color-ly-green: #01754a;
   --color-ly-middlegreen: #036141;
@@ -262,6 +264,9 @@ body {
   .ly-thin-shadow{
       box-shadow: 0 0px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
   }
+  .ly-vipplus-bg{
+    background: linear-gradient(128.05deg, rgba(250, 231, 200, 1) 0%, rgba(249, 255, 235, 1) 27.29%, rgba(242, 215, 172, 1) 64.42%, rgba(232, 202, 153, 1) 100%);
+  }
 }
 
 apple-pay-button {

+ 4 - 3
src/components/cart/CartModal.tsx

@@ -9,7 +9,7 @@ import { Price } from "../theme/ui/Price";
 import CloseCart from "../common/icons/cart/CloseCart";
 import { DeleteItemButton } from "../common/icons/cart/DeleteItemButton";
 import { EditItemQuantityButton } from "../common/icons/cart/EditItemQuantityButton";
-import { useCartDetail } from "@utils/hooks/useCartDetail";
+import { useAppSelector } from "@/store/hooks";
 import Image from "next/image";
 import { NOT_IMAGE } from "@utils/constants";
 import LoadingDots from "@components/common/icons/LoadingDots";
@@ -49,8 +49,9 @@ export default function CartModal({
   const finalOnOpen = isControlled ? onOpen : internalOnOpen;
   const finalOnClose = isControlled ? onClose : internalOnClose;
 
-  const { isLoading, cartData: cartDetail } = useCartDetail();
-  // const cartDetail = useAppSelector((state) => state.cartDetail);
+  // const { isLoading, cartData: cartDetail } = useCartDetail();
+  const cartState= useAppSelector((state) => state.cartDetail);
+  const {cart:cartDetail, loading: isLoading} = cartState;
   const cart = Array.isArray(cartDetail?.items?.edges)
     ? cartDetail?.items?.edges
     : [];

文件差異過大導致無法顯示
+ 482 - 0
src/components/common/LoginModal/LoginModal.tsx


+ 4 - 5
src/components/common/button/ReviewButton.tsx

@@ -1,13 +1,12 @@
-import { IS_GUEST } from "@utils/constants";
-import { getCookie } from "@utils/cookie-tools";
+
+import { getIsGuest } from "@utils/cookie-tools";
 import { useRouter } from "next/navigation";
 
 export const ReviewButton = ({ setShowForm, className }: { setShowForm: () => void, className?: string }) => {
-    const IsGuest = getCookie(IS_GUEST);
+    const IsGuest = getIsGuest();
     const router = useRouter();
     const handleAddReview = () => {
-        if (IsGuest === "true" || IsGuest === null) {
-            //   setShowForm();
+        if (IsGuest) {
             router.push("/customer/login");
         } else {
             setShowForm();

+ 14 - 13
src/components/customer/LoginForm.tsx

@@ -12,12 +12,16 @@ import InputText from "@components/common/form/Input";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useMergeCart } from "@utils/hooks/useMergeCart";
 
-import { getCookie, setCookie } from "@/utils/cookie-tools";
-
-import { useAppDispatch, useAppSelector } from "@/store/hooks";
-import { setUser } from "@/store/slices/user-slice";
+import { 
+  setCookie, 
+  getGuestCartToken,
+  getGuestCartId,
+  deleteGuestCookie
+} from "@/utils/cookie-tools";
+
+import { useAppSelector } from "@/store/hooks";
 import { useCartDetail } from "@utils/hooks/useCartDetail";
-import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
+import { IS_GUEST } from "@/utils/constants";
 
 type LoginFormInputs = {
   username: string;
@@ -26,7 +30,6 @@ type LoginFormInputs = {
 
 export default function LoginForm() {
   const router = useRouter();
-  const dispatch = useAppDispatch();
   const { showToast } = useCustomToast();
   const { getCartDetail } = useCartDetail()
   const { mergeCart } = useMergeCart();
@@ -43,8 +46,8 @@ export default function LoginForm() {
   const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
     try {
       // First, handle cart merging before sign in
-      const guestCartId = getCookie(GUEST_CART_ID);
-      const guestCartToken = getCookie(GUEST_CART_TOKEN);
+      const guestCartId = getGuestCartId();
+      const guestCartToken = getGuestCartToken();
 
       /**
        * @todo 使用 signInAuth 重写
@@ -72,9 +75,6 @@ export default function LoginForm() {
         console.error('userToken is required');
       }
 
-      if (session?.user) {
-        dispatch(setUser(session.user as any));
-      }
 
       // Only merge cart if user had a guest cart before login
       if (userToken && guestCartId && guestCartToken) {
@@ -86,13 +86,14 @@ export default function LoginForm() {
         } catch (err) {
           console.error("mergeCart failed:", err);
         }
-        setCookie(GUEST_CART_TOKEN, userToken);
+        // 登录成功后不应该保存用户的token
         setCookie(IS_GUEST, "false");
+        deleteGuestCookie();
         await getCartDetail();
       } else if (userToken) {
         // User logged in without a guest cart, just set the token
-        setCookie(GUEST_CART_TOKEN, userToken);
         setCookie(IS_GUEST, "false");
+        deleteGuestCookie();
       }
       setTimeout(() => {
         router.push("/");

+ 9 - 10
src/components/customer/credentials/CredentialModal.tsx

@@ -3,7 +3,7 @@
 import { useDisclosure } from "@heroui/use-disclosure";
 import { AnimatePresence, motion } from "framer-motion";
 import clsx from "clsx";
-import { signOut } from "next-auth/react";
+import { useSession, signOut } from "next-auth/react";
 import Link from "next/link";
 import { Avatar } from "@heroui/avatar";
 import { useForm } from "react-hook-form";
@@ -14,11 +14,13 @@ import OpenAuth from "../OpenAuth";
 import { isObject } from '@/utils/type-guards';
 import LoadingDots from "@components/common/icons/LoadingDots";
 import { logoutAction } from "@utils/actions";
-import { useAppDispatch, useAppSelector } from "@/store/hooks";
-import { clearUser } from "@/store/slices/user-slice";
+import { useAppDispatch } from "@/store/hooks";
 import { clearCart } from "@/store/slices/cart-slice";
-import { setCookie, deleteCookie } from "@utils/cookie-tools";
-import { IS_GUEST,GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
+import { 
+  setCookie, 
+  deleteGuestCookie
+} from "@utils/cookie-tools";
+import { IS_GUEST } from "@/utils/constants";
 
 export default function CredentialModal({
   children,
@@ -59,8 +61,7 @@ export default function CredentialModal({
     formState: { isSubmitting },
   } = useForm();
 
-  const { user } = useAppSelector((state) => state.user);
-  const session = { user };
+  const {data: session} = useSession();
 
   const onSubmit = async () => {
     try {
@@ -75,10 +76,8 @@ export default function CredentialModal({
         redirect: false,
       });
 
-      deleteCookie(GUEST_CART_TOKEN);
-      deleteCookie(GUEST_CART_ID);
+      deleteGuestCookie(); // 这里本质是要删除登录用户的token
       setCookie(IS_GUEST, 'true');
-      dispatch(clearUser());
       dispatch(clearCart());
       showToast("You are logged out successfully!", "success");
       setTimeout(() => {

+ 18 - 9
src/components/home/HomeImageBanner.tsx

@@ -1,12 +1,12 @@
 "use client";
 
+import { useRouter } from 'next/navigation';
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch";
 import makeClient from "@/lib/ApolloClientBrowser";
-
-import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON } from "@/graphql";
-import { useMutation, useLazyQuery } from "@apollo/client/react";
+import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON, GET_COUNTRY_STATES} from "@/graphql";
+import { useMutation, useLazyQuery, useQuery } from "@apollo/client/react";
 import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 import { useCustomToast } from "@/utils/hooks/useToast";
 // interface ImageCarouselProps {
@@ -19,14 +19,15 @@ import { useCustomToast } from "@/utils/hooks/useToast";
 //     };
 // }
 export default function HomeImageBanner() {
+  const router = useRouter();
   const { showToast } = useCustomToast();
   const apolloClient = makeClient();
-  // const res = useQuery(GET_COUNTRY_STATES, {
-  //         variables: {
-  //             countryId: 1000
-  //         }
-  //     });
-  //     console.log('query state --- res', res);
+  const res = useQuery(GET_COUNTRY_STATES, {
+          variables: {
+              countryId: 1000
+          }
+      });
+      console.log('query state --- res', res);
   const ttt = async () => {
     try {
       const res = await apolloClient.mutate({
@@ -118,6 +119,12 @@ const [getContent] = useLazyQuery(GET_FILTER_ATTRIBUTES,{
                 console.log('testApplyCoupon error ---- ',err);
             }
         };
+
+
+        const toggleLoginModal = () => {
+          // openLoginModal();
+          router.refresh();
+        };
   
   return (
     <>
@@ -141,6 +148,8 @@ const [getContent] = useLazyQuery(GET_FILTER_ATTRIBUTES,{
       <SwiperSlide>Slide 4</SwiperSlide>
     </Swiper>
     <CurrencySwitch/>
+
+    <p><button onClick={() => toggleLoginModal()}>show Login Modal</button></p>
     </>
   )
 }

+ 59 - 18
src/components/theme/ui/CommonModal.tsx

@@ -10,6 +10,7 @@ import Portal from "@/components/common/portal/Portal";
  */
 export default function CommonModal({ 
     isOpen,
+    destroyOnClose = false,
     contentClassName,
     clickOutsideClose,
     onClose,
@@ -18,6 +19,7 @@ export default function CommonModal({
     footer
 }: {
     isOpen: boolean;
+    destroyOnClose?: boolean;
     contentClassName?: string;
     clickOutsideClose?: boolean;
     onClose: (e: boolean) => void;
@@ -29,6 +31,7 @@ export default function CommonModal({
     // outing - 隐藏动画中
     // hidden - 隐藏
     const [rootVisible, setRootVisible] = useState<'show' | 'outing' | 'hidden'>('hidden');
+    const [mounted, setMounted] = useState(false);
     
     const cssInit = useRef(false);
     const rootRef = useRef<HTMLDivElement>(null);
@@ -38,33 +41,65 @@ export default function CommonModal({
     const [rootPddingBottom, setRootPddingBottom] = useState(0);
     const [rootPddingTop, setRootPddingTop] = useState(0);
 
-    const baseClassNname = "absolute box-border w-full bg-white transition-transform duration-300 ease-out";
-    let applyClassName = "bottom-0 left-0 h-17/20";
+    const baseClassNname = "absolute box-border transition-transform duration-300 ease-out";
+    let applyClassName = "bottom-0 left-0 h-17/20 w-full bg-white rounded-t-xl";
     if(contentClassName) {
         applyClassName = contentClassName;
     }
 
     useLayoutEffect(() => {
         const original = document.documentElement.style.overflow;
-        if(isOpen) {
+        if(isOpen){
+            // destroy模式重新创建DOM
+            if(destroyOnClose){
+                // eslint-disable-next-line react-hooks/set-state-in-effect
+                setMounted(true);
+            }
             document.documentElement.style.overflow = "hidden";
-        } else {
+            requestAnimationFrame(()=>{
+                setRootVisible("show");
+            });
+        }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');
+            if(destroyOnClose){
+                // 先播放关闭动画
+                if(mounted){
+                    // eslint-disable-next-line react-hooks/set-state-in-effect
+                    setRootVisible("outing");
+                }
+            }else{
+                // 保留DOM
+                if(cssInit.current){
+                    // eslint-disable-next-line react-hooks/set-state-in-effect
+                    setRootVisible("outing");
+                }
             }
-            
+
         }
+        return ()=>{ document.documentElement.style.overflow = original; };
+    },[isOpen, destroyOnClose, mounted]);
 
-        return () => { document.documentElement.style.overflow = original; };
-    },[isOpen]);
+    // useLayoutEffect(() => {
+    //     const original = document.documentElement.style.overflow;
+    //     if(isOpen) {
+    //         document.documentElement.style.overflow = "hidden";
+    //     } 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');
+    //         }
+            
+    //     }
+
+    //     return () => { document.documentElement.style.overflow = original; };
+    // },[isOpen]);
     // 当弹窗完全显示后计算 footer 高度
     useLayoutEffect(() => {
         if (rootVisible === 'show' && !cssInit.current && footerRef.current && headerRef.current) {
@@ -85,12 +120,18 @@ export default function CommonModal({
     const handlerAnimationEnd = (e: React.AnimationEvent<HTMLDivElement>) => {
         if(e.animationName === "fade-out") {
             setRootVisible('hidden');
-        } 
-        
+            if(destroyOnClose){
+                setMounted(false);
+                cssInit.current=false;
+            }
+        }
     }
     // 初始化时是隐藏状态
     // fade-in的时候display block + fade-in
     // fade-out的时候需要先加fade-out 类名,动画效果结束之后才能display none 
+    if(destroyOnClose && !mounted){
+        return null;
+    }
     return (
         <Portal>
             <div 

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

@@ -1,5 +1,7 @@
-import React from "react";
+"use client";
 
+import React from "react";
+type InputType = 'text' | 'email';
 function InputText({
     type = 'text',
     placeholder,
@@ -10,7 +12,7 @@ function InputText({
     ref
 }: {
     // 用于react-hook-form时,推荐使用useForm的 defaultValues 对整个表单设置默认值
-    type?: 'text' | 'email' | 'password';
+    type?: InputType;
     placeholder: string;
     // 用于react-hook-form时,React 事件处理器类型定义中的“双变(bivariance)”特性 和返回值void的兼容性,使typescript没有提示类型错误
     onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;

+ 102 - 0
src/components/theme/ui/LyDropDown.tsx

@@ -0,0 +1,102 @@
+"use client";
+
+import { useState, useRef, useLayoutEffect, ReactNode, useEffect } from "react";
+import clsx from "clsx";
+
+
+export function LyDropDown({
+    showDropDown,
+    children,
+    onClose
+}: {
+    showDropDown: boolean;
+    children: ReactNode;
+    onClose: () => void;
+}) {
+    const listRef = useRef<HTMLDivElement>(null);
+    const rootRef = useRef<HTMLDivElement>(null);
+    const [listPosition, setListPosition] = useState({
+        top: '0',
+        transform: 'translate3d(0, 0%, 0)', 
+    });
+
+    useLayoutEffect(() => {
+        if (listRef.current && rootRef.current && showDropDown) {
+            const { height } = listRef.current.getBoundingClientRect();
+            // setListHeight(height);
+            console.log('Measured tooltip height: ' + height);
+
+            const triggerRect = rootRef.current.getBoundingClientRect();
+            if(triggerRect.bottom + height > window.innerHeight) { // 显示在上方
+                setListPosition({
+                    top: '0',
+                    transform: 'translate3d(0, -100%, 0)',
+                });
+            } else { // 显示在下方
+                setListPosition({
+                    top: `${triggerRect.height}px`,
+                    transform: `translate3d(0, 0%, 0)`
+                });
+            }
+            
+        }
+    }, [showDropDown]);
+
+
+    // 点击外部关闭
+    useEffect(() => {
+
+        if (!showDropDown) return;
+
+
+        function handleClickOutside(event: MouseEvent) {
+
+            const target = event.target as Node;
+
+            if (
+                rootRef.current &&
+                !rootRef.current.contains(target)
+            ) {
+                onClose();
+            }
+        }
+
+
+        // 放到 document 捕获阶段
+        document.addEventListener(
+            "pointerdown",
+            handleClickOutside,
+            true
+        );
+
+
+        return () => {
+            document.removeEventListener(
+                "pointerdown",
+                handleClickOutside,
+                true
+            );
+        };
+
+    }, [showDropDown, onClose]);
+
+
+
+    return (
+        <>
+            <div className="w-full relative" ref={rootRef}>
+
+                <div ref={listRef} className={clsx(
+                    "absolute w-full max-h-48 overflow-auto bg-white ly-thin-shadow border border-ly-gray rounded-md z-1",
+                    showDropDown  ? "visible pointer-events-auto" : "invisible pointer-events-none"
+                )}
+                    onClick={(e) => { e.stopPropagation(); }}
+                    style={listPosition}
+                >
+                    {children}
+                    
+                </div>
+            </div>
+        </>
+    );
+}

+ 74 - 0
src/components/theme/ui/PasswordInput.tsx

@@ -0,0 +1,74 @@
+"use client";
+
+import React, {useState} from "react";
+import clsx from "clsx";
+type InputType = 'text' | 'password';
+function PasswordInput({
+    placeholder,
+    onChange,
+    onBlur,
+    name,
+    error,
+    ref
+}: {
+    // 用于react-hook-form时,推荐使用useForm的 defaultValues 对整个表单设置默认值
+    placeholder: string;
+    // 用于react-hook-form时,React 事件处理器类型定义中的“双变(bivariance)”特性 和返回值void的兼容性,使typescript没有提示类型错误
+    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    onBlur: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    name: string;
+    error?: string;
+    ref?: React.Ref<HTMLInputElement>;
+}) {
+    const [inputType, setInputType] = useState<InputType>('password');
+    const changeType = () => {
+        if(inputType === 'text') {
+            setInputType('password');
+        }
+         if(inputType === 'password') {
+            setInputType('text');
+        }
+    };
+    return (
+        <div className="w-full">
+            <div className="relative w-full">
+                <input className="ly-input w-full" 
+                    type={inputType}
+                    placeholder={placeholder}
+                    onChange={onChange}
+                    onBlur={onBlur}
+                    name={name}
+                    ref={ref}
+                />
+                
+                <div className="absolute top-1/2 right-3 -translate-y-1/2 w-4 h-4" onClick={changeType}>
+                    <svg className={clsx("block w-full h-full icon-eyehide",{
+                        "hidden": inputType !== 'password'
+                    })} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none">
+                        <path fill="none" stroke="#000000" strokeWidth="1.5" strokeLinecap="round" d="M3.28587 6C2.07945 7 1.33325 8 1.33325 8C1.33325 8 4.31802 12 7.99992 12C8.45655 12 8.90245 11.9385 9.33325 11.8307M6.67719 4.16667C7.10472 4.06053 7.54705 4 7.99992 4C11.6818 4 14.6666 8 14.6666 8C14.6666 8 13.9204 9 12.714 10">
+                        </path>
+                        <path fill="none" stroke="#000000" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round" d="M6.77132 6.87372C6.49929 7.17032 6.33325 7.56575 6.33325 7.99995C6.33325 8.92042 7.07945 9.66662 7.99992 9.66662C8.45415 9.66662 8.86595 9.48489 9.16658 9.19018">
+                        </path>
+                        <path fill="none"  stroke="#000000"strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" d="M14 14L2 2">
+                        </path>
+                    </svg> 
+                    <svg className={clsx("block w-full h-full icon-eyeshow",{
+                        "hidden": inputType !== 'text'
+                    })} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none">
+                        <path stroke="#000000" d="M7.99992 12C11.6818 12 14.6666 8 14.6666 8C14.6666 8 11.6818 4 7.99992 4C4.31802 4 1.33325 8 1.33325 8C1.33325 8 4.31802 12 7.99992 12Z" fill="none" strokeWidth="1.5">
+                        </path>
+                        <path stroke="#000000" d="M7.99992 9.66665C8.92039 9.66665 9.66658 8.92045 9.66658 7.99998C9.66658 7.07951 8.92039 6.33331 7.99992 6.33331C7.07945 6.33331 6.33325 7.07951 6.33325 7.99998C6.33325 8.92045 7.07945 9.66665 7.99992 9.66665Z" fill="none" strokeWidth="1.5" strokeLinejoin="round">
+                        </path>
+                    </svg>
+                </div>
+
+            </div>
+            
+            {error && <p className="text-red-500 text-ly-12">{error}</p>}
+        </div> 
+    );
+}
+// 设置 displayName,便于调试
+PasswordInput.displayName = 'PasswordInput';
+
+export default PasswordInput;

+ 6 - 6
src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx

@@ -46,9 +46,9 @@ interface PhoneNumberInputProps {
     name: string;
     error?: string;
     ref?: React.Ref<HTMLInputElement>;
-    countryCode: string;//父组件表单国家字段
-    onHasUserSelectedPhoneCode: () => void;
-    hasUserSelectedPhoneCode: boolean;
+    countryCode?: string;//父组件表单国家字段
+    onHasUserSelectedPhoneCode?: () => void;
+    hasUserSelectedPhoneCode?: boolean;
 }
 function PhoneNumberInput({
     value,
@@ -58,7 +58,7 @@ function PhoneNumberInput({
     name,
     error,
     ref,
-    countryCode,
+    countryCode = 'US',
     onHasUserSelectedPhoneCode,
     hasUserSelectedPhoneCode
 }: PhoneNumberInputProps) {
@@ -142,7 +142,7 @@ function PhoneNumberInput({
         setSelectedPhoneCode(pCode);
         setSelectedCountryForPhoneCode(ccode);
         
-        if(!hasUserSelectedPhoneCode) {
+        if(!hasUserSelectedPhoneCode && onHasUserSelectedPhoneCode) {
             onHasUserSelectedPhoneCode(); // 标记为用户手动选择过
         }
         setShowCodeList(false);
@@ -169,7 +169,7 @@ function PhoneNumberInput({
                 ref={ref}
             />
             <div className="flex gap-4 box-border border-1 border-ly-inputborder">
-                <div className="flex-auto px-3.75" onClick={handleShowCodeList}>
+                <div className="px-3.75" onClick={handleShowCodeList}>
                     <div className="h-11.5 text-ly-12 flex items-center">
                         
                         <Image alt="china flag" width={24} height={18} className="w-6 h-4.5 mr-2"

+ 1 - 0
src/graphql/cart/mutations/AddProductToCart.ts

@@ -50,6 +50,7 @@ export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData> = gql`
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

+ 70 - 0
src/graphql/cart/mutations/CreateApplyCoupon.ts

@@ -0,0 +1,70 @@
+import { gql, TypedDocumentNode } from "@apollo/client";
+import { CreateApplyCouponData } from "@/types/cart/type";
+
+export const CREATE_APPLY_COUPON: TypedDocumentNode<CreateApplyCouponData,{couponCode: string;}> = gql`
+    mutation createApplyCoupon (
+        $couponCode: String!
+    ) {
+        createApplyCoupon(input: {
+            couponCode: $couponCode
+        }) {
+                applyCoupon {
+                    id
+                    appliedTaxes
+                    itemsQty
+                    isGuest
+                    itemsCount
+                    items {
+                    edges {
+                        node {
+                        id
+                        cartId
+                        productId
+                        name
+                        price
+                        baseImage
+                        sku
+                        quantity
+                        type
+                        productUrlKey
+                        canChangeQty
+                        }
+                    }
+                    }
+                    isVip
+                    vipDiscountAmount
+                    formattedVipDiscountAmount
+                    giftcardNumber
+                    subtotal
+                    subTotalInclTax
+                    discountAmount
+                    giftcardAmount
+                    vipPlusAmount
+                    vipExpireDate
+                    taxAmount
+                    taxTotal
+                    shippingAmount
+                    shippingAmountInclTax
+                    grandTotal
+                    formattedSubtotal
+                    formattedSubTotalInclTax
+                    formattedGiftcardAmount
+                    formattedDiscountAmount
+                    formattedVipPlusAmount
+                    formattedTaxAmount
+                    formattedTaxTotal
+                    formattedShippingAmount
+                    formattedShippingAmountInclTax
+                    formattedGrandTotal
+                    couponCode
+                    selectedShippingRate
+                    selectedShippingRateTitle
+                    paymentMethod
+                    paymentMethodTitle
+                    haveStockableItems
+                    billingAddress
+                    shippingAddress
+                }
+            }
+    }
+`;

+ 1 - 0
src/graphql/cart/mutations/CreateMergeCart.ts

@@ -42,6 +42,7 @@ export const CREATE_MERGE_CART: TypedDocumentNode<CreateMergeCartData> = gql`
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

+ 66 - 0
src/graphql/cart/mutations/CreateRemoveCoupon.ts

@@ -0,0 +1,66 @@
+import { gql, TypedDocumentNode } from "@apollo/client";
+import { CreateRemoveCouponData } from "@/types/cart/type";
+
+export const CREATE_REMOVE_COUPON: TypedDocumentNode<CreateRemoveCouponData> = gql`
+    mutation createRemoveCoupon {
+        createRemoveCoupon(input:{}) {
+                removeCoupon {
+                    id
+                    appliedTaxes
+                    itemsQty
+                    isGuest
+                    itemsCount
+                    items {
+                    edges {
+                        node {
+                        id
+                        cartId
+                        productId
+                        name
+                        price
+                        baseImage
+                        sku
+                        quantity
+                        type
+                        productUrlKey
+                        canChangeQty
+                        }
+                    }
+                    }
+                    isVip
+                    vipDiscountAmount
+                    formattedVipDiscountAmount
+                    giftcardNumber
+                    subtotal
+                    subTotalInclTax
+                    discountAmount
+                    giftcardAmount
+                    vipPlusAmount
+                    vipExpireDate
+                    taxAmount
+                    taxTotal
+                    shippingAmount
+                    shippingAmountInclTax
+                    grandTotal
+                    formattedSubtotal
+                    formattedSubTotalInclTax
+                    formattedGiftcardAmount
+                    formattedDiscountAmount
+                    formattedVipPlusAmount
+                    formattedTaxAmount
+                    formattedTaxTotal
+                    formattedShippingAmount
+                    formattedShippingAmountInclTax
+                    formattedGrandTotal
+                    couponCode
+                    selectedShippingRate
+                    selectedShippingRateTitle
+                    paymentMethod
+                    paymentMethodTitle
+                    haveStockableItems
+                    billingAddress
+                    shippingAddress
+                }
+            }
+    }
+`;

+ 1 - 0
src/graphql/cart/mutations/GetCartItem.ts

@@ -36,6 +36,7 @@ export const GET_CART_ITEM: TypedDocumentNode<GetCartItemData> = gql`
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

+ 2 - 1
src/graphql/cart/mutations/RemoveCartItem.ts

@@ -1,7 +1,7 @@
 import { gql, TypedDocumentNode } from "@apollo/client";
 import { RemoveCartItemData } from "@/types/cart/type";
 
-export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData> = gql`
+export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData,{cartItemId: number}> = gql`
   mutation RemoveCartItem( $cartItemId: Int! ) {
     createRemoveCartItem(input: { cartItemId: $cartItemId }) {
       removeCartItem {
@@ -36,6 +36,7 @@ export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData> = gql`
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

+ 2 - 1
src/graphql/cart/mutations/UpdateCartItems.ts

@@ -1,7 +1,7 @@
 import { gql, TypedDocumentNode } from "@apollo/client";
 import { UpdateCartItemData } from "@/types/cart/type";
 
-export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
+export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData,{cartItemId: number;quantity: number;}> = gql
   `
   mutation UpdateCartItem(
     $cartItemId: Int!
@@ -45,6 +45,7 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

+ 3 - 1
src/graphql/cart/mutations/index.ts

@@ -3,4 +3,6 @@ export {REMOVE_CART_ITEM} from './RemoveCartItem';
 export {UPDATE_CART_ITEM} from './UpdateCartItems';
 export {GET_CART_ITEM} from './GetCartItem';
 export {CREATE_CART_TOKEN} from './CreateCartToken';
-export {CREATE_MERGE_CART} from "./CreateMergeCart"
+export {CREATE_MERGE_CART} from "./CreateMergeCart"
+export {CREATE_APPLY_COUPON} from "./CreateApplyCoupon"
+export {CREATE_REMOVE_COUPON} from "./CreateRemoveCoupon";

+ 8 - 1
src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts

@@ -1,7 +1,13 @@
 import { gql,TypedDocumentNode } from "@apollo/client";
 import { SaveCheckoutCartData } from "@/types/checkout/type";
 
-export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData> = gql`
+export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData,{
+    shippingMethod: string;
+    paymentMethod: string;
+    couponCode: string;
+    giftCardNumber: string;
+    memberDiscount: string;
+}> = gql`
     mutation CreateSaveCheckoutCart(
         $shippingMethod: String
         $paymentMethod: String
@@ -50,6 +56,7 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData>
                 discountAmount
                 giftcardAmount
                 vipPlusAmount
+                vipExpireDate
                 taxAmount
                 taxTotal
                 shippingAmount

+ 3 - 2
src/graphql/customer/mutations/CustomerRegistration.ts

@@ -1,6 +1,7 @@
-import { gql } from "@apollo/client";
+import { gql, TypedDocumentNode } from "@apollo/client";
+import {CreateUserResponse} from "@/types/types";
 
-export const CUSTOMER_REGISTRATION = gql`
+export const CUSTOMER_REGISTRATION: TypedDocumentNode<CreateUserResponse> = gql`
   mutation registerCustomer($input: createCustomerInput!) {
     createCustomer(input: $input) {
       customer {

+ 44 - 0
src/graphql/customer/query/GetCustomerAddress.ts

@@ -0,0 +1,44 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { GetCustomerAddressResponse, GetCustomerAddressVariables } from "@/types/customer/type";
+
+export const GET_CUSTOMER_ADDRESS: TypedDocumentNode<GetCustomerAddressResponse,GetCustomerAddressVariables> = gql`
+query getCustomerAddresses($first: Int, $after: String, $last:Int, $before: String) {
+  getCustomerAddresses(first: $first, after: $after, last: $last, before: $before) {
+    edges {
+      cursor
+      node {
+        id
+        _id
+        parentAddressId
+        addressType
+        companyName
+        name
+        firstName
+        lastName
+        email
+        address
+        city
+        state
+        country
+        postcode
+        phone
+        vatId
+        cartId
+        orderId
+        gender
+        defaultAddress
+        useForShipping
+        additional
+        createdAt
+        updatedAt
+      }
+    }
+    pageInfo {
+      hasNextPage
+      hasPreviousPage
+      startCursor
+      endCursor
+    }
+    totalCount
+  }
+}`;

+ 2 - 1
src/graphql/customer/query/index.ts

@@ -1,3 +1,4 @@
 export {
     GET_ORDER_DETAILS
-} from "./GetOrderDetails";
+} from "./GetOrderDetails";
+export {GET_CUSTOMER_ADDRESS} from "./GetCustomerAddress";

+ 1 - 0
src/graphql/index.ts

@@ -6,6 +6,7 @@ export * from "./cart/mutations";
 export * from "./checkout/queries";
 export * from "./checkout/mutations";
 export * from "./customer/query/index";
+export * from "./customer/mutations/index";
 export * from "./currency/query/index";
 export * from "./types";
 

+ 394 - 27
src/lib/Airwallex/airwallexInit.ts

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

+ 68 - 215
src/lib/Airwallex/useAirwallexCard.ts

@@ -1,232 +1,85 @@
-import { useEffect, useRef, useReducer, useEffectEvent } from "react";
+import { useSyncExternalStore, useEffect } from "react";
 import { 
     airwallexInit,
     airwallexManager,
-    AirwallexCartNumberElementType,
-    AirwallexCvcElementType,
-    AirwallexExpiryDateElementType
+    AirwallexCardSnapshot
 } from "@/lib/Airwallex/airwallexInit";
-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};
+const serverSnapshot: AirwallexCardSnapshot = {
+    cardNumberReady: false,
+    cvcReady: false,
+    expiryReady: false,
+
+    cardNumberValid: false,
+    cvcValid: false,
+    expiryValid: false,
+
+    cardNumberErrorMsg: '',
+    cvcErrorMsg: '',
+    expiryErrorMsg: '',
+};
+
+export {airwallexManager};
+
+export function useAirwallexCardState() {
+    // react 订阅 卡支付输入框状态
+    return useSyncExternalStore(
+        airwallexManager.subscribe,
+        airwallexManager.getSnapshot,
+        //第三个参数 用于 SSR 和 hydration,而且服务端和 hydration 的初始 snapshot 必须一致(与airwallexInit.ts中的initialSnapshot一致)
+        () => serverSnapshot
+    );
 }
 
-
-
-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,
-            });
+export function useAirwallexCard(mountId:{
+        cardNumber: string;
+        cvc: string;
+        expiry: string;
+}) {
+
+    // const state = useAirwallexCardState();
+
+
+    useEffect(() => {
+
+        airwallexManager.setRefCount(); // 引用计数
+        let canceled = false;
+        // init 函数里做了两次canceled为true然后return的判断
+        // 原因是这里有两个异步调用,异步调用的事件可能很快,也可能很慢
+        // 那么就会出现第一次effect触发后,异步调用还没有完成,然后因为用户修改购物车,导致第一次effect 的cleanup函数执行,接着触发第二次effect
+        // 那么就需要取消第一次effect中之后的逻辑,以第二次的effect为准。
+        async function init() {
+
+            await airwallexInit();
+            if (canceled) {
+                return;
+            }
+            await airwallexManager.createElements();
+            
+            if (canceled) {
+                return;
+            }
+
+            airwallexManager.mount(mountId);
+            airwallexManager.elementsBindHandler();
         }
-    });
 
 
+        init();
 
 
+        return () => {
+            
+            canceled = true;
 
+            airwallexManager.release();
+        };
 
-  const mountedRef = useRef(false);
-  const elementRef = useRef<{
-    cardNumber?: AirwallexCartNumberElementType;
-    cvc?: AirwallexCvcElementType;
-    expiry?: AirwallexExpiryDateElementType;
-  }>({});
 
-  useEffect(() => {
-    if (!enabled) return;
-
-    let destroyed = false;
-
-    const init = async () => {
-      await airwallexInit();
-    //   await airwallexManager.init();
-
-      const els = await airwallexManager.createElements();
-      elementRef.current = els;
-console.log('useAirwallexCard +++++++++++++++++++++++++++++',destroyed);
-      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 () => {
-        console.log('useAirwallexCard ___________________________________',mountedRef.current)
-      destroyed = true;
-      if (mountedRef.current) {
-        airwallexManager.unmount();// 解除挂载
-        mountedRef.current = false;
-      }
-    };
-  }, [enabled]);
-
-
-
-  return {
-      getCardElements:() => elementRef.current,
-      airwallexCardState: airwallexCardState
-  };
-
-}
+    }, [
+        mountId.cardNumber,
+        mountId.cvc,
+        mountId.expiry,
+    ]);
 
-function bindEvents(els: any,callback:EventCallback) {
-  const { cardNumber, cvc, expiry } = els;
-  const {focusCallback,blurCallback, changeCallback, readyCallback} = callback;
-
-  airwallexManager.on(cardNumber,"focus", focusCallback);
-  airwallexManager.on(cardNumber,"blur", blurCallback);
-  airwallexManager.on(cardNumber,"change", changeCallback);
-  airwallexManager.on(cardNumber,"ready", readyCallback);
-
-  airwallexManager.on(cvc,"focus", focusCallback);
-  airwallexManager.on(cvc,"blur", blurCallback);
-  airwallexManager.on(cvc,"change", changeCallback);
-  airwallexManager.on(cvc,"ready", readyCallback);
-
-  airwallexManager.on(expiry,"focus", focusCallback);
-  airwallexManager.on(expiry,"blur", blurCallback);
-  airwallexManager.on(expiry,"change", changeCallback);
-  airwallexManager.on(expiry,"ready", readyCallback);
-
-  // ⚠️ 关键:绑定只发生一次(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 - 32
src/lib/ApolloClientBrowser.ts

@@ -1,4 +1,3 @@
-import { cache as reactCache } from "react";
 import { HttpLink,ApolloLink } from "@apollo/client";
 import { SetContextLink } from "@apollo/client/link/context";
 import {
@@ -8,9 +7,6 @@ import {
 import { ErrorLink } from "@apollo/client/link/error";
 import {CombinedProtocolErrors} from "@apollo/client/errors";
 import {handleErrorForErrorLink} from "@/lib/ApolloErrorHandler";
-import { getSession } from "next-auth/react";
-import { getCartToken } from "@/utils/getCartToken";
-import { BagistoSession } from "@/types/types";
 
 // 这里注册的是客户端使用的apollo client
 
@@ -19,29 +15,6 @@ CombinedProtocolErrors.formatMessage = (errors,{defaultFormatMessage})=>{
 };
 
 
-
-let sessionCache: { session: BagistoSession | null; timestamp: number } | null = null;
-const SESSION_CACHE_TTL = 5000;
-
-const getSessionForRequest = reactCache(async () => {
-  return (await getSession()) as BagistoSession | null;
-});
-
-async function getCachedSession(): Promise<BagistoSession | null> {
-  if (typeof window === "undefined") {
-    return getSessionForRequest();
-  }
-
-  const now = Date.now();
-
-  if (sessionCache && now - sessionCache.timestamp < SESSION_CACHE_TTL) {
-    return sessionCache.session;
-  }
-  const session = (await getSession()) as BagistoSession | null;
-  sessionCache = { session, timestamp: now };
-  return session;
-}
-
 export default function makeClient() {
     const httpLink = new HttpLink({
         uri: "/api/graphql",
@@ -60,15 +33,11 @@ export default function makeClient() {
 
   const authLink = new SetContextLink(async (prevContext) => {
       
-      const session = await getCachedSession();
-      const userToken = session?.user?.accessToken;
-      const guestToken = !userToken ? getCartToken() : null;
-      const token = userToken || guestToken;
   
       return {
         headers: {
           ...prevContext.headers,
-          ...(token && { Authorization: `Bearer ${token}` }),
+          // ...(token && { Authorization: `Bearer ${token}` }),
           "Content-Type": "application/json",
         },
       };

+ 1 - 26
src/lib/restApiClient.ts

@@ -1,36 +1,11 @@
 'use client';
 /**前端客户端组件调rest api 接口 */
-import { getSession } from "next-auth/react";
-import { getCartToken } from "@/utils/getCartToken";
-import { BagistoSession } from "@/types/types";
-
-
-let sessionCache: { session: BagistoSession | null; timestamp: number } | null = null;
-const SESSION_CACHE_TTL = 5000;
-
-async function getCachedSession(): Promise<BagistoSession | null> {
-
-  const now = Date.now();
-
-  if (sessionCache && now - sessionCache.timestamp < SESSION_CACHE_TTL) {
-    return sessionCache.session;
-  }
-  //When called, getSession() will send a request to /api/auth/session and returns a promise with a session object, or null if no session exists.
-  const session = (await getSession()) as BagistoSession | null;
-  sessionCache = { session, timestamp: now };
-  return session;
-}
 
 export async function clientFetch<T = any>(apiUrl: string, options: RequestInit = {}): Promise<T> {
     // 请求的是nextjs的代理接口
 
-    const session = await getCachedSession();
-    const userToken = session?.user?.accessToken;
-    const guestToken = !userToken ? getCartToken() : null;
-    const token = userToken || guestToken;
-
     const headers = {
-        ...(token && { Authorization: `Bearer ${token}` }),
+        //...(token && { Authorization: `Bearer ${token}` }),
         "Content-Type": "application/json",
     };
     if(options.headers) {

+ 4 - 1
src/providers/GlobalProviders.tsx

@@ -5,6 +5,7 @@ import { ThemeProvider } from "./ThemeProvider";
 import { ToastProvider } from "./ToastProvider";
 import { ApolloWrapper } from "./ApolloWrapper";
 import { PaymentSDKProvider } from "./PaymentSDKProvider";
+import { LoginModalProvider } from "./LoginModalProvider";
 
 export function GlobalProviders({ children }: { children: ReactNode }) {
   return (
@@ -12,7 +13,9 @@ export function GlobalProviders({ children }: { children: ReactNode }) {
         <ToastProvider>
           <ApolloWrapper>
             <PaymentSDKProvider>
-            {children}
+              <LoginModalProvider>
+                {children}
+              </LoginModalProvider>
             </PaymentSDKProvider>
           </ApolloWrapper>
         </ToastProvider>

+ 103 - 0
src/providers/LoginModalProvider.tsx

@@ -0,0 +1,103 @@
+"use client";
+
+import {
+    createContext,
+    useContext,
+    useState,
+    ReactNode,
+    useCallback
+} from "react";
+
+import LoginModal, {LoginModalReult} from "@/components/common/LoginModal/LoginModal";
+
+type LoginModalOptions = {
+    callbackUrl?: string;
+    onFinalResult?:(param:LoginModalReult)=>void | Promise<void>;
+};
+
+type LoginModalContextType = {
+    isOpen: boolean;
+    openLoginModal: (options?:LoginModalOptions)=>void;
+    closeLoginModal:()=>void;
+};
+
+const LoginModalContext = createContext<LoginModalContextType|null>(null);
+
+
+export function LoginModalProvider({
+    children
+}:{
+    children:ReactNode;
+}){
+
+
+    const [isOpen,setIsOpen]=useState(false);
+
+    const [modalOptions,setModalOptions]= useState<LoginModalOptions|null>(null);
+
+
+
+    const openLoginModal = useCallback((options?:LoginModalOptions)=>{
+
+        setModalOptions(options ?? null);
+
+        setIsOpen(true);
+
+    },[]);
+
+
+
+    const closeLoginModal = useCallback(()=>{
+
+        setIsOpen(false);
+
+        setModalOptions(null);
+
+    },[]);
+
+
+
+    return (
+
+        <LoginModalContext.Provider
+            value={{
+                isOpen,
+                openLoginModal,
+                closeLoginModal
+            }}
+        >
+
+            {children}
+
+
+            <LoginModal
+                isShow={isOpen}
+                closeModal={closeLoginModal}
+                callbackUrl={modalOptions?.callbackUrl}
+                onFinalResult={ modalOptions?.onFinalResult }
+            />
+
+
+        </LoginModalContext.Provider>
+
+    );
+
+}
+
+
+
+export function useLoginModal(){
+
+    const context=useContext(LoginModalContext);
+
+
+    if(!context){
+        throw new Error(
+            "useLoginModal must be used inside LoginModalProvider"
+        );
+    }
+
+
+    return context;
+
+}

+ 0 - 2
src/providers/SessionManager.tsx

@@ -1,13 +1,11 @@
 "use client";
 
 import { SessionProvider } from "next-auth/react";
-import { SessionSync } from "./SessionSync";
 import { ReactNode } from "react";
 
 export function SessionManager({ children }: { children: ReactNode }) {
   return (
     <SessionProvider>
-      <SessionSync />
       {children}
     </SessionProvider>
   );

+ 0 - 21
src/providers/SessionSync.tsx

@@ -1,21 +0,0 @@
-"use client";
-
-import { useSession } from "next-auth/react";
-import { useEffect } from "react";
-import { useAppDispatch } from "@/store/hooks";
-import { setUser, clearUser } from "@/store/slices/user-slice";
-
-export const SessionSync = () => {
-    const { data: session, status } = useSession();
-    const dispatch = useAppDispatch();
-
-    useEffect(() => {
-        if (status === "authenticated" && session?.user) {
-            dispatch(setUser(session.user as any));
-        } else if (status === "unauthenticated") {
-            dispatch(clearUser());
-        }
-    }, [session, status, dispatch]);
-
-    return null;
-}

+ 9 - 8
src/providers/ToastProvider.tsx

@@ -1,5 +1,5 @@
 "use client";
-import { ReactNode, createContext, useContext, useState } from "react";
+import { ReactNode, createContext, useContext, useState, useCallback } from "react";
 import { ToastContainer } from "@/components/theme/toast/ToastContainer";
 
 export type ToastType = "success" | "danger" | "warning" | "primary";
@@ -22,9 +22,12 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined);
 export const ToastProvider = ({ children }: { children: ReactNode }) => {
   const [toasts, setToasts] = useState<ToastDataType[]>([]);
 
-  const addToast = (toast: Omit<ToastDataType, "id">) => {
-    // eslint-disable-next-line react-hooks/purity
-    const id = Math.random().toString(36).substr(2, 9);
+  const removeToast = useCallback((id: string) => {
+    setToasts((prev) => prev.filter((toast) => toast.id !== id));
+  },[]);
+  const addToast = useCallback((toast: Omit<ToastDataType, "id">) => {
+
+    const id = crypto.randomUUID();
     const newToast = { id, ...toast };
 
     setToasts((prev) => [...prev, newToast]);
@@ -32,11 +35,9 @@ export const ToastProvider = ({ children }: { children: ReactNode }) => {
     setTimeout(() => {
       removeToast(id);
     }, toast.duration || 5000);
-  };
+  },[removeToast]);
 
-  const removeToast = (id: string) => {
-    setToasts((prev) => prev.filter((toast) => toast.id !== id));
-  };
+  
 
   return (
     <ToastContext.Provider value={{ toasts, addToast, removeToast }}>

+ 0 - 1
src/providers/index.ts

@@ -7,6 +7,5 @@ export { SessionManager } from "./SessionManager";
 export { ThemeProvider } from "./ThemeProvider";
 export { ToastProvider, useToast } from "./ToastProvider";
 export { SessionProvider } from "./SessionProvider";
-export { SessionSync } from "./SessionSync";
 export { PayPalWrapper } from "./PaypalWrapper";
 export { ConfigProvider } from "./ConfigProvider";

+ 0 - 30
src/store/slices/user-slice.ts

@@ -1,30 +0,0 @@
-import { PayloadAction, createSlice } from "@reduxjs/toolkit";
-import { BagistoUser } from "@/types/types";
-
-interface UserState {
-    user: BagistoUser | null;
-    isAuthenticated: boolean;
-}
-
-const initialState: UserState = {
-    user: null,
-    isAuthenticated: false,
-};
-
-const userSlice = createSlice({
-    name: "user",
-    initialState,
-    reducers: {
-        setUser: (state, action: PayloadAction<BagistoUser>) => {
-            state.user = action.payload;
-            state.isAuthenticated = true;
-        },
-        clearUser: (state) => {
-            state.user = null;
-            state.isAuthenticated = false;
-        },
-    },
-});
-
-export const { setUser, clearUser } = userSlice.actions;
-export default userSlice.reducer;

+ 0 - 2
src/store/store.ts

@@ -1,7 +1,6 @@
 import { configureStore } from "@reduxjs/toolkit";
 
 import cartSlice, {CartState} from "./slices/cart-slice";
-import userSlice from "./slices/user-slice";
 import addToCartDialogReducer from "./slices/addToCartDialogSlice";
 
 // export const store = configureStore({
@@ -22,7 +21,6 @@ export const makeStore = (preloadedState: PreloadedStateType) => {
     return configureStore({
       reducer: {
         cartDetail: cartSlice,
-        user: userSlice,
         addToCartDialog: addToCartDialogReducer,
       },
       preloadedState

+ 6 - 0
src/types/cart/type.ts

@@ -54,6 +54,7 @@ export interface CartDetail {
   // baseDiscountAmount: number;
   giftcardAmount: number;
   vipPlusAmount: number;
+  vipExpireDate: string;
   taxAmount: number;
   taxTotal: number;
   shippingAmount: number;
@@ -150,6 +151,11 @@ export interface CreateApplyCouponPayload {
 export interface CreateApplyCouponData {
   createApplyCoupon: CreateApplyCouponPayload;
 }
+export interface CreateRemoveCouponData {
+  createRemoveCoupon: {
+    removeCoupon: CartDetail
+  };
+}
 // Merge Cart
 export interface CreateMergeCartPayload {
   mergeCart: CartDetail;

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

@@ -524,16 +524,3 @@ export type PlaceOrderFunction = <T = null>(params?: CreatePaymentInitiateVariab
 
 
 
-export interface AirwallexCardStateType {
-    cardNumberReady: boolean;
-    cvcReady: boolean;
-    expiryReady: boolean;
-
-    cardNumberValid: boolean;
-    cvcValid: boolean;
-    expiryValid: boolean;
-
-    cardNumberErrorMsg: string;
-    cvcErrorMsg: string;
-    expiryErrorMsg: string;
-}

+ 63 - 1
src/types/customer/type.ts

@@ -8,4 +8,66 @@ export type {
     AddressListInOrderDetails,
     AddressInOrderDetails,
     OrderAddressType
-} from "./order";
+} from "./order";
+
+export interface CustomerLoginData{
+    id: string;
+    apiToken: string;
+    token: string;
+    message: string;
+    success: boolean;
+}
+export interface CustomerLoginResponse{
+    createCustomerLogin:{
+        customerLogin: CustomerLoginData
+    }
+    
+}
+
+export interface CustomerAddressItem {
+    id: string;
+    _id: number;
+    parentAddressId: string | null;
+    addressType: string;
+    companyName: string | null;
+    name: string;
+    firstName: string;
+    lastName: string;
+    email: string;
+    address: string;
+    city: string;
+    state: string;
+    country: string;
+    postcode: string;
+    phone: string;
+    vatId: string | null;
+    cartId: string | null;
+    orderId: string | null;
+    gender: string | null;
+    defaultAddress: boolean;
+    useForShipping: boolean;
+    additional: string | null;
+    createdAt: string;
+    updatedAt: string;
+}
+export interface GetCustomerAddressVariables {
+    first?: number;
+    after?: string;
+    last?: number;
+    before?: string;
+}
+export interface GetCustomerAddressResponse {
+    getCustomerAddresses: {
+        edges: Array<{
+            cursor: string;
+            node: CustomerAddressItem
+        }>
+    };
+    pageInfo: {
+        hasNextPage: boolean;
+        hasPreviousPage: boolean;
+        startCursor: string;
+        endCursor: string;
+    };
+    totalCount: number;
+}

+ 13 - 9
src/types/types.ts

@@ -592,14 +592,8 @@ export type BagistoUserTypes = {
   };
 };
 
-export type BagistoCreateUserOperation = {
-  data: {
-    createCustomer: {
-      customer: BagistoUser;
-    };
-  };
-  variables: {
-    input: {
+export type CreateUserVariables = {
+  input: {
       firstName: string;
       lastName: string;
       email: string;
@@ -612,8 +606,13 @@ export type BagistoCreateUserOperation = {
       isVerified?: string;
       isSuspended?: string;
       subscribedToNewsLetter?: boolean;
-    };
   };
+}
+
+export type CreateUserResponse = {
+    createCustomer: {
+      customer: BagistoUser;
+    }
 };
 
 export type BagistoUser = {
@@ -637,6 +636,11 @@ export type BagistoUser = {
   name: string;
 };
 
+export type BagistoCreateUserOperation = {
+  data: CreateUserResponse;
+  variables: CreateUserVariables;
+};
+
 export type SuperAttribute = {
   attributeId: number;
   attributeOptionId: number;

+ 5 - 1
src/utils/auth.ts

@@ -2,6 +2,7 @@ import NextAuth, { NextAuthOptions } from "next-auth";
 import CredentialsProvider from "next-auth/providers/credentials";
 import { bagistoFetch, /*restApiFetch*/} from "@/utils/bagisto";
 import { CUSTOMER_LOGIN } from "@/graphql/customer/mutations";
+import {CustomerLoginResponse} from "@/types/customer/type";
 
 export const authOptions: NextAuthOptions = {
   session: {
@@ -30,7 +31,10 @@ export const authOptions: NextAuthOptions = {
         };
 
 
-        const res = await bagistoFetch<any>({
+        const res = await bagistoFetch<{
+          data: CustomerLoginResponse,
+          variables: {input:{email:string;password:string;}}
+        }>({
           query: CUSTOMER_LOGIN,
           variables: { input },
           cache: "no-store",

+ 56 - 49
src/utils/bagisto/index.ts

@@ -24,7 +24,6 @@ import {
 } from "@/utils/constants/server";
 import { 
   GUEST_CART_TOKEN,
-  IS_GUEST,
   CURRENT_CURRENCY,
   CURRENT_LOCAL,
   CURRENT_CHANNEL,
@@ -44,14 +43,14 @@ import {
   ThemeCustomizationResponse,
   PageData,
 } from "@/types/theme/theme-customization";
-import { decodeJWT } from "@/utils/jwt-cookie";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 
 
 type ExtractVariables<T> = T extends { variables: object }
   ? T["variables"]
   : any;
-type ExtractData<T> = T extends { data: infer D } ? D : any;
+type ExtractRestFulData<T> = T extends { data: infer D } ? D : any;
+type ExtractGraphqlData<T> = T extends { data: infer D } ? { data: D } : any;
 
 interface PageByUrlKeyResponse {
   pageByUrlKeypages?: PageData[];
@@ -88,7 +87,29 @@ async function getBaseHeader() {
     }
     return baseHeaders;
 }
+export async function getAuthorizationToken(): Promise<{token: string | null; isGuest: boolean;}> {
+  
+  // 登录用户从nextAuth里获取
+  const authSession = (await getServerSession(
+        authOptions,
+      )) as BagistoSession | null;
+  const accessToken = authSession?.user?.accessToken;
+  if (accessToken) {
+    return {
+      token: accessToken,
+      isGuest: false,
+    };
+  }
 
+  // 游客从cookie里获取
+  const cookieStore = await cookies();
+  const guestToken = cookieStore.get(GUEST_CART_TOKEN)?.value;
+  
+  return {
+    token: guestToken || null,
+    isGuest: true
+  };
+}
 
 // rest api fetch
 export async function restApiFetch<T>({
@@ -98,8 +119,6 @@ export async function restApiFetch<T>({
   headers,
   tags,
   variables,
-  isCookies = true,
-  guestToken,
   revalidate = 60,
 }: {
   api: string;
@@ -111,11 +130,16 @@ export async function restApiFetch<T>({
   isCookies?: boolean;
   guestToken?: string;
   revalidate?: number;
-}): Promise<{ status: number; body: ExtractData<T> } | never> {
+}): Promise<{ status: number; body: ExtractRestFulData<T> } | never> {
   try {
     const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
     const url = new URL(apiUrl);
 
+    const tokenRes = await getAuthorizationToken();
+
+    const headerRes = await getBaseHeader();
+    const baseHeaders: Record<string, string> = {...headerRes};
+    /*
     let accessToken: string | undefined = undefined;
 
     if (isCookies) {
@@ -125,17 +149,19 @@ export async function restApiFetch<T>({
       accessToken = sessions?.user?.accessToken;
     }
 
-    const headerRes = await getBaseHeader();
-    const baseHeaders: Record<string, string> = {...headerRes};
 
     if (accessToken) {
       baseHeaders.Authorization = `Bearer ${accessToken}`;
     } else if (guestToken) {
       baseHeaders.Authorization = `Bearer ${guestToken}`;
     }
+    */
+    if (tokenRes.token) {
+      baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+    }
 
 
-    if (isCookies && headers) {
+    if (headers) {
       if (headers instanceof Headers) {
         headers.forEach((value, key) => (baseHeaders[key] = value));
       } else {
@@ -196,36 +222,13 @@ export async function serverGraphqlFetch<
   try {
     const queryString = typeof query === "string" ? query : print(query);
 
-    let accessToken: string | undefined = undefined;
-    let guestToken: string | undefined = undefined;
-
-    const cookieStore = await cookies();
-    
-    const isGuest = cookieStore.get(IS_GUEST)?.value;
-    const gt =  cookieStore.get(GUEST_CART_TOKEN)?.value;
-    if(isGuest === 'false') {
-      // 登录用户
-      const sessions = (await getServerSession(
-        authOptions,
-      )) as BagistoSession | null;
-      accessToken = sessions?.user?.accessToken;
-    } else if(gt) {
-        // 游客
-        const jwtRes = decodeJWT<{
-            sessionToken: string;
-            cartId: number;
-            isGuest: boolean;
-        }>(gt, true);
-        guestToken = jwtRes?.sessionToken || '';
-    }
-
+    const tokenRes = await getAuthorizationToken();
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
 
-    if (accessToken) {
-      baseHeaders['Authorization'] = `Bearer ${accessToken}`;
-    } else if (guestToken) {
-      baseHeaders['Authorization'] = `Bearer ${guestToken}`;
+
+    if (tokenRes.token) {
+      baseHeaders['Authorization'] = `Bearer ${tokenRes.token}`;
     }
 
     if (headers) {
@@ -271,8 +274,8 @@ export async function bagistoFetch<T>({
   query,
   tags,
   variables,
-  isCookies = true,
-  guestToken,
+  // isCookies = true,
+  // guestToken,
   revalidate = 60,
 }: {
   cache?: RequestCache;
@@ -280,15 +283,18 @@ export async function bagistoFetch<T>({
   query: string | DocumentNode;
   tags?: string[];
   variables?: ExtractVariables<T>;
-  isCookies?: boolean;
-  guestToken?: string;
+  // isCookies?: boolean;
+  // guestToken?: string;
   revalidate?: number;
-}): Promise<{ status: number; body: T } | never> {
+}): Promise<{ status: number; body: ExtractGraphqlData<T> } | never> {
   try {
     const queryString =
       typeof query === "string" ? query : print(query);
 
-
+    const tokenRes = await getAuthorizationToken();
+    const headerRes = await getBaseHeader();
+    const baseHeaders: Record<string, string> = {...headerRes};
+    /*
     let accessToken: string | undefined = undefined;
 
     if (isCookies) {
@@ -298,19 +304,20 @@ export async function bagistoFetch<T>({
       )) as BagistoSession | null;
       accessToken = sessions?.user?.accessToken;
     }
-
-    const headerRes = await getBaseHeader();
-    const baseHeaders: Record<string, string> = {...headerRes};
-
+    
     if (accessToken) {
       baseHeaders.Authorization = `Bearer ${accessToken}`;
     } else if (guestToken) {
       baseHeaders.Authorization = `Bearer ${guestToken}`;
     }
+    */
+    if (tokenRes.token) {
+      baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+    }
 
     
 
-    if (isCookies && headers) {
+    if (headers) {
       if (headers instanceof Headers) {
         headers.forEach((value, key) => (baseHeaders[key] = value));
       } else {
@@ -444,7 +451,7 @@ export async function logoutUser() {
       variables: { input: { token: string } };
     }>({
       query: CUSTOMER_LOGOUT,
-      isCookies: true,
+      // isCookies: true,
       revalidate: 3600,
     });
 
@@ -591,7 +598,7 @@ export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
   }>({
     query: PAGE_BY_URL_KEY,
     cache: "no-store",
-    isCookies: false,
+    // isCookies: false,
     variables: { pageByUrlKey: input.urlKey },
   });
 

+ 1 - 0
src/utils/cartDetailTools.ts

@@ -14,6 +14,7 @@ export function formatCartDetail(cart:CartDetail): CartDetail {
         giftcardNumber: cart.giftcardNumber,
         giftcardAmount: cart.giftcardAmount,
         vipPlusAmount: cart.vipPlusAmount,
+        vipExpireDate: cart.vipExpireDate,
         subtotal: cart.subtotal,
         subTotalInclTax: cart.subTotalInclTax,
         discountAmount: cart.discountAmount,

+ 10 - 0
src/utils/constants.ts

@@ -106,6 +106,16 @@ export const SortByFields: SortOrderTypes[] = [
   },
 ];
 
+export const DEFAULT_EXPIRES_DAYS = 7; // cookie里的 GUEST_CART_TOKEN GUEST_CART_ID IS_GUEST 的过期时间
+export const GUEST_COOKIE_OPTION = {
+    days: DEFAULT_EXPIRES_DAYS,
+    encode: true,          // 默认编码,更加安全
+    path: '/',
+    // domain:,
+    secure: process.env.NEXT_PUBLIC_APP_ENV !== 'development',
+    sameSite: 'lax',
+};
+
 export const GUEST_CART_TOKEN = "guest_cart_token";
 export const GUEST_CART_ID = "guest_cart_id";
 export const IS_GUEST = "is_guest";

+ 33 - 8
src/utils/cookie-tools.ts

@@ -34,10 +34,9 @@ export const deleteCookie = (name: string) => {
   document.cookie = `${name}=; Max-Age=0; path=/`;
 };
 */
+import {GUEST_COOKIE_OPTION,GUEST_CART_TOKEN,IS_GUEST,GUEST_CART_ID} from "@/utils/constants";
 // cookie.ts
 
-const DEFAULT_EXPIRES_DAYS = 7;
-
 export interface CookieOptions {
   /** 过期天数(从当前时间起算),默认 7 天 */
   days?: number;
@@ -83,15 +82,15 @@ export function setCookie(
   options: CookieOptions = {}
 ): void {
   if (typeof document === 'undefined') return;
-
+  const setOption = Object.assign({},GUEST_COOKIE_OPTION,options);
   const {
-    days = DEFAULT_EXPIRES_DAYS,
-    encode = true,          // 默认编码,更加安全
-    path = '/',
+    days,
+    encode,          // 默认编码,更加安全
+    path,
     domain,
     secure,
     sameSite,
-  } = options;
+  } = setOption;
 
   // 处理过期时间
   const expires = new Date(Date.now() + days * 864e5).toUTCString();
@@ -119,8 +118,14 @@ export function deleteCookie(
   options: Pick<CookieOptions, 'path' | 'domain' | 'secure'> = {}
 ): void {
   if (typeof document === 'undefined') return;
+  const {
+    path: defaultPath,
+    secure: defaultSecure
+  } = GUEST_COOKIE_OPTION;
+  const { domain } = options;
 
-  const { path = '/', domain, secure } = options;
+  const path = options.path || defaultPath;
+  const secure = options.secure || defaultSecure;
 
   // 若删除时指定了 domain 或 secure,需要与设置时一致才能删除成功
   let cookie = `${name}=; Max-Age=0; path=${path}`;
@@ -128,4 +133,24 @@ export function deleteCookie(
   if (secure) cookie += '; secure';
 
   document.cookie = cookie;
+}
+
+export function getGuestCartId(): string | null {
+  const res = getCookie(GUEST_CART_ID);
+  return res;
+}
+export function getIsGuest(): boolean {
+  const res = getCookie(IS_GUEST);
+  return res === 'true' || res === null;
+}
+export const getGuestCartToken = (): string | null => {
+  const raw = getCookie(GUEST_CART_TOKEN);
+  if (!raw) return null;
+
+  return raw;
+};
+
+export function deleteGuestCookie() {
+    deleteCookie(GUEST_CART_ID);
+    deleteCookie(GUEST_CART_TOKEN);
 }

+ 0 - 13
src/utils/getCartToken.ts

@@ -1,13 +0,0 @@
-import { GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
-import { decodeJWT } from "@/utils/jwt-cookie";
-import { getCookie } from "@/utils/cookie-tools";
-// 管理游客的token
-export const getCartToken = (): string | null => {
-  const raw = getCookie(GUEST_CART_TOKEN);
-  if (!raw) return null;
-
-  const isGuest = getCookie(IS_GUEST) !== "false";
-
-  const decoded = decodeJWT<{ sessionToken: string }>(raw, isGuest);
-  return decoded?.sessionToken ?? null;
-};

+ 94 - 18
src/utils/hooks/useAddToCart.ts

@@ -1,15 +1,19 @@
 "use client";
 
+import { useCallback } from "react";
 import { useCustomToast } from "./useToast";
 import { useAppDispatch } from "@/store/hooks";
 import { addItem, clearCart } from "@/store/slices/cart-slice";
 import { isObject } from "@utils/type-guards";
-import { getCartToken } from "@utils/getCartToken";
-import { getCookie, setCookie, deleteCookie } from "@utils/cookie-tools";
-import { encodeJWT } from "@/utils/jwt-cookie";
+import { 
+  setCookie, 
+  getIsGuest,
+  getGuestCartToken,
+  deleteGuestCookie
+} from "@utils/cookie-tools";
 import { useGuestCartToken } from "./useGuestCartToken";
 import { IS_GUEST,GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
-import { useMutation } from "@apollo/client/react";
+import { useMutation, useApolloClient } from "@apollo/client/react";
 import {
   CREATE_ADD_PRODUCT_IN_CART,
   REMOVE_CART_ITEM,
@@ -23,6 +27,7 @@ export const useAddProduct = () => {
   const dispatch = useAppDispatch();
   const { createGuestToken } = useGuestCartToken();
   const { showToast } = useCustomToast();
+  const apolloClient = useApolloClient();
 
   const [mutateAsync, { loading: isCartLoading }] = useMutation(
     CREATE_ADD_PRODUCT_IN_CART,
@@ -39,18 +44,12 @@ export const useAddProduct = () => {
           if (responseData.success) {
             /** 兜底代码 start*/
             // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
-            const isGuest = getCookie(IS_GUEST) !== "false";// 'true' 是游客
+            const isGuest = getIsGuest();// true 是游客
             // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
             const responseCartId = responseData.id;
             const responseCartToken = responseData.cartToken;
             if(isGuest && responseCartId !== responseCartToken) {
-                const newToken = encodeJWT({
-                  sessionToken: responseCartToken,
-                  cartId: responseCartId,
-                  isGuest: isGuest,
-                });
-        
-                setCookie(GUEST_CART_TOKEN, newToken,{encode: false});
+                setCookie(GUEST_CART_TOKEN, responseCartToken,{encode: false});
                 setCookie(GUEST_CART_ID, responseCartId);
                 setCookie(IS_GUEST, String(isGuest));
             }
@@ -84,9 +83,10 @@ export const useAddProduct = () => {
   }) => {
     
     // Ensure token exists - create if needed
-    let token = getCartToken(); // 从cookie获取token
+    let token = getGuestCartToken(); // 从cookie获取token
+    const isGuest = getIsGuest();
 
-    if (!token) {
+    if (!token && isGuest) {
       // 没有token,则创建一个并写入cookie
       token = await createGuestToken();
 
@@ -114,6 +114,81 @@ export const useAddProduct = () => {
     };
   };
 
+  // 删除购物车中的产品
+  const deleteProductFromCart = useCallback((cartItemId: number) => {
+      return apolloClient.mutate({
+          mutation: REMOVE_CART_ITEM,
+          variables: {
+            cartItemId: cartItemId
+          },
+      }).then((res) => {
+          const resCatData =  res.data?.createRemoveCartItem?.removeCartItem ?? null;
+          if(resCatData && resCatData.itemsQty) {
+            const cartDetail = formatCartDetail(resCatData);
+            dispatch(addItem(cartDetail));
+            showToast('Cart item removed successfully', "warning");
+          } 
+          
+          if(!resCatData || !resCatData?.itemsQty) {
+              dispatch(clearCart());
+              // @todo 可以优化为通过接口或者server action 删除cookie
+              const isGuest = getIsGuest();
+              if (isGuest) {
+                deleteGuestCookie();
+              }
+          }
+          return {
+              data: resCatData?.itemsQty ? formatCartDetail(resCatData) : null,
+              error: false,
+              msg: ""
+          };
+      
+      }).catch((err) => {
+          return {
+              data: null,
+              error: true,
+              msg: err.message,
+          }
+      });
+  },[apolloClient]);
+  
+  // 修改购物车中产品的数量
+  const editProductQtyFromCart = useCallback((cartItemId: number,quantity:number) => {
+      if (quantity < 1) {
+        showToast("Quantity must be at least 1", "warning");
+        return {
+              data: null,
+              error: true,
+              msg: "Quantity must be at least 1",
+          };
+      }
+      return apolloClient.mutate({
+          mutation: UPDATE_CART_ITEM,
+          variables: {
+            cartItemId: cartItemId,
+            quantity: quantity
+          },
+      }).then((res) => {
+          const resCatData =  res.data?.createUpdateCartItem?.updateCartItem ?? null;
+          if(resCatData) {
+            const cartDetail = formatCartDetail(resCatData);
+            dispatch(addItem(cartDetail));
+          } 
+          return {
+              data: resCatData ? formatCartDetail(resCatData) : null,
+              error: false,
+              msg: ""
+          };
+      
+      }).catch((err) => {
+          return {
+              data: null,
+              error: true,
+              msg: err.message,
+          }
+      });
+  },[apolloClient]);
+
   //--------Remove Cart Product Quantity--------//
   const [removeFromCart, { loading: isRemoveLoading }] = useMutation(
     REMOVE_CART_ITEM,
@@ -129,10 +204,9 @@ export const useAddProduct = () => {
           if (!responseData?.itemsQty) {
             dispatch(clearCart());
 
-            const isGuest = getCookie(IS_GUEST);
-            if (isGuest === "true") {
-              deleteCookie(GUEST_CART_TOKEN);
-              deleteCookie(GUEST_CART_ID);
+            const isGuest = getIsGuest();
+            if (isGuest) {
+              deleteGuestCookie();
             }
           }
         } else {
@@ -205,5 +279,7 @@ export const useAddProduct = () => {
     onAddToRemove,
     onUpdateCart,
     isUpdateLoading,
+    deleteProductFromCart,
+    editProductQtyFromCart
   };
 };

+ 10 - 13
src/utils/hooks/useCartDetail.ts

@@ -2,8 +2,8 @@
 
 import { useAppDispatch, useAppSelector } from "@/store/hooks";
 import { fetchCartDetail } from "@/store/slices/cart-slice";
-import { useCallback, useEffect } from "react";
-import { getCartToken } from "@/utils/getCartToken";
+import { useCallback } from "react";
+
 
 
 export function useCartDetail() {
@@ -12,20 +12,17 @@ export function useCartDetail() {
   const {cart, loading, error} = cartDetail;
 
   const getCartDetail = useCallback(async () => {
-    const token = getCartToken();
-    if (!token) {
-      return;
-    }
+
     dispatch(fetchCartDetail()); 
   }, [dispatch]);
 
-  // useCartDetail在组件中被调用时就会触发useEffect
-  // 初始化执行一次,获取购物车数据
-  useEffect(() => {
-    if (cart === null && !loading) {
-      getCartDetail();
-    }
-  }, [getCartDetail]);  
+  // // useCartDetail在组件中被调用时就会触发useEffect
+  // // 初始化执行一次,获取购物车数据
+  // useEffect(() => {
+  //   if (cart === null && !loading) {
+  //     getCartDetail();
+  //   }
+  // }, [getCartDetail]);  
 
   return {
     cartData: cart,

+ 63 - 0
src/utils/hooks/useGetCustomerAddress.ts

@@ -0,0 +1,63 @@
+"use client";
+
+import { useCallback } from "react";
+import { GET_CUSTOMER_ADDRESS } from "@/graphql";
+import {useApolloClient} from "@apollo/client/react";
+import { GetCustomerAddressVariables } from "@/types/customer/type";
+import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
+
+
+export function useGetCustomerAddress() {
+    const apolloClient = useApolloClient();
+
+
+    const getCustomerAddress = useCallback((param:GetCustomerAddressVariables) => {
+        return apolloClient.query({
+            query: GET_CUSTOMER_ADDRESS,
+            variables: param,
+            fetchPolicy: "no-cache",
+            context: {
+                fetchOptions: {
+                    cache: 'no-store',
+                },
+        
+            }
+        }).then((res) => {
+            const addressList =  res.data?.getCustomerAddresses?.edges?.map((edge) => {
+                    const item = {...edge.node};
+                    item.phone = normalizePhoneForForm(edge.node.phone, edge.node.country);
+                    return item;
+                    // return {
+                    //     cursor: edge.cursor,
+                    //     node: item
+                    // };
+                }) || [];
+            
+            return {
+                data: {
+                    list: addressList,
+                    pageInfo: res.data?.pageInfo,
+                    totalCount: res.data?.totalCount
+                },
+                error: false,
+                msg: ""
+            };
+        
+        }).catch((err) => {
+            return {
+                data: null,
+                error: true,
+                msg: err.message,
+            }
+        });
+    },[apolloClient]);
+
+   
+
+  return {
+    getCustomerAddress
+  };
+}
+
+
+

+ 14 - 47
src/utils/hooks/useGuestCartToken.ts

@@ -3,8 +3,12 @@
 import { useState, useRef } from "react";
 import { fetchHandler } from "../fetch-handler";
 import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
-import { encodeJWT, decodeJWT } from "@/utils/jwt-cookie";
-import { setCookie, deleteCookie, getCookie } from "@/utils/cookie-tools";
+import { 
+  setCookie, 
+  getGuestCartToken,
+  getGuestCartId,
+  deleteGuestCookie
+} from "@/utils/cookie-tools";
 import { CREATE_CART_TOKEN } from "@/graphql";
 
 // ---------------------------
@@ -13,41 +17,13 @@ import { CREATE_CART_TOKEN } from "@/graphql";
 export const useGuestCartToken = () => {
 
   const [token, setToken] = useState(() => {
-      const cookieToken = getCookie(GUEST_CART_TOKEN);
-      let guestCartToken: string | null = null;
-
-      if (cookieToken) {
-
-        const isGuest = getCookie(IS_GUEST) !== "false";
-        const decoded = decodeJWT<{
-          sessionToken: string;
-          cartId: number;
-          isGuest: boolean;
-        }>(cookieToken, isGuest);
-
-        if (decoded) {
-          guestCartToken = decoded.sessionToken;
-        }
-      }
+      const guestCartToken = getGuestCartToken();
       return guestCartToken;
   });
   const [cartId, setCartId] = useState(() => {
-    const cookieToken = getCookie(GUEST_CART_TOKEN);
-    let guestCartId: number | null = null;
-    if (cookieToken) {
-
-      const isGuest = getCookie(IS_GUEST) !== "false";
-      const decoded = decodeJWT<{
-        sessionToken: string;
-        cartId: number;
-        isGuest: boolean;
-      }>(cookieToken, isGuest);
-
-      if (decoded) {
-        guestCartId = decoded.cartId;
-      }
-    }
-    return guestCartId;
+    const guestCartId = getGuestCartId();
+    
+    return guestCartId ? Number(guestCartId) : null;
   });
   // const [isReady, setIsReady] = useState(true);
 
@@ -61,11 +37,9 @@ export const useGuestCartToken = () => {
     tokenPromiseRef.current = (async () => {
       if (tokenCreatedRef.current) {
         // Return existing raw token from cookie
-        const cookieVal = getCookie(GUEST_CART_TOKEN);
+        const cookieVal = getGuestCartToken();
         if (cookieVal) {
-          const isGuest = getCookie(IS_GUEST) !== "false";
-          const decoded = decodeJWT<{ sessionToken: string }>(cookieVal, isGuest);
-          return decoded?.sessionToken ?? null;
+          return cookieVal;
         }
         return null;
       }
@@ -87,14 +61,9 @@ export const useGuestCartToken = () => {
           return null;
         }
 
-        const newToken = encodeJWT({
-          sessionToken: cart.sessionToken,
-          cartId: cart.id,
-          isGuest: cart.isGuest,
-        });
         const newCartId = Number(cart.id);
 
-        setCookie(GUEST_CART_TOKEN, newToken,{encode: false});
+        setCookie(GUEST_CART_TOKEN, cart.sessionToken,{encode: false});
         setCookie(GUEST_CART_ID, String(newCartId));
         setCookie(IS_GUEST, String(cart?.isGuest));
 
@@ -123,8 +92,7 @@ export const useGuestCartToken = () => {
     tokenCreatedRef.current = false;
 
     // delete old
-    deleteCookie(GUEST_CART_TOKEN);
-    deleteCookie(GUEST_CART_ID);
+    deleteGuestCookie();
 
     await createGuestToken();
 
@@ -138,6 +106,5 @@ export const useGuestCartToken = () => {
     // isReady,
     createGuestToken,
     resetGuestToken,
-    deleteCookie,
   };
 };

+ 45 - 0
src/utils/hooks/useUserRegister.ts

@@ -0,0 +1,45 @@
+import {useCallback} from "react";
+import { useApolloClient } from "@apollo/client/react";
+import { CUSTOMER_REGISTRATION } from "@/graphql";
+import {
+    BagistoUser,
+    CreateUserVariables
+} from "@/types/types";
+
+interface UserRegisterResult {
+    data: BagistoUser | null;
+    msg: string;
+    error: boolean;
+}
+
+export const useUserRegister = () => {
+    const client = useApolloClient();
+    const userRegister = useCallback((params: CreateUserVariables) => {
+        return client.mutate({
+            mutation: CUSTOMER_REGISTRATION,
+            variables: params
+        }).then((res) => {
+            const resData = res.data?.createCustomer.customer ?? null; 
+            const result: UserRegisterResult = { 
+                data:resData,
+                msg: '',
+                error: false
+            };
+            return result;
+        }).catch((err) => {
+            const result: UserRegisterResult = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            return result;
+        });
+
+    },[client]);
+
+
+
+  return {
+    userRegister
+  };
+};

+ 0 - 42
src/utils/jwt-cookie.ts

@@ -1,42 +0,0 @@
-// 把游客token编码
-export const encodeJWT = (payload: object): string => {
-  try {
-    const jsonStr = JSON.stringify(payload);
-    const encodedPayload = btoa(encodeURIComponent(jsonStr));
-
-    const header = btoa(JSON.stringify({ alg: "none", typ: "JWT" }));
-
-    const token = `${header}.${encodedPayload}`;
-
-    return encodeURIComponent(token);
-  } catch (e) {
-    console.error("Error encoding JWT:", e);
-    return "";
-  }
-};
-
-
-export const decodeJWT = <T = any>(token: string, isGuest: boolean = true): T | null => {
-  try {
-    if (!isGuest) {
-      return { sessionToken: token } as unknown as T;
-    }
-
-    const decodedToken = decodeURIComponent(token);
-    const parts = decodedToken.split(".");
-
-    // if (parts.length !== 2) {
-    //   return null;
-    // }
-
-    const payloadPart = parts[1];
-    if (!payloadPart) return null;
-
-    const jsonStr = decodeURIComponent(atob(payloadPart));
-    console.log("jsonStr", jsonStr);
-    return JSON.parse(jsonStr) as T;
-  } catch (e) {
-    console.warn("Error decoding JWT:", e);
-    return null;
-  }
-};