Forráskód Böngészése

合并dev-checkoutproduct

fogwind 1 hete
szülő
commit
89a403f5b5
32 módosított fájl, 1614 hozzáadás és 434 törlés
  1. 4 1
      README.md
  2. 0 6
      package.json
  3. 6 0
      pnpm-workspace.yaml
  4. 0 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplay.tsx
  5. 0 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplayLoading.tsx
  6. 153 23
      src/app/(checkout)/checkout/_components/BillingAddressCheckout.tsx
  7. 448 0
      src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx
  8. 173 11
      src/app/(checkout)/checkout/_components/ShippingAddressCheckout.tsx
  9. 110 0
      src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx
  10. 1 1
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  11. 197 0
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  12. 68 374
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  13. 4 0
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  14. 6 0
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  15. 1 0
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  16. 1 1
      src/app/(checkout)/checkout/page.tsx
  17. 10 1
      src/components/common/LoginModal/LoginModal.tsx
  18. 1 1
      src/components/theme/ui/CommonModal.tsx
  19. 102 0
      src/components/theme/ui/LyDropDown.tsx
  20. 1 1
      src/graphql/cart/mutations/CreateApplyCoupon.ts
  21. 66 0
      src/graphql/cart/mutations/CreateRemoveCoupon.ts
  22. 1 1
      src/graphql/cart/mutations/RemoveCartItem.ts
  23. 1 1
      src/graphql/cart/mutations/UpdateCartItems.ts
  24. 2 1
      src/graphql/cart/mutations/index.ts
  25. 7 1
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  26. 44 0
      src/graphql/customer/query/GetCustomerAddress.ts
  27. 2 1
      src/graphql/customer/query/index.ts
  28. 9 8
      src/providers/ToastProvider.tsx
  29. 5 0
      src/types/cart/type.ts
  30. 48 0
      src/types/customer/type.ts
  31. 80 1
      src/utils/hooks/useAddToCart.ts
  32. 63 0
      src/utils/hooks/useGetCustomerAddress.ts

+ 4 - 1
README.md

@@ -349,7 +349,10 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
-46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)
+46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
+47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
+    对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
+
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054

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

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

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 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";

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 197 - 0
src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx


+ 68 - 374
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";
@@ -13,9 +11,6 @@ import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 import { 
-    ShipAddressFormData,
-    FullAddressFormData,
-    CreateCheckoutAddressVariables,
     PlaceOrderResult,
     PlaceOrderFunction,
     CreatePaymentInitiateVariables
@@ -24,198 +19,23 @@ 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 {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,
@@ -229,7 +49,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);
@@ -254,24 +74,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' || 
@@ -296,110 +106,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',
@@ -449,10 +172,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) {
@@ -510,7 +238,7 @@ export default function CheckoutWrapper({
 
         return true;
 
-    },[addressForm,showToast,saveCheckoutCart,dispatchCartStore]);
+    },[showToast,saveCheckoutCart,dispatchCartStore]);
     // 苹果支付下单
     const applepayPlaceOrder = useCallback(async () => { 
         const res = await createOrder();
@@ -567,91 +295,49 @@ export default function CheckoutWrapper({
             return resultData;
     }
 
-    const onCartDataChange = 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>
            
@@ -664,6 +350,15 @@ 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 
@@ -729,7 +424,6 @@ export default function CheckoutWrapper({
                 </div>
             </div>
 
-        </section> 
-        
-    );
+        </section>  
+    </>);
 };

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

+ 6 - 0
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -28,6 +28,12 @@ export default function PromotionsDetails({cartData}:{
                         <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>

+ 1 - 0
src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx

@@ -193,6 +193,7 @@ export default function CheckoutVipPlus({
         }
 
         <CommonModal 
+            destroyOnClose={true}
             isOpen={isShow}
             onClose={toggleModal}
             contentClassName={"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 rounded-xl bg-white px-3"}

+ 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 || ""}
             />

+ 10 - 1
src/components/common/LoginModal/LoginModal.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState} from "react";
+import {useState, useLayoutEffect} from "react";
 import { useRouter } from 'next/navigation';
 import { signIn } from "next-auth/react";
 import clsx from "clsx";
@@ -81,6 +81,15 @@ export default function LoginModal({
         }
     });
     const [activeTab, setActiveTab] = useState<TabFlag>('login');
+
+
+    useLayoutEffect(() => {
+        if(isShow) {
+            loginForm.reset();
+            registerForm.reset();
+        }
+    },[isShow]);
+
     const toggleLoginRegister = (tab: TabFlag) => {
         setActiveTab(tab);
     };

+ 1 - 1
src/components/theme/ui/CommonModal.tsx

@@ -42,7 +42,7 @@ export default function CommonModal({
     const [rootPddingTop, setRootPddingTop] = useState(0);
 
     const baseClassNname = "absolute box-border transition-transform duration-300 ease-out";
-    let applyClassName = "bottom-0 left-0 h-17/20 w-full bg-white";
+    let applyClassName = "bottom-0 left-0 h-17/20 w-full bg-white rounded-t-xl";
     if(contentClassName) {
         applyClassName = contentClassName;
     }

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

+ 1 - 1
src/graphql/cart/mutations/CreateApplyCoupon.ts

@@ -1,7 +1,7 @@
 import { gql, TypedDocumentNode } from "@apollo/client";
 import { CreateApplyCouponData } from "@/types/cart/type";
 
-export const CREATE_APPLY_COUPON: TypedDocumentNode<CreateApplyCouponData> = gql`
+export const CREATE_APPLY_COUPON: TypedDocumentNode<CreateApplyCouponData,{couponCode: string;}> = gql`
     mutation createApplyCoupon (
         $couponCode: String!
     ) {

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

+ 1 - 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!

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

@@ -4,4 +4,5 @@ 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_APPLY_COUPON} from "./CreateApplyCoupon"
+export {CREATE_APPLY_COUPON} from "./CreateApplyCoupon"
+export {CREATE_REMOVE_COUPON} from "./CreateRemoveCoupon";

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

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

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

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

@@ -151,6 +151,11 @@ export interface CreateApplyCouponPayload {
 export interface CreateApplyCouponData {
   createApplyCoupon: CreateApplyCouponPayload;
 }
+export interface CreateRemoveCouponData {
+  createRemoveCoupon: {
+    removeCoupon: CartDetail
+  };
+}
 // Merge Cart
 export interface CreateMergeCartPayload {
   mergeCart: CartDetail;

+ 48 - 0
src/types/customer/type.ts

@@ -22,4 +22,52 @@ export interface CustomerLoginResponse{
         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;
 }

+ 80 - 1
src/utils/hooks/useAddToCart.ts

@@ -1,5 +1,6 @@
 "use client";
 
+import { useCallback } from "react";
 import { useCustomToast } from "./useToast";
 import { useAppDispatch } from "@/store/hooks";
 import { addItem, clearCart } from "@/store/slices/cart-slice";
@@ -12,7 +13,7 @@ import {
 } 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,
@@ -26,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,
@@ -112,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,
@@ -202,5 +279,7 @@ export const useAddProduct = () => {
     onAddToRemove,
     onUpdateCart,
     isUpdateLoading,
+    deleteProductFromCart,
+    editProductQtyFromCart
   };
 };

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