Преглед на файлове

结账页产品展示,修改产品数量,删除产品;优惠券使用、取消

fogwind преди 1 седмица
родител
ревизия
91029c2e36

+ 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

Файловите разлики са ограничени, защото са твърде много
+ 110 - 0
src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx


Файловите разлики са ограничени, защото са твърде много
+ 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 - 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

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

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