fogwind 4 дней назад
Родитель
Сommit
4ed3d7f605
50 измененных файлов с 1032 добавлено и 441 удалено
  1. 52 9
      README.md
  2. 10 4
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  3. 171 107
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  4. 4 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  5. 16 6
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  6. 176 0
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  7. 37 23
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  8. 23 4
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  9. 18 5
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  10. 20 1
      src/app/(checkout)/checkout/continuetopay/page.tsx
  11. 1 2
      src/app/(checkout)/checkout/page.tsx
  12. 1 1
      src/app/(public)/customer/account/mycoupon/_components/GiftCardList.tsx
  13. 2 2
      src/app/(public)/customer/account/pointsreceivelist/_components/RedeemGiftCard.tsx
  14. 7 1
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  15. 6 16
      src/app/api/gift/add/route.ts
  16. 5 16
      src/app/api/gift/lists/route.ts
  17. 20 19
      src/app/api/gift/my-gift-cards/route.ts
  18. 12 66
      src/app/api/graphql/route.ts
  19. 13 5
      src/app/layout.tsx
  20. 6 11
      src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx
  21. 113 0
      src/components/home/HomeImageBanner.tsx
  22. 8 0
      src/graphql/cart/mutations/AddProductToCart.ts
  23. 8 0
      src/graphql/cart/mutations/CreateMergeCart.ts
  24. 8 0
      src/graphql/cart/mutations/GetCartItem.ts
  25. 8 0
      src/graphql/cart/mutations/RemoveCartItem.ts
  26. 8 0
      src/graphql/cart/mutations/UpdateCartItems.ts
  27. 3 3
      src/graphql/catalog/mutations/CreateProductReview.ts
  28. 3 3
      src/graphql/catalog/queries/GetProductByUrlKey.ts
  29. 24 12
      src/graphql/checkout/mutations/CreateCheckoutAddress.ts
  30. 12 0
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  31. 7 0
      src/graphql/customer/query/GetOrderDetails.ts
  32. 3 0
      src/lib/Airwallex/airwallexInit.ts
  33. 2 1
      src/lib/Airwallex/useAirwallexCard.ts
  34. 15 2
      src/lib/ApolloClientBrowser.ts
  35. 53 3
      src/lib/ApolloErrorHandler.ts
  36. 9 13
      src/lib/restApiClient.ts
  37. 7 0
      src/types/api/gift/my-gift-cards.ts
  38. 15 1
      src/types/cart/type.ts
  39. 37 0
      src/types/checkout/type.ts
  40. 7 0
      src/types/customer/order.ts
  41. 24 78
      src/utils/bagisto/index.ts
  42. 9 0
      src/utils/cartDetailTools.ts
  43. 10 4
      src/utils/hooks/useAddToCart.ts
  44. 16 6
      src/utils/hooks/useCheckoutAddress.ts
  45. 4 4
      src/utils/hooks/useCheckoutPaymentMethod.ts
  46. 4 4
      src/utils/hooks/useCheckoutShippingMethod.ts
  47. 3 2
      src/utils/hooks/usePlaceOrder.ts
  48. 6 3
      src/utils/hooks/useProductReview.ts
  49. 3 2
      src/utils/hooks/useSaveCheckoutCart.ts
  50. 3 2
      src/utils/hooks/useToast.ts

+ 52 - 9
README.md

@@ -294,8 +294,17 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
   - line: 913
 
 8. apollo client 升级后需要用新的方法设置返回数据的类型 -- 已完成
-9. Apollo client的错误处理机制 (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
+9. Apollo client的错误处理机制:
+   1. 一般try...catch 捕获;2. 如果是像useQuery这种返回结果中有error字段的,中通过error属性处理;3.还有的是通过hook的onError回调处理。
+   但是不管哪种处理方式,获取到的error对象都是一样的。
+    ---- (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
     ---- 下单错误处理(afterpay 使用支付失败的卡测试) afterpay金额40 报错
+    ---- 登录用户token过期错误处理
+    ---- serverGraphqlFetch 错误处理:1.业务错误,需要提示的在页面提示;2.throw的错误交给error.tsx和global-error.tsx处理
+    ---- rest api 错误处理
+    ---- graphql接口返回的错误有的有extensions字段,有的没有 具体可以查看nshop项目packages/Webkul/BagistoApi/src/Exception/下的异常类(一般看是否有getExtensions方法)
+    ---------骨架已搭好,待验证是否好用-------------
+
 10. 产品详情页代码优化 --- 已完成
 11. 代码eslint检查修改 -- 已完成
 12. 下单成功后需要重新创建购物车token -- 无需处理
@@ -319,29 +328,63 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
     - https://github.com/livechat/chat-widget-adapters
 28. cancelOrder接口报错   --  已解决
 29. 购物车数据管理方案 -- 已完成
-30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理;
-31. 当前货币等采用redux管理;
-32. 当前货币切换功能
+30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
+31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
+32. 当前货币切换功能      --- 已完成
 33. 登录用户购物车页获取未支付订单
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
     - 通过修改结账页数据初始化逻辑解决
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
 36. 学习set-state-in-effect,然后检查代码,看是否可以优化(特别是CommonModal组件,看能不能不使用useEffect实现)
-37. paypal provider 支付参数根据环境变量配置
+37. paypal provider 支付参数根据环境变量配置 --- 已完成
 38. CartAndUserActions 组件在支付结果页多次挂载问题
 39. 服务端去掉apollo client。apollo client 只在客户端使用; apollo 错误处理(第9条);
 40. Authorization 写入header优化;
+    ---- https://chatgpt.com/share/6a6acb5d-e184-83ea-acfc-839fe0d34caf
 41. 货币切换功能,货币切换后货币符号没有修改问题
 42. 错误处理 https://nextjs.org/docs/app/getting-started/error-handling
 43. redux中的user删除
 44. 关于订单id,管理后台订单列表里显示的是increment_id(数据库里的字段名),前端页面上用的是id(数据库里的字段名)对应接口里的orderId
 
+45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
+    -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
+
+
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 
-## 接入klarna
-1. 从服务端获取client_token
-2. 需要加载js sdk
-3. 获取authorization_token
+## 关于请求接口和错误处理
+接口请求有四种情况:服务端组件里的graphql请求和restful请求;客户端组件里的graphql请求和restful请求。
+### 服务端组件里的graphql请求
+服务端组件里的graphql请求统一使用serverGraphqlFetch方法。
+
+对于php接口返回的错误可以通过serverGraphqlFetch方法返回的error字段解析,根据情况在页面上提示用户;
+
+对于不是php接口引起的错误,也可以说是serverGraphqlFetch方法本身引起的错误,会被抛出,然后被error.tsx或global-error.tsx捕获处理。
+### 服务端组件里的restful请求。
+服务端组件里的restful请求使用restApiFetch方法直接请求php后端接口,根据body字段的结构解析php接口返回的错误;
+
+对于不是php接口引起的错误,也可以说是restApiFetch方法本身引起的错误,会被抛出,然后被error.tsx或global-error.tsx捕获处理。
+### 客户端组件中的graphql请求。
+客户端组件中的graphql请求使用的是apollo client。
+
+apollo client的错误处理([文档](https://www.apollographql.com/docs/react/data/error-handling)):
+- try...catch 捕获;
+- 如果是像useQuery这种返回结果中有error字段的,中通过error属性处理;
+- 通过hook的onError回调处理;
+
+不管哪种方式都可以使用`src\lib\ApolloErrorHandler.ts`里的handleApolloBusinessError方法处理业务错误;handleErrorForErrorLink处理其他错误。
+
+### 客户端组件里的restful请求
+客户端组件里的restful请求使用`src\lib\restApiClient.ts`里的clientFetch方法发起。
+
+客户端发起的restful请求都是请求的next.js的route handler。
+在route handler里请求php的接口。
+
+clientFetch方法没有使用try...catch,不主动抛出异常。在实际调用clientFetch的地方使用try...catch捕获代码错误。在clientFetch的返回结果中解析接口返回的错误。
+
+> fetch方法不会抛出异常。接口的状态码即使是500,也不会抛出异常,也是resolve。所以try...catch包裹fetch不会捕获到状态码非2xx的错误(除非你手动抛出异常,具体可以看MDN文档)。
+
+> graphql请求的状态码只有200。所以即使php接口报错,状态码也是200。
 
 
 ## 关于购物车数据管理方案

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

@@ -1,5 +1,6 @@
 "use client";
 
+import clsx from 'clsx';
 import { useRouter } from 'next/navigation'
 import {AirwallexCartNumberElementType} from "@/lib/Airwallex/airwallexInit";
 import PaypalButton from "./PaymentButton/PaypalButton";
@@ -297,11 +298,14 @@ export default function CheckoutPlaceOrder({
     return (
         <>
             <div className="relative w-full">
-                {paymentMethod === 'paypal_smart_button' && 
+        
+                <div className={clsx("w-full",{
+                    "hidden": paymentMethod !== 'paypal_smart_button'
+                })}>
                     <PaypalButton
                         createOrder={paypalCreateOrder}
                     />
-                }
+                </div>
                 {(paymentMethod === 'awxklarna' || paymentMethod === 'awxafterpay') &&
                     <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
                         onClick={airwallexPlaceOrder}
@@ -323,9 +327,11 @@ export default function CheckoutPlaceOrder({
                         Place Order
                     </button>
                 }
-                {paymentMethod === 'klarna' &&
+                <div className={clsx("w-full",{
+                    "hidden": paymentMethod !== 'klarna'
+                })}>
                     <KlarnaButton clickHandler={klarnaPlaceOrder} />
-                }
+                </div>
                 
 
             </div>

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

@@ -1,9 +1,17 @@
 "use client";
 
-import {useState, useRef, useEffect } from "react";
-import { LoadingSpinner } from "@components/common/LoadingSpinner";
-import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
+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";
+import { updateCart, clearCart } from "@/store/slices/cart-slice";
+import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 import { 
     ShipAddressFormData,
     FullAddressFormData,
@@ -12,31 +20,26 @@ import {
     PlaceOrderFunction,
     CreatePaymentInitiateVariables
 } from "@/types/checkout/type";
+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 { 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";
-import { updateCart } from "@/store/slices/cart-slice";
 import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
-import LoadingPaymentMethod from "./LoadingPaymentMethod";
+
 import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
 import CommonModal from "@/components/theme/ui/CommonModal";
-import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
-import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
-import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import { CartDetail,CartAddress } from "@/types/cart/type";
-import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
-import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
-import { clearCart } from "@/store/slices/cart-slice";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
-import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
+import CheckoutGiftCardGate from "./GiftCard/CheckoutGiftCardGate";
+import PromotionsDetails from "./PromotionsDetails";
+
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -250,7 +253,7 @@ export default function CheckoutWrapper({
     } = useCheckoutPaymentMethod();
 
     const { showToast } = useCustomToast();
-    const { getCheckoutAddress, saveCheckoutAddress } = useCheckoutAddress(loginEmail);
+    const { saveCheckoutAddress } = useCheckoutAddress(loginEmail);
 
     const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
     const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
@@ -283,6 +286,15 @@ export default function CheckoutWrapper({
         changePaymentMethodToloadSdk(selectedPaymentMethod);
     }, [selectedPaymentMethod]);
 
+
+    const dispatchCartStore = useCallback((action: "clear" | "update", payload?:Partial<CartDetail>) => {
+        if(action === "clear") {
+            dispatch(clearCart());
+        } else if(action === "update" && payload) {
+            dispatch(updateCart(payload));
+        }
+    },[dispatch]);
+
     const openAddressModal = () => {
         setAddressModalOpen(true);
 
@@ -308,35 +320,65 @@ export default function CheckoutWrapper({
         setAddressSaving(true);
         try {
 
-            const res = await saveCheckoutAddress(saveAddressParam);
-            console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
-            // 地址保存成功后重新获取运输方式和支付方式 
-            await getShippingMethod();
-            await getPaymentMethod();
-            
-            // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
-            const queryAddressRes = await getCheckoutAddress();
-            if(queryAddressRes !== null) {
-                addressForm.resetField('shippingAddressId',{
-                    defaultValue: queryAddressRes.shippingAddress.shippingAddressId
-                });
-                addressForm.resetField('billingAddressId',{
-                    defaultValue: queryAddressRes.billingAddress.billingAddressId
-                });
-
-      
-                // 同步地址到购物车详情
-                const newBillingAddress = billingAddressToCartAddress(queryAddressRes.billingAddress);
-                const newShippingAddress = shippingAddressToCartAddress(queryAddressRes.shippingAddress);
-                dispatch(updateCart({
-                    billingAddress: newBillingAddress,
-                    shippingAddress: newShippingAddress
-                }));
-                setCartData(Object.assign({...cartData},{
-                    billingAddress: {...newBillingAddress},
-                    shippingAddress: {...newShippingAddress}
-                }));
+            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) {
@@ -353,21 +395,23 @@ export default function CheckoutWrapper({
 
     const handlePaymentMethodChange =  async (method: string) => { 
         changePaymentMethodToloadSdk(method);
-        let shippingMethod = '-1';
-        if(methodShiippingRef.current) {
-            shippingMethod = methodShiippingRef.current.getSelectShipMethod();
-        }
+        // let shippingMethod = '-1';
+        // if(methodShiippingRef.current) {
+        //     shippingMethod = methodShiippingRef.current.getSelectShipMethod();
+        // }
         overlayLoading.start();
         const saveRes = await saveCheckoutCart({ 
-            shippingMethod: shippingMethod,
+            shippingMethod: '-1',
             paymentMethod: method,
-            couponCode: '-1'
+            couponCode: '-1',
+            giftCardNumber: '-1',
+            memberDiscount: '-1'
         });
         overlayLoading.stop();
         if(!saveRes.error) {
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
-                dispatch(updateCart(newCartDetail));
+                dispatchCartStore('update',newCartDetail);
                 setCartData({...newCartDetail});
             }
             return true;
@@ -377,18 +421,20 @@ export default function CheckoutWrapper({
         }
 
     };
-    const handleShippingMethodChange =  async (method: string) => { 
+    const handleShippingMethodChange =  useCallback(async (method: string) => { 
         overlayLoading.start();
         const saveRes = await saveCheckoutCart({ 
             shippingMethod: method,
             paymentMethod: '-1',
-            couponCode: '-1'
+            couponCode: '-1',
+            giftCardNumber: '-1',
+            memberDiscount: '-1'
         });
         overlayLoading.stop();
         if(!saveRes.error) {
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
-                dispatch(updateCart(newCartDetail));
+                dispatchCartStore('update',newCartDetail);
                 setCartData({...newCartDetail});
             }
             // 保存成功之后需要重新获取支付方式
@@ -397,11 +443,11 @@ export default function CheckoutWrapper({
             showToast(saveRes.msg, 'danger');
         }
 
-    };
+    },[saveCheckoutCart,showToast,getPaymentMethod,dispatchCartStore]);
 
 
     // 校验地址 运输方式,支付方式;
-    const validateCheckout = async () => { 
+    const validateCheckout = useCallback(async () => { 
         const addressValid = await addressForm.trigger();
 
         if(!addressValid) {
@@ -446,26 +492,30 @@ export default function CheckoutWrapper({
             const saveRes = await saveCheckoutCart({ 
                 shippingMethod: '-1',
                 paymentMethod: pmethod,
-                couponCode: '-1'
+                couponCode: '-1',
+                giftCardNumber: '-1',
+                memberDiscount: '-1'
             });
             if(!saveRes.error) {
                 if(saveRes.data) {
                     const newCartDetail = formatCartDetail(saveRes.data);
-                    dispatch(updateCart(newCartDetail));
+                    dispatchCartStore('update',newCartDetail);
                     setCartData({...newCartDetail});
                 }
+            } else {
+                showToast(saveRes.msg, 'danger');
             }
         }
 
         return true;
 
-    };
+    },[addressForm,showToast,saveCheckoutCart,dispatchCartStore]);
     // 苹果支付下单
-    const applepayPlaceOrder = async () => { 
+    const applepayPlaceOrder = useCallback(async () => { 
         const res = await createOrder();
         if(!res.error) {
             //创建订单成功后清空redux 购物车
-            dispatch(clearCart());  
+            dispatchCartStore('clear'); 
             return {
                 orderId: res.data?.gatewayOrderId ?? '',
                 webOrderId: res.data?.orderId ?? ''
@@ -474,24 +524,20 @@ export default function CheckoutWrapper({
             // 抛出错误让第三方支付能够捕获
             throw new Error("CREATE_ORDER_ERROR: " + res.msg);
         } 
-    };
-    // 下单
+    },[createOrder,dispatchCartStore]);
+    // 下单(下单和校验分开)
     const handlePlaceOrder: PlaceOrderFunction = async <T,>(params?: CreatePaymentInitiateVariables): Promise<PlaceOrderResult<T>>  => { 
-        // 校验地址
-        // overlayLoading.start();
-        // const validRes = await validateCheckout();
-        // if(validRes) {
             const res = await createOrder(params);
             if(res.error) {
                 overlayLoading.stop();
                 confirmDialog({
                     title: "Warning",
-                    content: res.msg + " Create order failed. Please try again.",
+                    content: res.msg + ". Create order failed. Please try again.",
                     noCancel: true,
                 }).then(() => {});
             } else {
                 //创建订单成功后清空redux 购物车
-                dispatch(clearCart());  
+                dispatchCartStore('clear');
             }
             const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
             let resultData: PlaceOrderResult<T>;
@@ -518,12 +564,16 @@ export default function CheckoutWrapper({
                 };
             }
             return resultData;
-        // } else {
-        //     overlayLoading.stop();
-        //     return {valid: false, data: null, otherData: null};
-        // }
     }
 
+    const onChangeGiftcard = async (paylod: CartDetail) => {
+        dispatchCartStore('update',paylod);
+        setCartData({...paylod});
+        // @todo 还要请求支付方式和运输方式
+        // await getShippingMethod();
+        //     await getPaymentMethod();
+        await Promise.all([getShippingMethod(),getPaymentMethod()]);
+    };
 
     // function tt() {
     //     const arr = [
@@ -603,42 +653,53 @@ export default function CheckoutWrapper({
             </div>
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium" onClick={testEruda}>Shipping Method</h3>
-                {shippingMethodLoading ?
-                
-                    <CheckoutShippingMethodLoading /> 
-                :
-                    <ShippingMethodCheckout 
-                        ref={methodShiippingRef}  
-                        shipMethods={shippingMethodDatas}
-                        errorMsg={shippingMethodError}  
-                        selectedShippingRate={selectedShippingRate}
-                        onShipMethodChange={handleShippingMethodChange}  
-                    />
-                }
+           
+                <ShippingMethodCheckout 
+                    ref={methodShiippingRef}  
+                    shipMethods={shippingMethodDatas}
+                    errorMsg={shippingMethodError}  
+                    selectedShippingRate={selectedShippingRate}
+                    shippingMethodLoading={shippingMethodLoading}
+                    onShipMethodChange={handleShippingMethodChange}  
+                />
+            </div>
 
+            <div className="mt-6 box-border px-4">
+                <CheckoutGiftCardGate 
+                    giftcardNumber={cartData.giftcardNumber || ''}
+                    formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
+                    onChangeGiftcard={onChangeGiftcard}
+                />
+            </div>
+            
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-24 font-medium">Promotion Details</h3>
+                <div className="w-full box-border mt-3">
+                    <PromotionsDetails cartData={cartData} />
+                </div>
             </div>
 
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium">Payment Method</h3>
-                { paymentMethodLoading && <LoadingPaymentMethod /> }
-                { (!paymentMethodLoading && paymentMethodError) && <p className="mt-3 text-red-500 text-ly-12">{paymentMethodError}</p>}
-                { (!paymentMethodLoading && !paymentMethodError) && 
-                    <PaymentMethodCheckout 
-                        ref={methodPaymentRef}
-                        paymentMethods={paymentMethodDatas} 
-                        selectedPaymentMethod={selectedPaymentMethod}
-                        onPaymentMethodChange={handlePaymentMethodChange}
-                    />
-                }
+                <PaymentMethodCheckout 
+                    ref={methodPaymentRef}
+                    paymentMethods={paymentMethodDatas} 
+                    selectedPaymentMethod={selectedPaymentMethod}
+                    paymentMethodLoading={paymentMethodLoading}
+                    loadError={paymentMethodError}
+                    onPaymentMethodChange={handlePaymentMethodChange}
+                />
+                
                 
             </div>
             <div className="mt-3 box-border px-4 pb-8">
                 <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
                 <div className="mt-6 w-full">
-                    { paymentMethodLoading ?
-                        <LoadingCheckoutPlaceOrder />
-                    :
-                        (selectedPaymentMethod === 'applepay' ?
+                    { paymentMethodLoading && <LoadingCheckoutPlaceOrder />}
+                    
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'applepay' || paymentMethodLoading
+                    })}>
                         <PaypalApplepayButton 
                             grandTotal={cartData.grandTotal}
                             billingContact={cartData.billingAddress}
@@ -646,14 +707,17 @@ export default function CheckoutWrapper({
                             validateCheckout={validateCheckout}
                             createOrder={applepayPlaceOrder}
                         />
-                        :
+                    </div>
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod === 'applepay' || paymentMethodLoading
+                    })}>
                         <CheckoutPlaceOrder 
                             paymentMethod={selectedPaymentMethod}
                             validateCheckout={validateCheckout}
                             clickPlaceOrder={handlePlaceOrder}
-                        />)
-                        
-                    }
+                        />
+                    </div>  
+                    
                 </div>
             </div>
 

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

@@ -172,6 +172,10 @@ export default function ContinueToPayOrderInfo({
                     <p className="text-ly-12 leading-ly-20">Discount</p>
                     <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.discountAmount}</p>
                 </div>
+                <div className="flex justify-between mt-3 first:mt-0">
+                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                    <p className="text-ly-12 leading-ly-20">-{currencySymbol}{orderDetail.giftcardAmount}</p>
+                </div>
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>

+ 16 - 6
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import clsx from "clsx";
 import Image from "next/image";
 import { use,useState,useCallback } from "react";
 import { useForm, get, useWatch } from "react-hook-form";
@@ -505,12 +506,15 @@ export function PaymentMethodContinueTpPay({
                 </p>
 
                 <div className="mt-6 w-full">
-                    {selectedPaymentMethod === 'paypal_smart_button' && 
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'paypal_smart_button'
+                    })}>
                         <PaypalButton
                             createOrder={paypalRepayOrder}
                             isRePay={true}
                         />
-                    }
+                    </div>
+                    
                     {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
                         <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
                             onClick={airwallexRepay}
@@ -534,7 +538,10 @@ export function PaymentMethodContinueTpPay({
                             Place Order
                         </button>
                     }
-                    {selectedPaymentMethod === 'applepay' &&
+
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'applepay'
+                    })}>
                         <PaypalApplepayButton 
                             grandTotal={orderDetail.grandTotal}
                             billingContact={billingContact}
@@ -543,12 +550,15 @@ export function PaymentMethodContinueTpPay({
                             createOrder={applepayPlaceOrder}
                             isRePay={true}
                         />
-                    }
-                    {selectedPaymentMethod === 'klarna' &&
+                    </div>
+                   
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'klarna'
+                    })}>
                         <KlarnaButton 
                             clickHandler={klarnaPlaceOrder}
                         />
-                    }
+                    </div>
 
                 </div>
             </div>

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


+ 37 - 23
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,13 +1,15 @@
 "use client";
 
+import clsx from "clsx";
 import Image from "next/image";
-import { Ref, useImperativeHandle } from "react";
+import { Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import { CheckoutPaymentMethod } from "@/types/checkout/type";
 import { AirwallexCardElements } from "@/lib/Airwallex/airwallexInit";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCard} from "@/lib/Airwallex/useAirwallexCard";
+import LoadingPaymentMethod from "./LoadingPaymentMethod";
 
 /**
  * 关于如何确定默认选中哪个支付方式:
@@ -24,49 +26,39 @@ export function PaymentMethodCheckout({
     ref,
     paymentMethods,
     selectedPaymentMethod,
+    paymentMethodLoading,
+    loadError,
     onPaymentMethodChange,
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
+    paymentMethodLoading: boolean;
+    loadError: string | null;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 
     const { showToast } = useCustomToast();
 
 
-    const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
+    const {airwallexCardState,getCardElements} = useAirwallexCard(!paymentMethodLoading,{
         cardNumber: 'airwallex_cardNumber', 
         cvc: 'airwallex_cvc', 
         expiry: 'airwallex_expiry'
     });
   
-    let defaultValue = '';
- 
-    const findItem = paymentMethods.find((item) => item.method === selectedPaymentMethod);
-    if(findItem !== undefined) {
-        defaultValue = selectedPaymentMethod;
-
-    } else {
-        paymentMethods.forEach((item) => {
-            if(item.method === 'paypal_smart_button') {
-                defaultValue = item.method;
-            }
-        });
-    }
-
-  
     const {
         register,
         getValues,
         trigger,
+        reset,
         formState: { errors }
     } = useForm({
         mode: "onChange",
         reValidateMode: "onChange", 
-        defaultValues: {
-            paymentMethod: defaultValue,
-        }
+        // defaultValues: {
+        //     paymentMethod: defaultValue,
+        // }
     });
 
     // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
@@ -77,6 +69,23 @@ export function PaymentMethodCheckout({
         onPaymentMethodChange(event.target.value);                                
     };
 
+    useEffect(() => {
+        let defaultValue = '';
+ 
+        const findItem = paymentMethods.find((item) => item.method === selectedPaymentMethod);
+        if(findItem !== undefined) {
+            defaultValue = selectedPaymentMethod;
+
+        } else {
+            paymentMethods.forEach((item) => {
+                if(item.method === 'paypal_smart_button') {
+                    defaultValue = item.method;
+                }
+            });
+        }
+        reset({ paymentMethod: defaultValue });
+    },[paymentMethods,selectedPaymentMethod]);
+
     useImperativeHandle(ref, () => {
         return {
             // 获取用户选择的支付方式
@@ -132,8 +141,13 @@ export function PaymentMethodCheckout({
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
     
-    return (
-        <form name="shipping method" className="w-full">
+    return (<>
+
+        { paymentMethodLoading && <LoadingPaymentMethod /> }
+        { (!paymentMethodLoading && loadError) && <p className="mt-3 text-red-500 text-ly-12">{loadError}</p>}
+        <form name="shipping method" className={clsx("w-full", {
+            "hidden": (paymentMethodLoading || loadError)
+        })}>
         
             <div className="mt-3 w-full">
                 <ul className="w-full">
@@ -186,7 +200,7 @@ export function PaymentMethodCheckout({
             </div>
             
         </form>
-    );
+    </>);
   
 }
 

+ 23 - 4
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -1,15 +1,34 @@
 "use client";
 
-
+import { CartDetail } from "@/types/cart/type";
 
 
 // 金额明细
-export default function PromotionsDetails() { 
+export default function PromotionsDetails({cartData}:{
+    cartData: CartDetail
+}) { 
 
 
     return (
-        <div className="">
-
+        <div className="w-full box-border">
+            <ul className="w-full">
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Subtotal</span>
+                    <span>{cartData.formattedSubtotal}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Shipping Method</span>
+                    <span>{cartData.formattedShippingAmount}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Gift Card</span>
+                    <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Grand Total</span>
+                    <span>{cartData.formattedGrandTotal}</span>
+                </li>
+            </ul>
         </div>
     );
 }

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

@@ -1,10 +1,11 @@
 "use client";
 
-
+import clsx from "clsx";
 import { useEffect, useImperativeHandle, Ref  } from "react";
 import { useForm, get } from "react-hook-form";
 
 import {CheckoutShippingRate} from "@/types/checkout/type";
+import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 
 
 
@@ -24,12 +25,14 @@ export function ShippingMethodCheckout({
     errorMsg,
     shipMethods,
     selectedShippingRate,
+    shippingMethodLoading,
     onShipMethodChange
 }: {
     ref: Ref<RefShippingMethodsHandle>;
     errorMsg: string | null;
     shipMethods: CheckoutShippingRate[];
     selectedShippingRate: string | null;
+    shippingMethodLoading: boolean;
     onShipMethodChange: (value: string) => void;
 }) {
 
@@ -61,7 +64,7 @@ export function ShippingMethodCheckout({
         }
         
     },[]);
-    // 根据cartDetail 的loading判断,是true 返回,是false时才往下执行
+    
     useEffect(() => { 
         if(shipMethods.length > 0 ) {
             let defaultValue = '';
@@ -72,6 +75,10 @@ export function ShippingMethodCheckout({
                     defaultValue = selectedShippingRate;
                 }
             }
+            if(defaultValue) {
+                reset({ shippingMethod: defaultValue });
+            }
+            /*
             if(!defaultValue) {
                 findItem = shipMethods.find((item) => item.price === 0) || shipMethods[0];
                 defaultValue = findItem.method;
@@ -83,12 +90,18 @@ export function ShippingMethodCheckout({
             if(defaultValue !== selectedShippingRate) {
                 onShipMethodChange(defaultValue);
             }
+            */
         }
         
     }, [shipMethods,selectedShippingRate]);
     
-    return (
-        <form name="shipping method" className="w-full mt-3">
+    return (<>
+       
+        {shippingMethodLoading && <CheckoutShippingMethodLoading />}
+    
+        <form name="shipping method" className={clsx("w-full mt-3",{
+            "hidden": shippingMethodLoading
+        })}>
             {errorMsg && <p className="text-red-500 text-ly-12">{errorMsg}</p>}
             {shipMethods.length === 0 && <p className="text-red-500 text-ly-12">Please complete address data to get shipping methods list.</p>}
             <div className="w-full">
@@ -129,7 +142,7 @@ export function ShippingMethodCheckout({
             </div>
             
         </form>
-    );
+    </>);
   
 }
 

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

@@ -1,5 +1,6 @@
 import {Suspense} from "react";
 import { getServerSession } from "next-auth";
+import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
 import { authOptions } from "@utils/auth";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
@@ -43,7 +44,7 @@ export default async function ContinueToPay({searchParams}: {
 
     const query = await searchParams;
     const orderid = query?.orderid;
-    const {data: orderDetailResponse} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
+    const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         variables: {
             id: orderid as string
@@ -62,7 +63,25 @@ export default async function ContinueToPay({searchParams}: {
         }
     });
       
+    if(orderDetailError) {
+        return (
+            <div className="box-border px-4 w-full">
+            
+                
+                <div className="w-12 h-12 mx-auto mt-9">
+                    <svg className="failure block w-full h-full" width="200" height="200" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27635"><path d="M736 332.8L556.8 512l179.2 179.2-44.8 44.8L512 556.8l-179.2 179.2-44.8-44.8L467.2 512 288 332.8l44.8-44.8L512 467.2l179.2-179.2 44.8 44.8z" p-id="27636"></path><path d="M512 0a512 512 0 1 1 0 1024A512 512 0 0 1 512 0z m0 64a448 448 0 1 0 0 896A448 448 0 0 0 512 64z" p-id="27637"></path></svg>
+                </div>
+                <p className="text-ly-16 text-center mt-6">
+                    Get Pending Order failed:
+                    <span className="text-ly-16">{orderDetailError.message}</span>
+                </p>
 
+                <Link href="/" className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                    Continue Shopping
+                </Link>
+            </div>
+        );
+    }
     return (
         <div className="w-full pb-8">
         

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

@@ -25,8 +25,7 @@ export default async function CheckoutPage() {
 
 
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
-        query: GET_CART_ITEM,
-        operationName:'GetCartItem'
+        query: GET_CART_ITEM
     });
     if(cartDetailsRes.createReadCart === null) {
         redirect('/', RedirectType.replace);

+ 1 - 1
src/app/(public)/customer/account/mycoupon/_components/GiftCardList.tsx

@@ -32,7 +32,7 @@ const GiftCardList: React.FC = () => {
   useEffect(() => {
     const load = async () => {
       const resp = await clientFetch("/api/gift/my-gift-cards");
-      const dataObj = resp.data.data;
+      const dataObj = resp.data;
       setGiftCards((prev) => [...prev, ...dataObj]);
       console.log("完整返回:", dataObj);
     };

+ 2 - 2
src/app/(public)/customer/account/pointsreceivelist/_components/RedeemGiftCard.tsx

@@ -19,10 +19,10 @@ const RedeemGiftCard: React.FC = () => {
   useEffect(() => {
     const load = async () => {
       const resp = await clientFetch("/api/gift/lists");
-      const dataObj = resp.data.data.lists;
+      const dataObj = resp.data.lists;
       const dataArray = Object.values(dataObj);
       setList(dataObj);
-      setmyPoints(resp.data.data.myPoints);
+      setmyPoints(resp.data.myPoints);
        console.log("完整返回:", dataArray,myPoints);
     };
 

+ 7 - 1
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -9,7 +9,9 @@ import { useConfig } from "@utils/hooks/useConfig";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import Faqs from "./Faqs";
 
-
+/**
+ * 订单详情里的金额的货币符号不会根据用户选择的货币改变 ,而是固定为下单时使用的货币
+ */
 
 function getProductAdditionalInfo(productItem: ProductItemAdditional) { 
     const attributeKeys = Object.keys(productItem.attributes);
@@ -186,6 +188,10 @@ export default function OrderDetailWrapper({
                     <p className="text-ly-12 leading-ly-20">Discount</p>
                     <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.discountAmount}</p>
                 </div>
+                <div className="flex justify-between mt-3 first:mt-0">
+                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                    <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.giftcardAmount}</p>
+                </div>
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>

+ 6 - 16
src/app/api/gift/add/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 
 export async function POST(req: NextRequest) {
@@ -21,28 +20,19 @@ export async function POST(req: NextRequest) {
         });
         // 打印后端原始返回结构
         console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
+
     } catch (error) {
         console.log('/gift/add --- ', error); // 调试用
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

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

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
 export async function GET(req: NextRequest) {
@@ -12,27 +11,17 @@ export async function GET(req: NextRequest) {
             cache:'no-store',
             guestToken,
         });
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 20 - 19
src/app/api/gift/my-gift-cards/route.ts

@@ -1,38 +1,39 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
-import type { GiftCardRespBody,FetchResult  } from '@/types/api/gift/my-gift-cards';
+import type { GiftCardRespBody } from '@/types/api/gift/my-gift-cards';
 export async function GET(req: NextRequest) {
     try {
         const guestToken = getAuthToken(req);
-        const response = await restApiFetch<FetchResult<GiftCardRespBody>>({
+
+        const searchParams = req.nextUrl.searchParams;
+        const page = searchParams.get('page') || '1';
+        const per_page = searchParams.get('per_page') || '10';
+
+        const response = await restApiFetch<{
+            data: GiftCardRespBody,
+            variables: {page: string;per_page: string;}
+        }>({
+            variables: {
+                page,
+                per_page
+            },
             api: `/gift/my-gift-cards`,
             method:'GET',
             cache:'no-store',
             guestToken,
         });
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
-        return NextResponse.json(
+         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 12 - 66
src/app/api/graphql/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { bagistoFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 import {
     CREATE_ADD_PRODUCT_IN_CART,
@@ -62,55 +61,7 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
     const guestToken = getAuthToken(req);
     const query = ALLOWED_OPERATIONS[operationName];
     const finalVariables = variables;
-
-    /*if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
-        finalVariables = { ...variables };
-    }
-
-    if (operationName === 'CreateCheckoutPaymentMethod') {
-        finalVariables = {
-            ...variables,
-            successUrl: variables?.successUrl ?? `payment/success`,
-            failureUrl: variables?.failureUrl ?? `payment/failure`,
-            cancelUrl: variables?.cancelUrl ?? `payment/cancel`
-        };
-    }*/
-
-    /*if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
-        finalVariables = {
-            billingFirstName: body.billingFirstName,
-            billingLastName: body.billingLastName,
-            billingEmail: body.billingEmail,
-            billingAddress: body.billingAddress,
-            billingCity: body.billingCity,
-            billingCountry: body.billingCountry,
-            billingState: body.billingState,
-            billingPostcode: body.billingPostcode,
-            billingPhoneNumber: body.billingPhoneNumber,
-            billingCompanyName: body.billingCompanyName,
-            useForShipping: body.useForShipping,
-            ...(!body.useForShipping && {
-                shippingFirstName: body.shippingFirstName,
-                shippingLastName: body.shippingLastName,
-                shippingEmail: body.billingEmail,
-                shippingAddress: body.shippingAddress,
-                shippingCity: body.shippingCity,
-                shippingCountry: body.shippingCountry,
-                shippingState: body.shippingState,
-                shippingPostcode: body.shippingPostcode,
-                shippingPhoneNumber: body.shippingPhoneNumber,
-                shippingCompanyName: body.shippingCompanyName,
-            })
-        };
-    }*/
-
-    /*if (operationName === 'createAddProductInCart' && body.productId) {
-        finalVariables = {
-            cartId: body.cartId ?? null,
-            productId: body.productId,
-            quantity: body.quantity,
-        };
-    }*/
+ 
     return {
         query,
         variables: finalVariables,
@@ -151,27 +102,22 @@ export async function POST(req: NextRequest) {
 
         
         const response = await bagistoFetch<any>(fetchOption);
-        // console.log('response ------ ', response);
-        return NextResponse.json({
-            data: response.body.data,
+        console.log('bagistoFetch response ------ ', response);
+        return NextResponse.json(response.body,{
+            status: response.status
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
+        console.log('bagistoFetch ERROR ------ ', error);
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                errors:[
+                    {
+                        message: error instanceof Error ? error.message : "Network error"
+                    }
+                ],
+                data:null
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 13 - 5
src/app/layout.tsx

@@ -68,6 +68,9 @@ export default async function RootLayout({
   }
 
   const cookieStore = await cookies();
+  ////////eruda调试/////////
+  const loadEruda = cookieStore.get('9527_LOAD_ERUDA_3345678');
+  ////////eruda调试////////
   const currentCurrency = cookieStore.get(CURRENT_CURRENCY);
   const currentLocale = cookieStore.get(CURRENT_LOCAL);
   const currentChannel = cookieStore.get(CURRENT_CHANNEL);
@@ -85,10 +88,16 @@ export default async function RootLayout({
   const {data: countryResData} = await serverGraphqlFetch<{
       countries: Country[] 
   }>({
-    query: GET_COUNTRIES
+    query: GET_COUNTRIES,
+    cache: 'force-cache',
+    revalidate: 3600,
+    tags: ["config-country-list"]
   });
   const {data: currencyResData} = await serverGraphqlFetch<CurrenciesData>({
-    query: GET_CURRENCIES
+    query: GET_CURRENCIES,
+    cache: 'force-cache',
+    revalidate: 3600,
+    tags: ["config-currency-list"]
   });
  
   const countries = countryResData.countries;
@@ -106,8 +115,7 @@ export default async function RootLayout({
   };
 
   const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
-    query: GET_CART_ITEM,
-    operationName:'GetCartItem'
+    query: GET_CART_ITEM
   });
   
   const cartDetails = cartDetailsRes.createReadCart?.readCart ?? null;
@@ -115,7 +123,7 @@ export default async function RootLayout({
     <html lang="en" suppressHydrationWarning>
       <head>
         <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"></meta>
-        <Script src="https://cdn.jsdelivr.net/npm/eruda" strategy="beforeInteractive" />
+        {(loadEruda && loadEruda.value === '1') && <Script src="https://cdn.jsdelivr.net/npm/eruda" strategy="beforeInteractive" />}
       </head>
       <body className={clsx(
         "min-h-screen font-outfit text-foreground bg-background antialiased",

+ 6 - 11
src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx

@@ -4,17 +4,17 @@ import { useAppDispatch } from "@/store/hooks";
 import { openAddToCartDialog } from '@/store/slices/addToCartDialogSlice';
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
 import { useLazyQuery } from '@apollo/client/react';
-import { SingleProductResponse } from "@/components/catalog/type";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import ShoppingCartIcon from "@components/common/icons/ShoppingCartIcon";
 import { useCustomToast } from "@utils/hooks/useToast";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string}) {
     const dispatch = useAppDispatch();
     // const [loading, setLoading] = useState(false);
     const { showToast } = useCustomToast();
 
-    const [getContent, { loading }] = useLazyQuery<SingleProductResponse>(GET_PRODUCT_BY_URL_KEY,{
+    const [getContent, { loading }] = useLazyQuery(GET_PRODUCT_BY_URL_KEY,{
         fetchPolicy: 'network-only'
     });
     const openDialog = async () => {
@@ -24,7 +24,7 @@ export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string
                 variables: { urlKey: productUrlKey }
             });
 
-            console.log('product ----- ', result);// 返回的数据中有__typename,如何过滤 @todo
+            console.log('product ----- ', result);
 
             const productData = result?.data?.product;
             if(result.error) {
@@ -40,14 +40,9 @@ export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string
             
         } catch (error) {
             console.log('error ----- ', error);
-            let errorMessage = "Network Error";
-            if (error instanceof Error) {
-                errorMessage = error.message;
-            } else if (typeof error === 'object' && error !== null && 'message' in error) {
-                errorMessage = String((error as any).message);
-            }
-            
-            showToast(errorMessage, "danger");
+            handleApolloBusinessError(error, (err) => {
+                showToast(err.message, "danger");
+            });
         }
        
     }

+ 113 - 0
src/components/home/HomeImageBanner.tsx

@@ -3,7 +3,12 @@
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch";
+import makeClient from "@/lib/ApolloClientBrowser";
 
+import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON } from "@/graphql";
+import { useMutation, useLazyQuery } from "@apollo/client/react";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
+import { useCustomToast } from "@/utils/hooks/useToast";
 // interface ImageCarouselProps {
 //     options: {
 //         images: {
@@ -14,8 +19,116 @@ import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch"
 //     };
 // }
 export default function HomeImageBanner() {
+  const { showToast } = useCustomToast();
+  const apolloClient = makeClient();
+  // const res = useQuery(GET_COUNTRY_STATES, {
+  //         variables: {
+  //             countryId: 1000
+  //         }
+  //     });
+  //     console.log('query state --- res', res);
+  const ttt = async () => {
+    try {
+      const res = await apolloClient.mutate({
+        mutation: GET_CART_ITEM,
+        fetchPolicy: 'network-only', // 按需调整
+      });
+      console.log('----------',res);
+    } catch(error) {
+      console.log('ttt----------',error);
+      handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+      });
+    }  
+  }
+  // 76|nlq9qGztR7qmSS72Kal56NCjhRRVrLCeVOdZdLLYa2c769e9
+const [getContent] = useLazyQuery(GET_FILTER_ATTRIBUTES,{
+        fetchPolicy: 'network-only'
+    });
+
+  const testLazyQuery = async () => { 
+      try {
+            const result = await getContent();
+            console.log('lazy query res---', result);
+      } catch(err) {
+          console.log('lazy query ---- ', err);
+      }
+  }
+
+    // useMutation
+    
+
+
+    const [mutateAsync] = useMutation(
+        GET_CART_ITEM,
+        {
+          onCompleted: (res) => {
+            console.log('useMutation onCompleted res ----- 0',res);
+          },
+    
+          onError: (err) => {
+            console.log('useMutation onCompleted err ----- 0',err);
+          },
+        },
+      );
+
+
+    const testMutation = async () => { 
+        
+            const res = await mutateAsync();
+        console.log('testMutation ---',res)
+        
+    };
+    const testFilter= () => {
+            return apolloClient.query({
+                query: GET_FILTER_ATTRIBUTES,
+                fetchPolicy: "no-cache",
+                context: {
+                    fetchOptions: {
+                        cache: 'no-store',
+                    },
+            
+                },
+                // variables: {
+                //     orderId: Number(orderid)
+                // }
+            }).then((res) => {
+                
+                console.log('testFilter res ---- ',res);
+            
+            
+            }).catch((err) => {
+                console.error('testFilter error ---- ',err);
+                handleApolloBusinessError(err,(error) => {
+          showToast(error.message, "danger");
+      });
+            });
+        };
+    
+        const testApplyCoupon = async () => { 
+            try {
+                await apolloClient.mutate({
+                    mutation: CREATE_APPLY_COUPON,
+                    variables: {
+                        couponCode: 'BIRTHDAY20'
+                    },
+                    fetchPolicy: 'network-only', // 按需调整
+                });
+            } catch (err) {
+                console.log('testApplyCoupon error ---- ',err);
+            }
+        };
+  
   return (
     <>
+    <p>
+      <button onClick={ttt}>get cart detail</button>
+    </p>
+
+    <p><button onClick={testLazyQuery}>testLazyQuery</button></p>
+    <p><button onClick={testMutation}>testMutation</button></p>
+    <p><button onClick={testFilter}>testFilter</button></p>
+    <p><button onClick={testApplyCoupon}>testApplyCoupon</button></p>
     <Swiper
       spaceBetween={50}
       slidesPerView={3}

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

@@ -41,9 +41,15 @@ export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData> = gql`
             }
           }
         }
+        isVip
+        vipDiscountAmount
+        formattedVipDiscountAmount
+        giftcardNumber
         subtotal
         subTotalInclTax
         discountAmount
+        giftcardAmount
+        vipPlusAmount
         taxAmount
         taxTotal
         shippingAmount
@@ -51,7 +57,9 @@ export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData> = gql`
         grandTotal
         formattedSubtotal
         formattedSubTotalInclTax
+        formattedGiftcardAmount
         formattedDiscountAmount
+        formattedVipPlusAmount
         formattedTaxAmount
         formattedTaxTotal
         formattedShippingAmount

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

@@ -33,9 +33,15 @@ export const CREATE_MERGE_CART: TypedDocumentNode<CreateMergeCartData> = gql`
             }
           }
         }
+        isVip
+        vipDiscountAmount
+        formattedVipDiscountAmount
+        giftcardNumber
         subtotal
         subTotalInclTax
         discountAmount
+        giftcardAmount
+        vipPlusAmount
         taxAmount
         taxTotal
         shippingAmount
@@ -43,7 +49,9 @@ export const CREATE_MERGE_CART: TypedDocumentNode<CreateMergeCartData> = gql`
         grandTotal
         formattedSubtotal
         formattedSubTotalInclTax
+        formattedGiftcardAmount
         formattedDiscountAmount
+        formattedVipPlusAmount
         formattedTaxAmount
         formattedTaxTotal
         formattedShippingAmount

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

@@ -27,9 +27,15 @@ export const GET_CART_ITEM: TypedDocumentNode<GetCartItemData> = gql`
             }
           }
         }
+        isVip
+        vipDiscountAmount
+        formattedVipDiscountAmount
+        giftcardNumber
         subtotal
         subTotalInclTax
         discountAmount
+        giftcardAmount
+        vipPlusAmount
         taxAmount
         taxTotal
         shippingAmount
@@ -37,7 +43,9 @@ export const GET_CART_ITEM: TypedDocumentNode<GetCartItemData> = gql`
         grandTotal
         formattedSubtotal
         formattedSubTotalInclTax
+        formattedGiftcardAmount
         formattedDiscountAmount
+        formattedVipPlusAmount
         formattedTaxAmount
         formattedTaxTotal
         formattedShippingAmount

+ 8 - 0
src/graphql/cart/mutations/RemoveCartItem.ts

@@ -27,9 +27,15 @@ export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData> = gql`
             }
           }
         }
+        isVip
+        vipDiscountAmount
+        formattedVipDiscountAmount
+        giftcardNumber
         subtotal
         subTotalInclTax
         discountAmount
+        giftcardAmount
+        vipPlusAmount
         taxAmount
         taxTotal
         shippingAmount
@@ -37,7 +43,9 @@ export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData> = gql`
         grandTotal
         formattedSubtotal
         formattedSubTotalInclTax
+        formattedGiftcardAmount
         formattedDiscountAmount
+        formattedVipPlusAmount
         formattedTaxAmount
         formattedTaxTotal
         formattedShippingAmount

+ 8 - 0
src/graphql/cart/mutations/UpdateCartItems.ts

@@ -36,9 +36,15 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
             }
           }
         }
+        isVip
+        vipDiscountAmount
+        formattedVipDiscountAmount
+        giftcardNumber
         subtotal
         subTotalInclTax
         discountAmount
+        giftcardAmount
+        vipPlusAmount
         taxAmount
         taxTotal
         shippingAmount
@@ -46,7 +52,9 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
         grandTotal
         formattedSubtotal
         formattedSubTotalInclTax
+        formattedGiftcardAmount
         formattedDiscountAmount
+        formattedVipPlusAmount
         formattedTaxAmount
         formattedTaxTotal
         formattedShippingAmount

+ 3 - 3
src/graphql/catalog/mutations/CreateProductReview.ts

@@ -1,10 +1,10 @@
-import { gql } from "@apollo/client";
-
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { ProductReviewResponse } from "@/types/review";
 /**
  * Create a product review
  * @param input - Review input data
  */
-export const CREATE_PRODUCT_REVIEW = gql`
+export const CREATE_PRODUCT_REVIEW: TypedDocumentNode<ProductReviewResponse> = gql`
   mutation CreateProductReview($input: createProductReviewInput!) {
     createProductReview(input: $input) {
       productReview {

+ 3 - 3
src/graphql/catalog/queries/GetProductByUrlKey.ts

@@ -1,11 +1,11 @@
-import { gql } from "@apollo/client";
+import { gql,TypedDocumentNode } from "@apollo/client";
 import { PRODUCT_DETAILED_FRAGMENT } from "../fragments";
-
+import { SingleProductResponse } from "@/components/catalog/type";
 /**
  * Fetch a single product by URL key with all details
  * @param urlKey - Product URL key
  */
-export const GET_PRODUCT_BY_URL_KEY = gql`
+export const GET_PRODUCT_BY_URL_KEY: TypedDocumentNode<SingleProductResponse> = gql`
   ${PRODUCT_DETAILED_FRAGMENT}
 
   query GetProductById($urlKey: String!) {

+ 24 - 12
src/graphql/checkout/mutations/CreateCheckoutAddress.ts

@@ -1,16 +1,5 @@
 import { gql, TypedDocumentNode } from "@apollo/client";
-
-type CreateCheckoutAddressResponse = {
-  createCheckoutAddress:{
-      checkoutAddress: {
-          success: boolean;
-          message: string;
-          id: string;
-          cartToken: string;
-      };
-  }
-  
-};
+import  {CreateCheckoutAddressResponse} from "@/types/checkout/type"
 
 export const CREATE_CHECKOUT_ADDRESS: TypedDocumentNode<CreateCheckoutAddressResponse> = gql`
  mutation createCheckoutAddress(
@@ -66,6 +55,29 @@ export const CREATE_CHECKOUT_ADDRESS: TypedDocumentNode<CreateCheckoutAddressRes
       message
       id
       cartToken
+      billingFirstName
+      billingLastName
+      billingEmail
+      billingCompanyName
+      billingAddress
+      billingCountry
+      billingState
+      billingCity
+      billingPostcode
+      billingPhoneNumber
+      billingAddressId
+
+      shippingFirstName
+      shippingLastName
+      shippingEmail
+      shippingCompanyName
+      shippingAddress
+      shippingCountry
+      shippingState
+      shippingCity
+      shippingPostcode
+      shippingPhoneNumber
+      shippingAddressId
     }
   }
 }

+ 12 - 0
src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts

@@ -6,12 +6,16 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData>
         $shippingMethod: String
         $paymentMethod: String
         $couponCode: String
+        $giftCardNumber: String
+        $memberDiscount: String
     ) {
       createSaveCheckoutCart(
         input: {
             shippingMethod: $shippingMethod
             paymentMethod: $paymentMethod
             couponCode: $couponCode
+            giftCardNumber: $giftCardNumber
+            memberDiscount: $memberDiscount
         }
         ) {
             saveCheckoutCart {
@@ -37,9 +41,15 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData>
                         }
                     }
                 }
+                isVip
+                vipDiscountAmount
+                formattedVipDiscountAmount
+                giftcardNumber
                 subtotal
                 subTotalInclTax
                 discountAmount
+                giftcardAmount
+                vipPlusAmount
                 taxAmount
                 taxTotal
                 shippingAmount
@@ -47,7 +57,9 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData>
                 grandTotal
                 formattedSubtotal
                 formattedSubTotalInclTax
+                formattedGiftcardAmount
                 formattedDiscountAmount
+                formattedVipPlusAmount
                 formattedTaxAmount
                 formattedTaxTotal
                 formattedShippingAmount

+ 7 - 0
src/graphql/customer/query/GetOrderDetails.ts

@@ -34,6 +34,13 @@ export const GET_ORDER_DETAILS: TypedDocumentNode<OrderDetailsData> = gql`
     baseDiscountAmount
     shippingAmount
     baseShippingAmount
+    vipPlusAmount
+    vipDiscountAmount
+    baseVipPlusAmount
+    baseVipDiscountAmount
+    giftcardNumber
+    giftcardAmount
+    baseGiftcardAmount
     baseCurrencyCode
     channelCurrencyCode
     orderCurrencyCode

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

@@ -31,6 +31,9 @@ export function airwallexInit() {
           : 'prod',
 
       enabledElements: ['payments'],
+    }).catch((err) => {
+      initPromise = null;
+      throw err;
     });
 
   }

+ 2 - 1
src/lib/Airwallex/useAirwallexCard.ts

@@ -153,7 +153,7 @@ export function useAirwallexCard(
 
       const els = await airwallexManager.createElements();
       elementRef.current = els;
-
+console.log('useAirwallexCard +++++++++++++++++++++++++++++',destroyed);
       if (destroyed) return;
 
       airwallexManager.mount({
@@ -176,6 +176,7 @@ export function useAirwallexCard(
     init();
 
     return () => {
+        console.log('useAirwallexCard ___________________________________',mountedRef.current)
       destroyed = true;
       if (mountedRef.current) {
         airwallexManager.unmount();// 解除挂载

+ 15 - 2
src/lib/ApolloClientBrowser.ts

@@ -5,12 +5,21 @@ import {
   ApolloClient,
   InMemoryCache,
 } from "@apollo/client-integration-nextjs";
+import { ErrorLink } from "@apollo/client/link/error";
+import {CombinedProtocolErrors} from "@apollo/client/errors";
+import {handleErrorForErrorLink} from "@/lib/ApolloErrorHandler";
 import { getSession } from "next-auth/react";
 import { getCartToken } from "@/utils/getCartToken";
 import { BagistoSession } from "@/types/types";
 
 // 这里注册的是客户端使用的apollo client
 
+CombinedProtocolErrors.formatMessage = (errors,{defaultFormatMessage})=>{
+  return "[Subscription Error] " +  defaultFormatMessage(errors);
+};
+
+
+
 let sessionCache: { session: BagistoSession | null; timestamp: number } | null = null;
 const SESSION_CACHE_TTL = 5000;
 
@@ -44,6 +53,10 @@ export default function makeClient() {
         },*/
   });
 
+  const errorLink = new ErrorLink(({ error }) => {
+    handleErrorForErrorLink(error);
+  });
+
 
   const authLink = new SetContextLink(async (prevContext) => {
       
@@ -60,8 +73,8 @@ export default function makeClient() {
         },
       };
     });
-
-    const link = ApolloLink.from([authLink, httpLink]);
+    // httpLink 要放在最后
+    const link = ApolloLink.from([errorLink, authLink, httpLink]);
     return new ApolloClient({
         // ssrMode,
         link,

+ 53 - 3
src/lib/ApolloErrorHandler.ts

@@ -6,23 +6,73 @@ import {
   ServerParseError,
   UnconventionalError,
 } from "@apollo/client/errors";
-
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 // about apollo client error handle https://www.apollographql.com/docs/react/data/error-handling
 // Comprehensive error handling example.
-export function handleError(error: unknown) {
+
+export function handleApolloBusinessError(error: unknown, callback: (err:CombinedGraphQLErrors) => void) {
   if (CombinedGraphQLErrors.is(error)) {
     // Handle GraphQL errors
-  } else if (CombinedProtocolErrors.is(error)) {
+    // error.errors.forEach(({ message, locations, path }) =>
+    //   console.log(
+    //     `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
+    //   )
+    // );
+    callback(error);
+    // console.log('CombinedGraphQLErrors--',error.message);
+
+  }
+}
+// graphql接口额响应状态码貌似只有200 和 500。500表示: 1.后端代码报错;2.前端代理接口抛出异常
+// 已知有一种情况graphql的响应状态码是401,即请求接口的时候请求头缺少X-STOREFRONT-KEY
+export function handleErrorForErrorLink(error: unknown) {
+  // CombinedGraphQLErrors 交给具体的业务自己捕获处理
+  if (CombinedGraphQLErrors.is(error)) return;
+  if (CombinedProtocolErrors.is(error)) {
+    // CombinedProtocolErrors 是GraphQL 传输协议层错误,该项目中遇不到
     // Handle multipart subscription protocol errors
+    console.error( "GraphQL protocol error", error.errors);
+    console.log(error.message);
   } else if (LocalStateError.is(error)) {
+    /**
+     * LocalStateError 是 Apollo Client 在执行 Local State(本地状态) 时抛出的错误,通常和 @client 字段、本地 resolver 配置、LocalState 配置错误 有关。
+     * 它不是 GraphQL Server 返回的错误,而是 Apollo Client 自己在本地解析字段时发生的致命错误。
+     * 本项目没用到
+     */
     // Handle errors thrown by the `LocalState` class
+    console.error( "Local state error message:", error.message );
+    console.error( "Local state error cause:", error.cause );
+    console.error( "Local state error path:", error.path );
   } else if (ServerError.is(error)) {
+    // 状态码不是2xx的错误
     // Handle server HTTP errors
+    /*
+    console.log('ServerError--',error);
+    console.log(`Server returned status: ${error.statusCode}`,error.message);
+    console.log(`bodyText: `,JSON.parse(error.bodyText));
+    console.log('response: ', error.response);
+    */
+   
+    const bodyData = JSON.parse(error.bodyText);
+    const msg = bodyData.errors[0].message;
+    // Handle specific status codes
+    confirmDialog({
+        title: "Server Error",
+        content: 'AC Error: ' + msg,
+        noCancel: true,
+    }).then(() => {});
+    
   } else if (ServerParseError.is(error)) {
+    // 解析接口返回数据时报错
     // Handle JSON parse errors
+    console.log('ServerParseError--',error);
   } else if (UnconventionalError.is(error)) {
+    // 不是标准的JavaScript错误,一般是自定义apollo link或者第三方Apollo link抛出的
     // Handle errors thrown by irregular types
+    console.log('UnconventionalError--',error);
+    console.error( "非法错误:", error.cause);
   } else {
     // Handle other errors
+    console.error("[other errors]:", error);
   }
 }

+ 9 - 13
src/lib/restApiClient.ts

@@ -21,7 +21,7 @@ async function getCachedSession(): Promise<BagistoSession | null> {
   return session;
 }
 
-export async function clientFetch(apiUrl: string, options: RequestInit = {}) {
+export async function clientFetch<T = any>(apiUrl: string, options: RequestInit = {}): Promise<T> {
     // 请求的是nextjs的代理接口
 
     const session = await getCachedSession();
@@ -38,18 +38,14 @@ export async function clientFetch(apiUrl: string, options: RequestInit = {}) {
     } else {
         options.headers = headers;
     }
-    
+
+    // fetch 不会reject 非2xx http状态码的响应,而是resolve,所以不会走catch,如果要走catch,需要自己主动抛出异常
     const response = await fetch(apiUrl, options);
-    // console.log('response -- ', response);
-    if(response.ok) {
-        const result = await response.json();
-        return result;
-    } else {
-        return {
-            status: response.status,
-            statusText: response.statusText,
-            data: await response.text(),
-        }
-    }
+    const result = await response.json();
+    console.log('response -- ', response);
+    // if(!response.ok) { // 非2xx状态
+    //     // 
+    // }
+    return result;
     
 }

+ 7 - 0
src/types/api/gift/my-gift-cards.ts

@@ -15,6 +15,13 @@ export interface GiftCardRespBody {
   success: boolean;
   message: string;
   data: GiftCardItem[];
+  pagination: {
+    current_page: number;
+    has_more: boolean;
+    last_page: number;
+    per_page: number;
+    total: number;
+  };
 }
 
 // restApiFetch返回外层结构

+ 15 - 1
src/types/cart/type.ts

@@ -43,11 +43,17 @@ export interface CartDetail {
   isGuest: boolean;
   itemsCount: number;
   items: CartItemsConnection;
+  isVip: boolean;
+  vipDiscountAmount: number;
+  formattedVipDiscountAmount: string;
+  giftcardNumber: string | null;
   subtotal: number; // Subtotal before discounts and taxes
   subTotalInclTax: number;
   // baseSubtotal: number;
   discountAmount: number;
   // baseDiscountAmount: number;
+  giftcardAmount: number;
+  vipPlusAmount: number;
   taxAmount: number;
   taxTotal: number;
   shippingAmount: number;
@@ -56,6 +62,8 @@ export interface CartDetail {
 
   formattedSubtotal: string;
   formattedSubTotalInclTax: string;
+  formattedGiftcardAmount: string;
+  formattedVipPlusAmount: string;
   formattedDiscountAmount: string;
   formattedTaxAmount: string;
   formattedTaxTotal: string;
@@ -135,7 +143,13 @@ export interface CreateCartTokenOperation {
   data: CreateCartTokenData;
   variables: CreateCartTokenVariables;
 }
-
+// apply coupon
+export interface CreateApplyCouponPayload {
+  applyCoupon: CartDetail;
+}
+export interface CreateApplyCouponData {
+  createApplyCoupon: CreateApplyCouponPayload;
+}
 // Merge Cart
 export interface CreateMergeCartPayload {
   mergeCart: CartDetail;

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

@@ -58,7 +58,42 @@ export interface BillAddressFormData {
 
 export type FullAddressFormData = ShipAddressFormData & BillAddressFormData;
 
+export interface CreateCheckoutAddressData {
+    billingFirstName: string;
+    billingLastName: string;
+    billingEmail: string;
+    billingCompanyName: string;
+    billingAddress: string;
+    billingCountry: string;
+    billingState: string;
+    billingCity: string;
+    billingPostcode: string;
+    billingPhoneNumber: string;
+    billingAddressId: number;
 
+    shippingFirstName: string;
+    shippingLastName: string;
+    shippingEmail: string;
+    shippingCompanyName: string;
+    shippingAddress: string;
+    shippingCountry: string;
+    shippingState: string;
+    shippingCity: string;
+    shippingPostcode: string;
+    shippingPhoneNumber: string;
+    shippingAddressId: number;
+}
+export type CreateCheckoutAddressResponse = {
+  createCheckoutAddress:{
+      checkoutAddress: {
+          success: boolean;
+          message: string;
+          id: string;
+          cartToken: string;
+      } & CreateCheckoutAddressData
+  }
+  
+};
 
 export interface MappedCheckoutAddress {
   firstName: string;
@@ -197,6 +232,8 @@ export interface SaveCheckoutCartVariables {
   shippingMethod: string;
   paymentMethod: string;
   couponCode: string;
+  giftCardNumber: string;
+  memberDiscount: string;
 }
 // Checkout Place Order
 

+ 7 - 0
src/types/customer/order.ts

@@ -106,6 +106,13 @@ export interface OrderDetails {
     baseDiscountAmount: number;
     shippingAmount: number;
     baseShippingAmount: number;
+    vipPlusAmount: string;
+    vipDiscountAmount: string;
+    baseVipPlusAmount: string;
+    baseVipDiscountAmount: string;
+    giftcardNumber: string;
+    giftcardAmount: string;
+    baseGiftcardAmount: string;
     baseCurrencyCode: string | null;
     channelCurrencyCode: string | null;
     orderCurrencyCode: string;

+ 24 - 78
src/utils/bagisto/index.ts

@@ -50,7 +50,8 @@ import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 
 type ExtractVariables<T> = T extends { variables: object }
   ? T["variables"]
-  : never;
+  : any;
+type ExtractData<T> = T extends { data: infer D } ? D : any;
 
 interface PageByUrlKeyResponse {
   pageByUrlKeypages?: PageData[];
@@ -110,9 +111,11 @@ export async function restApiFetch<T>({
   isCookies?: boolean;
   guestToken?: string;
   revalidate?: number;
-}): Promise<{ status: number; body: T } | never> {
+}): Promise<{ status: number; body: ExtractData<T> } | never> {
   try {
     const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
+    const url = new URL(apiUrl);
+
     let accessToken: string | undefined = undefined;
 
     if (isCookies) {
@@ -139,7 +142,7 @@ export async function restApiFetch<T>({
         Object.assign(baseHeaders, headers);
       }
     }
-console.log('restApiFetch --- baseHeaders:', baseHeaders)
+    console.log('restApiFetch --- baseHeaders:', baseHeaders)
     const param: RequestInit = {
       method: method,
       headers: baseHeaders,
@@ -149,16 +152,22 @@ console.log('restApiFetch --- baseHeaders:', baseHeaders)
         ...(tags && { tags }),
       },
     };
-    if(variables) {
+    if(variables && method === "POST") {
       param.body = JSON.stringify({...variables});
     }
+    if(variables && method === "GET") {
+      const entries = Object.entries(variables as Record<string, unknown>);
+      for (const [key, val] of entries) {
+        if (val !== undefined && val !== null) {
+          url.searchParams.set(key, String(val));
+        }
+      }
+    }
 
-    const result = await fetch(apiUrl, param);
-    console.log('restApiFetch --- result:', result)
-    const body = await result.json();
-
-    if (body.errors) throw body.errors[0];
+    const result = await fetch(url, param);
 
+    const body = await result.json();
+    console.log('restApiFetch --- body:', body)
     return { status: result.status, body };
   } catch (e) {
     throw e;
@@ -176,7 +185,6 @@ export async function serverGraphqlFetch<
   tags,
   variables,
   revalidate = 0,
-  operationName = ''
 }: {
   cache?: RequestCache;
   headers?: HeadersInit | Record<string, string>;
@@ -184,7 +192,6 @@ export async function serverGraphqlFetch<
   tags?: string[];
   variables?: TVariables;
   revalidate?: number;
-  operationName?: string;
 }): Promise<FetchGraphqlResult<TData>> {
   try {
     const queryString = typeof query === "string" ? query : print(query);
@@ -245,15 +252,14 @@ export async function serverGraphqlFetch<
 
     const body = await result.json();
     console.log('serverGraphqlFetch --- body:',body);
-    if (body.errors) {
-      if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
-        return { status: result.status, data: body.data, error: null };
-      } else {
-        return { status: result.status, data: body.data, error: body.errors[0] };
-      }
+    return {
+      status:result.status,
+      data:body.data ?? null,
+      error:body.errors?.[0] ?? null
     }
-    return { status: result.status, data: body.data,error: null };
+
   } catch (e) {
+    console.error( "GraphQL request failed", e );
     throw e;
   }
 }
@@ -268,7 +274,6 @@ export async function bagistoFetch<T>({
   isCookies = true,
   guestToken,
   revalidate = 60,
-  operationName = ''
 }: {
   cache?: RequestCache;
   headers?: HeadersInit | Record<string, string>;
@@ -278,7 +283,6 @@ export async function bagistoFetch<T>({
   isCookies?: boolean;
   guestToken?: string;
   revalidate?: number;
-  operationName?: string;
 }): Promise<{ status: number; body: T } | never> {
   try {
     const queryString =
@@ -335,70 +339,12 @@ export async function bagistoFetch<T>({
 
     const body = await result.json();
     console.log('bagistoFetch --- body:',body);
-    if (body.errors) {
-      if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
-        return { status: result.status, body };
-      }
-      throw body.errors[0]
-    }
     return { status: result.status, body };
   } catch (e) {
     throw e;
   }
 }
-/*
-export async function bagistoFetchNoSession<T>({
-  query,
-  tags,
-  variables,
-  headers,
-  cache = "force-cache",
-  revalidate = 60,
-}: {
-  query: string;
-  tags?: string[];
-  variables?: ExtractVariables<T>;
-  headers?: HeadersInit | Record<string, string>;
-  cache?: RequestCache;
-  isCookies?: boolean;
-  revalidate?: number;
-}): Promise<{ status: number; body: T } | never> {
-  try {
-    const result = await fetch(GRAPHQL_URL, {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        "X-STOREFRONT-KEY": STOREFRONT_KEY,
-        "x-locale": "en",
-        "x-currency": "USD",
-        ...headers,
-      },
-      body: JSON.stringify({
-        ...(query && { query }),
-        ...(variables && { variables }),
-      }),
-      cache,
-      next: {
-        revalidate: cache === "no-store" ? 0 : revalidate || 60,
-        ...(tags && { tags }),
-      },
-    });
-
-    const body = await result.json();
-
-    if (body.errors) {
-      throw body.errors[0];
-    }
 
-    return {
-      status: result.status,
-      body,
-    };
-  } catch (e) {
-    throw { error: e, query };
-  }
-}
-*/
 export const removeEdgesAndNodes = <T>(array: Array<T>) => {
   return array?.map((edge) => edge);
 };

+ 9 - 0
src/utils/cartDetailTools.ts

@@ -8,6 +8,12 @@ export function formatCartDetail(cart:CartDetail): CartDetail {
         isGuest: cart.isGuest,
         itemsCount: cart.itemsCount,
         items: cart.items,
+        isVip: cart.isVip,
+        vipDiscountAmount: cart.vipDiscountAmount,
+        formattedVipDiscountAmount: cart.formattedVipDiscountAmount,
+        giftcardNumber: cart.giftcardNumber,
+        giftcardAmount: cart.giftcardAmount,
+        vipPlusAmount: cart.vipPlusAmount,
         subtotal: cart.subtotal,
         subTotalInclTax: cart.subTotalInclTax,
         discountAmount: cart.discountAmount,
@@ -20,6 +26,9 @@ export function formatCartDetail(cart:CartDetail): CartDetail {
         formattedSubtotal: cart.formattedSubtotal,
         formattedSubTotalInclTax: cart.formattedSubTotalInclTax,
 
+        formattedGiftcardAmount: cart.formattedGiftcardAmount,
+        formattedVipPlusAmount: cart.formattedVipPlusAmount,
+
         formattedDiscountAmount: cart.formattedDiscountAmount,
         formattedTaxAmount: cart.formattedTaxAmount,
         formattedTaxTotal: cart.formattedTaxTotal,

+ 10 - 4
src/utils/hooks/useAddToCart.ts

@@ -16,7 +16,7 @@ import {
   UPDATE_CART_ITEM,
 } from "@/graphql";
 import { formatCartDetail } from "@/utils/cartDetailTools";
-
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 
 export const useAddProduct = () => {
@@ -64,7 +64,9 @@ export const useAddProduct = () => {
       },
 
       onError: (err) => {
-        showToast(err?.message ?? "Error", "danger");
+        handleApolloBusinessError(err,(error) => {
+          showToast(error.message, "danger");
+        });
       },
     },
   );
@@ -138,7 +140,9 @@ export const useAddProduct = () => {
         }
       },
       onError: (error) => {
-        showToast(error?.message as string, "danger");
+        handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+        });
       },
     },
   );
@@ -167,7 +171,9 @@ export const useAddProduct = () => {
       },
 
       onError: (error) => {
-        showToast(error?.message as string, "danger");
+        handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+        });
       },
     },
   );

+ 16 - 6
src/utils/hooks/useCheckoutAddress.ts

@@ -38,10 +38,7 @@ export function useCheckoutAddress(loginEmail: string) {
     const apolloClient = useApolloClient();
 
 
-    const getCheckoutAddress = (
-        resolveCallback: (shippingAddress:ShipAddressFormData, billingAddress: BillAddressFormData) => void = () => {}, 
-        rejectCallback: () => void = ()=> {}
-    ) => {
+    const getCheckoutAddress = () => {
         return apolloClient.query({
             query: GET_CHECKOUT_ADDRESSES,
             fetchPolicy: "no-cache",
@@ -92,7 +89,6 @@ export function useCheckoutAddress(loginEmail: string) {
             const billSameAsShip = addressIsSame(defaultBillAddress, defaultShipAddress);
             defaultBillAddress.billingSameAsShipping = billSameAsShip;
             console.log('GET_CHECKOUT_ADDRESSES res ---- ',defaultShipAddress,defaultBillAddress);
-            resolveCallback(defaultShipAddress,defaultBillAddress);
             return {
                 shippingAddress: defaultShipAddress,
                 billingAddress: defaultBillAddress
@@ -100,7 +96,6 @@ export function useCheckoutAddress(loginEmail: string) {
         
         }).catch((err) => {
             console.error('GET_CHECKOUT_ADDRESSES error ---- ',err);
-            rejectCallback();
             return null;
         });
     };
@@ -109,6 +104,21 @@ export function useCheckoutAddress(loginEmail: string) {
         return apolloClient.mutate({
             mutation: CREATE_CHECKOUT_ADDRESS,
             variables:saveAddressParam,
+        }).then((res) => {
+            const resData = res.data?.createCheckoutAddress.checkoutAddress ?? null; 
+            const result = {
+                error: false,
+                msg: '',
+                data: resData
+            };
+            return result;
+        }).catch((err) => {
+            const result = {
+                error: true,
+                msg: err.message,
+                data: null,
+            };
+            return result;
         });
     }
 

+ 4 - 4
src/utils/hooks/useCheckoutPaymentMethod.ts

@@ -1,6 +1,6 @@
 "use client";
 
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useRef, useCallback } from "react";
 import { GET_CHECKOUT_PAYMENT_METHODS, CREATE_CHECKOUT_PAYMENT_METHODS } from "@/graphql";
 import {useApolloClient} from "@apollo/client/react";
 import {CheckoutPaymentMethod, CreateCheckoutPaymentMethodVariables} from "@/types/checkout/type";
@@ -17,7 +17,7 @@ export function useCheckoutPaymentMethod() {
     const [error, setError] = useState<string|null>(null);
     const isIntRef = useRef(true);
 
-    const getPaymentMethod = () => {
+    const getPaymentMethod = useCallback(() => {
         if(!isIntRef.current) {
             setLoading(true);
         }
@@ -63,7 +63,7 @@ export function useCheckoutPaymentMethod() {
             
             return resData;
         });
-    };
+    },[apolloClient]);
 
     const savePaymentMethod = (params: CreateCheckoutPaymentMethodVariables) => {
         return apolloClient.mutate({
@@ -99,7 +99,7 @@ export function useCheckoutPaymentMethod() {
         //     setError(rejectData.msg);
         //     setLoading(false);
         // });
-    }, []);
+    }, [getPaymentMethod]);
 
     return {
         data,

+ 4 - 4
src/utils/hooks/useCheckoutShippingMethod.ts

@@ -1,6 +1,6 @@
 "use client";
 
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, useCallback } from "react";
 import { GET_CHECKOUT_SHIPPING_RATES, CREATE_CHECKOUT_SHIPPING_METHODS } from "@/graphql";
 import {useApolloClient} from "@apollo/client/react";
 import {CheckoutShippingRate, CreateCheckoutShippingMethodVariables} from "@/types/checkout/type";
@@ -17,7 +17,7 @@ export function useCheckoutShippingMethod() {
     const [error, setError] = useState<string|null>(null);
     const isIntRef = useRef(true);
 
-    const getShippingMethod = () => {
+    const getShippingMethod = useCallback(() => {
         if(!isIntRef.current) {
             setLoading(true);
         }
@@ -61,7 +61,7 @@ export function useCheckoutShippingMethod() {
             // rejectCallback(resData);
             return resData;
         });
-    };
+    },[apolloClient]);
 
     const saveShippingMethod = (params: CreateCheckoutShippingMethodVariables) => {
         return apolloClient.mutate({
@@ -97,7 +97,7 @@ export function useCheckoutShippingMethod() {
         //     setError(rejectData.msg);
         //     setLoading(false);
         // });
-    }, []);
+    }, [getShippingMethod]);
 
 
     return {

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

@@ -1,3 +1,4 @@
+import { useCallback } from "react";
 import { useApolloClient } from "@apollo/client/react";
 import { 
     CREATE_PAYMENT_INITIATE,
@@ -39,7 +40,7 @@ interface PaymentReplayResult {
 export const usePlaceOrder = () => {
     const client = useApolloClient();
 
-    const createOrder = (params: CreatePaymentInitiateVariables = {}) => {
+    const createOrder = useCallback((params: CreatePaymentInitiateVariables = {}) => {
         return client.mutate({
             mutation: CREATE_PAYMENT_INITIATE,
             variables: params
@@ -60,7 +61,7 @@ export const usePlaceOrder = () => {
             return result;
         });
 
-    };
+    },[client]);
 
     // 支付回调
     const createPaymentCallback = (params: CreatePaymentCallbackVariables) => { 

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

@@ -3,12 +3,13 @@
 import { useMutation } from "@apollo/client/react";
 import { CREATE_PRODUCT_REVIEW } from "@/graphql";
 import { useCustomToast } from "./useToast";
-import { CreateProductReviewInput, ProductReviewResponse } from "@/types/review";
+import { CreateProductReviewInput } from "@/types/review";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 export function useProductReview() {
     const { showToast } = useCustomToast();
 
-    const [mutateAsync, { loading: isLoading, error }] = useMutation<ProductReviewResponse>(CREATE_PRODUCT_REVIEW, {
+    const [mutateAsync, { loading: isLoading, error }] = useMutation(CREATE_PRODUCT_REVIEW, {
         onCompleted: (response) => {
             const responseData = response?.createProductReview;
             if (responseData) {
@@ -16,7 +17,9 @@ export function useProductReview() {
             }
         },
         onError: (error) => {
-            showToast(error.message, "danger");
+            handleApolloBusinessError(error,(err) => {
+                showToast(err.message, "danger");
+            });
         },
     });
 

+ 3 - 2
src/utils/hooks/useSaveCheckoutCart.ts

@@ -1,3 +1,4 @@
+import {useCallback} from "react";
 import { useApolloClient } from "@apollo/client/react";
 import { 
     CREATE_SAVE_CHECKOUT_CART
@@ -13,7 +14,7 @@ interface SaveCheckoutCartResult {
 
 export const useSaveCheckoutCart = () => {
     const client = useApolloClient();
-    const saveCheckoutCart = (params: SaveCheckoutCartVariables) => {
+    const saveCheckoutCart = useCallback((params: SaveCheckoutCartVariables) => {
         return client.mutate({
             mutation: CREATE_SAVE_CHECKOUT_CART,
             variables: params
@@ -34,7 +35,7 @@ export const useSaveCheckoutCart = () => {
             return result;
         });
 
-    };
+    },[client]);
 
 
 

+ 3 - 2
src/utils/hooks/useToast.ts

@@ -1,15 +1,16 @@
+import {useCallback} from "react";
 import { useToast } from "@/providers";
 
 export const useCustomToast = () => {
   const { addToast } = useToast();
 
-  const showToast = (
+  const showToast = useCallback((
     message: string,
     type: "success" | "danger" | "warning" | "primary" = "primary",
     duration = 5000,
   ) => {
     addToast({ message, type, duration });
-  };
+  },[addToast]);
 
   return { showToast };
 };