Explorar o código

游客token 改成server-only 模式

fogwind hai 3 días
pai
achega
911588690e
Modificáronse 41 ficheiros con 494 adicións e 1021 borrados
  1. 1 0
      README.md
  2. 75 0
      src/actions/addProductToCartAction.ts
  3. 13 0
      src/actions/createGuestCartAction.ts
  4. 48 0
      src/actions/deleteCartProductAction.ts
  5. 13 0
      src/actions/deleteGuestCookieAction.ts
  6. 7 0
      src/actions/index.ts
  7. 42 0
      src/actions/logoutAction.ts
  8. 12 11
      src/actions/mergeCartAction.ts
  9. 5 5
      src/app/(checkout)/checkout/_components/LostInsurance/LostInsurance.tsx
  10. 5 2
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  11. 2 8
      src/app/(public)/product/_components/ProductAddToCart.tsx
  12. 29 26
      src/app/(public)/product/_components/ProductInformation.tsx
  13. 0 20
      src/app/api/clearCartCookie/route.ts
  14. 0 197
      src/components/cart/AddToCart.tsx
  15. 0 137
      src/components/catalog/product/ProductDescription.tsx
  16. 0 31
      src/components/catalog/product/ProductInfo.tsx
  17. 19 10
      src/components/common/AddToCartModal/AddToCartModal.tsx
  18. 3 5
      src/components/common/AddToCartModal/FooterBtnInAddToCartModal.tsx
  19. 1 1
      src/components/common/CurrencySwitch/CurrencySwitch.tsx
  20. 1 1
      src/components/common/LoginModal/LoginModal.tsx
  21. 6 4
      src/components/common/button/ReviewButton.tsx
  22. 2 2
      src/components/common/icons/cart/DeleteItemButton.tsx
  23. 14 51
      src/components/customer/LoginForm.tsx
  24. 4 8
      src/components/customer/credentials/CredentialModal.tsx
  25. 2 2
      src/graphql/cart/mutations/AddProductToCart.ts
  26. 3 2
      src/graphql/cart/mutations/CreateCartToken.ts
  27. 0 11
      src/providers/NextAuthProvider.tsx
  28. 0 8
      src/providers/SessionProvider.tsx
  29. 2 2
      src/providers/index.ts
  30. 103 0
      src/server-service/guestCartTokenService.ts
  31. 1 0
      src/types/cart/type.ts
  32. 8 1
      src/types/types.ts
  33. 0 4
      src/utils/actions.ts
  34. 19 51
      src/utils/bagisto/index.ts
  35. 4 2
      src/utils/constants.ts
  36. 4 23
      src/utils/cookie-tools.ts
  37. 0 55
      src/utils/fetch-handler.ts
  38. 46 158
      src/utils/hooks/useAddToCart.ts
  39. 0 110
      src/utils/hooks/useGuestCartToken.ts
  40. 0 37
      src/utils/hooks/useMergeCart.ts
  41. 0 36
      src/utils/signInAuth.ts

+ 1 - 0
README.md

@@ -352,6 +352,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
 47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
+48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
 
 
 

+ 75 - 0
src/actions/addProductToCartAction.ts

@@ -0,0 +1,75 @@
+"use server";
+
+import {cookies} from "next/headers";
+import { GUEST_CART_TOKEN } from "@/utils/constants";
+import { CREATE_ADD_PRODUCT_IN_CART } from "@/graphql";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@utils/auth";
+import { serverGraphqlFetch } from "@utils/bagisto";
+import { BagistoSession } from "@/types/types";
+import {createGuestCartToken,setGuestCookie} from "@/server-service/guestCartTokenService";
+import { AddToCartData,AddToCartVariables } from "@/types/cart/type";
+
+
+export async function addProductToCartAction(param:AddToCartVariables) {
+
+    const authSession = (await getServerSession(authOptions)) as BagistoSession | null;
+    const accessToken = authSession?.user?.accessToken;
+
+    const cookieStore = await cookies();
+    const guestCartToken = cookieStore.get(GUEST_CART_TOKEN)?.value;
+
+
+    if(!accessToken && !guestCartToken) {
+        const createRes = await createGuestCartToken();
+        if(!createRes.success) {
+            return {
+                success: false,
+                msg: createRes.msg,
+                cartData: null
+            }
+        }
+    }
+    try{
+        const response = await serverGraphqlFetch<AddToCartData,AddToCartVariables>({
+            query: CREATE_ADD_PRODUCT_IN_CART,
+            variables: param,
+            cache: "no-store",
+        });
+        if(!response.error) {
+            const responseData = response.data.createAddProductInCart?.addProductInCart;
+            /** 兜底代码 start*/
+            // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
+            // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
+            const responseCartId = responseData.id;
+            const responseCartToken = responseData.cartToken;
+            if(!accessToken && responseCartId !== responseCartToken) {
+                await setGuestCookie({
+                    guestCartId: responseCartId,
+                    guestToken: responseCartToken,
+                    isGuest: true
+                });
+            }
+            /** 兜底代码 end*/
+
+            return {
+                success: true,
+                msg: '',
+                cartData: response.data
+            }
+
+        } else {
+            return {
+                success: false,
+                msg: response.error.message,
+                cartData: null
+            }
+        }
+    } catch(error) {
+        return {
+            success: false,
+            msg: error instanceof Error ? error.message : 'Add product to cart failed!',
+            cartData: null
+        }
+    }
+};

+ 13 - 0
src/actions/createGuestCartAction.ts

@@ -0,0 +1,13 @@
+"use server";
+
+import {createGuestCartToken} from "@/server-service/guestCartTokenService";
+
+export async function createGuestCartAction() {
+
+    const res = await createGuestCartToken();
+    return {
+        success: res.success,
+        msg: res.msg
+    }
+
+}

+ 48 - 0
src/actions/deleteCartProductAction.ts

@@ -0,0 +1,48 @@
+"use server";
+
+
+import { REMOVE_CART_ITEM } from "@/graphql";
+import { serverGraphqlFetch } from "@utils/bagisto";
+import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
+import { RemoveCartItemData } from "@/types/cart/type";
+
+
+export async function deleteCartProductAction(cartItemId:number) {
+
+    try{
+        const response = await serverGraphqlFetch<RemoveCartItemData,{cartItemId:number}>({
+            query: REMOVE_CART_ITEM,
+            variables: {
+                cartItemId: cartItemId
+            },
+            cache: "no-store",
+        });
+        if(!response.error) {
+            const resCatData = response.data.createRemoveCartItem?.removeCartItem ?? null;
+            
+            if(!resCatData || !resCatData?.itemsQty) {
+                // 购物车空了,删除游客cookie
+                deleteGuestCookie();
+            }
+
+            return {
+                success: true,
+                msg: '',
+                cartData: response.data
+            }
+
+        } else {
+            return {
+                success: false,
+                msg: response.error.message,
+                cartData: null
+            }
+        }
+    } catch(error) {
+        return {
+            success: false,
+            msg: error instanceof Error ? error.message : 'Delete product failed!',
+            cartData: null
+        }
+    }
+};

+ 13 - 0
src/actions/deleteGuestCookieAction.ts

@@ -0,0 +1,13 @@
+"use server";
+
+import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
+
+export async function deleteGuestCookieAction() {
+        
+    await deleteGuestCookie();
+
+    return {
+        success: true,
+        msg: ""
+    };
+}

+ 7 - 0
src/actions/index.ts

@@ -0,0 +1,7 @@
+export {deleteGuestCookieAction} from "./deleteGuestCookieAction";
+export {mergeCartAction} from "./mergeCartAction";
+export {createGuestCartAction} from "./createGuestCartAction";
+export {switchCurrencyAction} from "./switchCurrencyAction";
+export {addProductToCartAction} from "./addProductToCartAction";
+export {deleteCartProductAction} from "./deleteCartProductAction";
+export {logoutAction} from "@/actions/logoutAction";

+ 42 - 0
src/actions/logoutAction.ts

@@ -0,0 +1,42 @@
+"use server";
+
+import {cookies} from "next/headers";
+import { IS_GUEST } from "@/utils/constants";
+import { serverGraphqlFetch } from "@utils/bagisto";
+import {CUSTOMER_LOGOUT} from "@/graphql/customer/mutations";
+import { LogoutData } from "@/types/types";
+
+export async function logoutAction() {
+
+    try {
+
+        const res = await serverGraphqlFetch<LogoutData>({
+            query: CUSTOMER_LOGOUT,
+        });
+        let success = true;
+        let msg = "";
+        if(res.error) {
+            success = false;
+            msg = res.error.message;
+
+        } else {
+            success = res.data?.createLogout?.logout?.success ?? false;
+            msg = res.data?.createLogout?.logout?.message ?? "";
+            if(success) {
+                const cookieStore = await cookies();
+                // 设置游客状态
+                cookieStore.set(IS_GUEST, "true");
+            }
+        }
+
+        return {
+            success,
+            message: msg,
+        };
+    } catch (error: unknown) {
+        return {
+            success: false,
+            message: error instanceof Error ? error.message : "Something went wrong",
+        };
+    }
+}

+ 12 - 11
src/actions/mergeCartAction.ts

@@ -3,7 +3,7 @@
 
 import {cookies} from "next/headers";
 import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
-import { bagistoFetch } from "@utils/bagisto";
+import { serverGraphqlFetch } from "@utils/bagisto";
 import { CREATE_MERGE_CART } from "@/graphql";
 import { CreateMergeCartData } from "@/types/cart/type";
 
@@ -26,23 +26,24 @@ export async function mergeCartAction(){
     }
     try{
         // 调 Bagisto merge cart mutation
-        const response = await bagistoFetch<{
-            data: CreateMergeCartData,
-            variables: {cartId:number}
-        }>({
+        const response = await serverGraphqlFetch<CreateMergeCartData,{cartId:number}>({
             query: CREATE_MERGE_CART,
             variables: {
                 cartId: Number(guestCartId)
             },
             cache: "no-store",
         });
-        // cookieStore.delete(GUEST_CART_ID);
-        // cookieStore.delete(GUEST_CART_TOKEN);
-        // cookieStore.set(IS_GUEST,"false");
+        const cartData = response.data.createMergeCart?.mergeCart ?? null;
+        let success = true;
+        let msg = "";
+        if(response.error) {
+            success = false;
+            msg = response.error.message;
+        }
         return {
-            cartDetail: response.body.data.createMergeCart.mergeCart,
-            success:true,
-            msg: ''
+            cartDetail: cartData,
+            success: success,
+            msg: msg
         };
     } catch(e: any) {
         return {

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

@@ -1,7 +1,7 @@
 "use client";
 
 
-import {useConfig} from "@/utils/hooks/useConfig";
+// import {useConfig} from "@/utils/hooks/useConfig";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail } from "@/utils/cartDetailTools";
@@ -21,13 +21,13 @@ export default function LostInsurance({
     onSwitchInsurance: (payload:CartDetail) => void;
 }) {
 
-    const {getCurrentCurrencyItem} = useConfig();
-    const currentCurrency = getCurrentCurrencyItem();
-    const currencySymbol = currentCurrency.symbol;
+    // const {getCurrentCurrencyItem} = useConfig();
+    // const currentCurrency = getCurrentCurrencyItem();
+    // const currencySymbol = currentCurrency.symbol;
     const { showToast } = useCustomToast();
     const {saveCheckoutCart} = useSaveCheckoutCart();
     const clickSwitchButton = async (bool: boolean) => {
-        let isInsurance = bool ? '1' : '';
+        const isInsurance = bool ? '1' : '';
         await setInsurance(isInsurance);
     };
     const setInsurance = async (insurance: string) => {

+ 5 - 2
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -5,6 +5,7 @@ import Link from "next/link";
 import Image from "next/image";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { OrderDetailsData,ProductItemAdditional } from "@/types/customer/type";
+import {deleteGuestCookieAction} from "@/actions";
 import { useConfig } from "@utils/hooks/useConfig";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import Faqs from "./Faqs";
@@ -59,8 +60,10 @@ export default function OrderDetailWrapper({
         /**Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#options */
 
         if(isGuest) {
-            fetch("/api/clearCartCookie",{
-                method:"POST"
+            deleteGuestCookieAction().then((res) => {
+                console.info(res);
+            }).catch((err) => {
+                console.error(err);
             });
         }
     }, [isGuest]);

+ 2 - 8
src/app/(public)/product/_components/ProductAddToCart.tsx

@@ -74,13 +74,11 @@ type VariantPriceInfo = {
 
 export function ProductAddToCart({
   isAvailable,
-  isLoading,
   onAddToCart,
   onBuyNow,
   priceInfo, // 父组件传入价格数据
 }: {
   isAvailable: boolean;
-  isLoading: boolean;
   onAddToCart: () => void;
   onBuyNow: () => void;
   priceInfo: VariantPriceInfo; // 价格数据源
@@ -155,9 +153,7 @@ export function ProductAddToCart({
           <button
             onClick={onBuyNow}
             type="button"
-            className={clsx(btnClass, "flex-1 bg-ly-deepgreen", {
-              "opacity-25": isLoading,
-            })}
+            className={clsx(btnClass, "flex-1 bg-ly-deepgreen")}
           >
             Buy Now
           </button>
@@ -165,9 +161,7 @@ export function ProductAddToCart({
           <button
             onClick={onAddToCart}
             type="button"
-            className={clsx(btnClass, "flex-1 bg-ly-middlegreen", {
-              "opacity-25": isLoading,
-            })}
+            className={clsx(btnClass, "flex-1 bg-ly-middlegreen")}
           >
             Add to Cart
           </button>

+ 29 - 26
src/app/(public)/product/_components/ProductInformation.tsx

@@ -22,6 +22,7 @@ import {
   findNearestAvailableVariant,
 } from "@/utils/variantTools";
 import { Price } from "@components/theme/ui/Price";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 
 export function ProductInformation({
   name,
@@ -36,7 +37,7 @@ export function ProductInformation({
   flexibleVariants: ResolvedVariant[];
   isSaleable: string | undefined;
 }) {
-  const { isCartLoading, onAddToCart } = useAddProduct();
+  const { appendProductToCart } = useAddProduct();
   const { showToast } = useCustomToast();
 
   // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
@@ -196,41 +197,44 @@ export function ProductInformation({
     }
   };
 
-  async function addProductToCart(action: string = "addtocart") {
-    const params = {
-      productId: String(productId),
-      quantity: productQty,
-      variantId: currentVariantInfo.variant?._id,
-    };
-    const res = await onAddToCart(params);
-    console.log("onAddToCart result --- ", res);
-    if (action === "buynow") {
-      if (res) {
-        const responseData = res.data?.createAddProductInCart?.addProductInCart;
-        if (responseData && responseData.success) {
-          redirect("/checkout?step=address", RedirectType.push);
+  async function addProductToCart(action:string = 'addtocart') {
+        overlayLoading.start();
+        const res = await appendProductToCart({
+            productId: productId, 
+            quantity: productQty,
+            variantId: currentVariantInfo.variant?._id,
+        });
+        console.log('onAddToCart result --- ', res);
+        overlayLoading.stop();
+        if(action === 'buynow') {
+            if(res.data) {
+                const responseData = res.data;
+                if(responseData.success) {
+                    redirect('/checkout?step=address', RedirectType.push);
+                }
+            }
         }
-      }
-    }
-  }
+  } 
 
   const addToCartHandler = async () => {
-    if (!isCurrentSelectionAvailable) {
-      showToast("The selected options are not available!", "warning");
-      return;
-    }
+   
+        if(!isCurrentSelectionAvailable) {
+            showToast("The selected options are not available!", "warning");
+            return;
+        }
 
-    if (currentVariantInfo.variant && !isCartLoading) {
-      addProductToCart();
-    }
+        if(currentVariantInfo.variant) {
+            addProductToCart();
+        }
   };
 
+
   const buyNowHandler = () => {
     if (!isCurrentSelectionAvailable) {
       showToast("The selected options are not available!", "warning");
       return;
     }
-    if (currentVariantInfo.variant && !isCartLoading) {
+    if (currentVariantInfo.variant) {
       addProductToCart("buynow");
     }
   };
@@ -420,7 +424,6 @@ export function ProductInformation({
       </div>
       <ProductAddToCart
         isAvailable={isCurrentSelectionAvailable}
-        isLoading={isCartLoading}
         onAddToCart={addToCartHandler}
         onBuyNow={buyNowHandler}
         priceInfo={{

+ 0 - 20
src/app/api/clearCartCookie/route.ts

@@ -1,20 +0,0 @@
-import { NextResponse } from "next/server";
-import {
-  GUEST_CART_TOKEN,
-  GUEST_CART_ID,
-} from "@/utils/constants";
-
-
-export async function POST() {
-
-    const response = NextResponse.json({
-        success:true
-    });
-
-
-    response.cookies.delete(GUEST_CART_TOKEN);
-    response.cookies.delete(GUEST_CART_ID);
-
-
-    return response;
-}

+ 0 - 197
src/components/cart/AddToCart.tsx

@@ -1,197 +0,0 @@
-"use client";
-
-import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-import { useSearchParams } from "next/navigation";
-import { useForm, useWatch } from "react-hook-form";
-import { ConfigurableProductIndexData } from "@/types/types";
-import { useAddProduct } from "@utils/hooks/useAddToCart";
-import LoadingDots from "@components/common/icons/LoadingDots";
-import { getVariantInfo } from "@utils/hooks/useVariantInfo";
-import { safeParse } from "@utils/helper";
-import { ProductSwatchReview } from "@/types/category/type";
-
-interface AddToCartFormData {
-  quantity: number;
-  isBuyNow: boolean;
-}
-
-function SubmitButton({
-  selectedVariantId,
-  pending,
-  type,
-  isSaleable,
-}: {
-  selectedVariantId: boolean;
-  pending: boolean;
-  type: string;
-  isSaleable: string;
-}) {
-  const buttonClasses =
-    "relative flex w-full max-w-[16rem] cursor-pointer h-fit items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white";
-  const disabledClasses = "cursor-wait opacity-60";
-
-  if (!isSaleable || isSaleable === "") {
-    return (
-      <button
-        aria-disabled
-        aria-label="Out of stock"
-        type="button"
-        disabled
-        className={clsx(buttonClasses, " opacity-60 !cursor-not-allowed")}
-      >
-        Out of Stock
-      </button>
-    );
-  }
-
-  if (!selectedVariantId && type === "configurable") {
-    return (
-      <button
-        aria-disabled
-        aria-label="Please select an option"
-        type="button"
-        disabled={!selectedVariantId}
-        className={clsx(buttonClasses, " opacity-60 !cursor-not-allowed")}
-      >
-        Add To Cart
-      </button>
-    );
-  }
-
-  return (
-    <button
-      aria-disabled={pending}
-      aria-label="Add to cart"
-      type="submit"
-      className={clsx(buttonClasses, {
-        "hover:opacity-90": true,
-        [disabledClasses]: pending,
-      })}
-      onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
-        if (pending) e.preventDefault();
-      }}
-    >
-      <div className="absolute left-0 ml-4">
-        {pending ? <LoadingDots className="mb-3 bg-white" /> : ""}
-      </div>
-      Add To Cart
-    </button>
-  );
-}
-
-export function AddToCart({
-  productSwatchReview,
-  index,
-  productId,
-  userInteracted,
-}: {
-  productSwatchReview: ProductSwatchReview;
-  productId: string;
-  index: ConfigurableProductIndexData[];
-  userInteracted: boolean;
-}) {
-  const isSaleable = productSwatchReview?.isSaleable || "";
-  const { onAddToCart, isCartLoading } = useAddProduct();
-  const { handleSubmit, setValue, control, register } = useForm<AddToCartFormData>({
-    defaultValues: {
-      quantity: 1,
-      isBuyNow: false,
-    },
-  });
-
-  const quantity = useWatch({
-    control,
-    name: "quantity",
-  });
-
-  const increment = (e: React.MouseEvent) => {
-    e.preventDefault();
-    e.stopPropagation();
-    setValue("quantity", Number(quantity) + 1);
-  };
-
-  const decrement = (e: React.MouseEvent) => {
-    e.preventDefault();
-    e.stopPropagation();
-    setValue("quantity", Math.max(1, Number(quantity) - 1));
-  };
-
-  const searchParams = useSearchParams();
-  const type = productSwatchReview?.type;
-
-  const superAttributes = productSwatchReview?.superAttributeOptions
-    ? safeParse(productSwatchReview.superAttributeOptions)
-    : productSwatchReview?.superAttributes?.edges?.map(
-        (e) => e.node,
-      ) || [];
-
-  const isConfigurable = superAttributes.length > 0;
-
-  const { productid: selectedVariantId, Instock: checkStock } = getVariantInfo(
-    isConfigurable,
-    searchParams.toString(),
-    superAttributes,
-    JSON.stringify(index),
-  );
-  const buttonStatus = !!selectedVariantId;
-
-  const actionWithVariant = async (data: AddToCartFormData) => {
-    const pid =
-      type === "configurable"
-        ? String(selectedVariantId)
-        : (String(productId).split("/").pop() ?? "");
-    onAddToCart({
-      productId: pid,
-      quantity: data.quantity,
-    });
-  };
-
-  return (
-    <>
-      {!checkStock && type === "configurable" && userInteracted && (
-        <div className="gap-1 px-2 py-1 my-2 font-bold text-red-500 dark:text-red-400">
-          <h1>NO STOCK AVAILABLE</h1>
-        </div>
-      )}
-      <form className="flex gap-x-4" onSubmit={handleSubmit(actionWithVariant)}>
-        <div className="flex items-center justify-center">
-          <div className="flex items-center rounded-full border-2 border-blue-500">
-            <div
-              aria-label="Decrease quantity"
-              role="button"
-              className="flex h-12 w-12 cursor-pointer items-center justify-center rounded-l-full text-gray-600 transition-colors hover:text-gray-800 dark:text-white hover:dark:text-white/[80%]"
-              onClick={decrement}
-            >
-              <MinusIcon className="h-4 w-4" />
-            </div>
-
-            <input
-              type="hidden"
-              {...register("quantity", { valueAsNumber: true })}
-            />
-
-            <div className="flex h-12 min-w-[4rem] items-center justify-center px-2 font-medium text-gray-800 dark:text-white">
-              {quantity}
-            </div>
-
-            <div
-              aria-label="Increase quantity"
-              role="button"
-              className="flex h-12 w-12 cursor-pointer items-center justify-center rounded-r-full text-gray-600 transition-colors hover:text-gray-800 dark:text-white hover:dark:text-white/[80%]"
-              onClick={increment}
-            >
-              <PlusIcon className="h-4 w-4" />
-            </div>
-          </div>
-        </div>
-        <SubmitButton
-          pending={isCartLoading}
-          selectedVariantId={buttonStatus}
-          type={type || ""}
-          isSaleable={isSaleable}
-        />
-      </form>
-    </>
-  );
-}

+ 0 - 137
src/components/catalog/product/ProductDescription.tsx

@@ -1,137 +0,0 @@
-"use client";
-import { Price } from "@/components/theme/ui/Price";
-import { Rating } from "@/components/common/Rating";
-import { AddToCart } from "@/components/cart/AddToCart";
-import { VariantSelector } from "./VariantSelector";
-import { ProductMoreDetails } from "./ProductMoreDetail";
-import { useState } from "react";
-import { getVariantInfo } from "@utils/hooks/useVariantInfo";
-import { useSearchParams } from "next/navigation";
-import Prose from "@components/theme/search/Prose";
-import { ProductData , ProductReviewNode } from "../type";
-import { safeCurrencyCode, safePriceValue, safeParse } from "@utils/helper";
-import Link from "next/link";
-
-export function ProductDescription({
-  product,
-  reviews,
-  totalReview,
-  productSwatchReview,
-  avgRating
-}: {
-  product: ProductData;
-  slug: string;
-  reviews: ProductReviewNode[] ; 
-  avgRating : number ;
-  totalReview: number;
-  productSwatchReview: any;
-}) {
-  const priceValue = safePriceValue(product);
-  const currencyCode = safeCurrencyCode(product);
-  const configurableProductIndexData = (safeParse(
-    productSwatchReview?.combinations
-  ) || []) as never[];
-  const searchParams = useSearchParams();
-  const [userInteracted, setUserInteracted] = useState(false);
-
-  const superAttributes = productSwatchReview?.superAttributeOptions
-    ? safeParse(productSwatchReview.superAttributeOptions)
-    : productSwatchReview?.superAttributes?.edges?.map(
-      (e: { node: any }) => e.node
-    ) || [];
-
-  const variantInfo = getVariantInfo(
-    product?.type === "configurable",
-    searchParams.toString(),
-    superAttributes,
-    productSwatchReview?.combinations
-  );
-
-  const additionalData =
-    productSwatchReview?.attributeValues?.edges?.map(
-      (e: { node: any }) => e.node
-    ) || [];
-  const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
-  const handleReviewClick = () => {
-    setExpandedKeys(new Set(["2"]));
-  };
-  
-  return (
-    <>
-      <div className="mb-2 flex flex-col pb-6">
-        {/* Breadcrumb */}
-        <div className="hidden lg:flex flex-col gap-3 shrink-0 mb-2">
-          <Link
-            href="/"
-            className="w-fit text-sm font-medium text-nowrap relative text-neutral-500 before:absolute before:bottom-0 before:left-0 before:h-px before:w-0 before:bg-current before:transition-all before:duration-300 before:content-[''] hover:text-black hover:before:w-full dark:text-neutral-400 dark:hover:text-neutral-300"
-          >
-            Home /
-          </Link>
-        </div>
-        <h1 className="font-outfit text-2xl md:text-3xl lg:text-4xl font-semibold">
-          {product?.name || ""}
-        </h1>
-
-        <div className="flex w-auto justify-between items-baseline gap-y-2 py-4 xs:flex-row xs:gap-y-0 sm:py-6 flex-wrap">
-          <div className="flex gap-4 items-baseline">
-            {product?.type === "configurable" && (
-              <p className="text-base text-gray-600 dark:text-gray-400">
-                As low as
-              </p>
-            )}
-            {product?.type === "simple" ? (
-              <>
-                <Price
-                  amount={String(product?.minimumPrice)}
-                  currencyCode={currencyCode}
-                  className="font-outfit text-xl md:text-2xl font-semibold"
-                />
-              </>
-            ) : (
-              <Price
-                amount={String(priceValue)}
-                currencyCode={currencyCode}
-                className="font-outfit text-xl md:text-2xl font-semibold"
-              />
-            )}
-          </div>
-
-          <Rating
-            length={5}
-            star={avgRating}
-            reviewCount={totalReview}
-            className="mt-2"
-            onReviewClick={handleReviewClick}
-          />
-        </div>
-      </div>
-
-      <VariantSelector
-        variants={variantInfo?.variantAttributes}
-        setUserInteracted={setUserInteracted}
-        possibleOptions={variantInfo.possibleOptions}
-      />
-
-      {product?.shortDescription ? (
-        <Prose className="mb-6 text-base text-selected-black dark:text-white font-light" html={product.shortDescription} />
-      ) : null}
-
-      <AddToCart
-        index={configurableProductIndexData}
-        productId={product?.id || ""}
-        productSwatchReview={productSwatchReview}
-        userInteracted={userInteracted}
-      />
-
-      <ProductMoreDetails
-        additionalData={additionalData}
-        description={product?.description ?? ""}
-        reviews={Array.isArray(reviews) ? reviews : []}
-        totalReview={totalReview}
-        productId={product?.id ?? ""}
-        expandedKeys={expandedKeys}
-        setExpandedKeys={setExpandedKeys}
-      />
-    </>
-  );
-}

+ 0 - 31
src/components/catalog/product/ProductInfo.tsx

@@ -1,31 +0,0 @@
-
-import { getAverageRating } from "@utils/helper";
-import { ProductData } from "../type";
-import { ProductDescription } from "./ProductDescription";
-import { getProductWithSwatchAndReview } from "@/utils/hooks/getProductSwatchAndReview";
-import { ProductReview } from "@/types/category/type";
-import { getProductReviews } from "@utils/hooks/getProductReviews";
-
-export default async function ProductInfo({
-  product,
-  slug,
-  reviews,
-}: {
-  product: ProductData;
-  slug: string;
-  reviews: ProductReview[];
-}) {
-  const productSwatchReview = await getProductWithSwatchAndReview(slug);
-  const getAllreviews = await getProductReviews(product?.id?.split("/").pop() || '')
-  
-  return (
-    <ProductDescription
-      product={product}
-      productSwatchReview={productSwatchReview}
-      slug={slug}
-      reviews={getAllreviews}
-      totalReview={reviews.length}
-      avgRating = {getAverageRating(reviews)}
-    />
-  );
-}

+ 19 - 10
src/components/common/AddToCartModal/AddToCartModal.tsx

@@ -9,6 +9,7 @@ import {
     clearAddToCartProduct 
 } from '@/store/slices/addToCartDialogSlice';
 import { useAddProduct } from "@utils/hooks/useAddToCart";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import {
   Drawer,
   DrawerContent,
@@ -38,7 +39,7 @@ export default function AddToCartModal() {
     const dispatch = useAppDispatch();
     const {isOpen, product} = useAppSelector((state) => state.addToCartDialog);
 
-    const { isCartLoading, onAddToCart } = useAddProduct();
+    const { appendProductToCart } = useAddProduct();
     const { showToast } = useCustomToast();
 
     const images = useMemo(() => {
@@ -180,14 +181,23 @@ export default function AddToCartModal() {
     };
 
     async function addProductToCart() {
-            
+        if(!product) {
+            showToast("Get product detail failed!", "danger");
+            return;
+        }
+        overlayLoading.start(); 
         const params = { 
-            productId: String(product?._id), 
+            productId: product._id, 
             quantity: productQty,
             variantId: currentVariantInfo.variant?._id,
         };
-        const res = await onAddToCart(params);
-        console.log('onAddToCart run ----- 1');
+        const res = await appendProductToCart({
+            productId: params.productId,
+            quantity: params.quantity,
+            variantId: params.variantId
+        });
+        console.log('onAddToCart run ----- 1',res);
+        overlayLoading.stop();
         return res;
     } 
 
@@ -197,7 +207,7 @@ export default function AddToCartModal() {
             return;
         }
 
-        if(currentVariantInfo.variant && !isCartLoading) {
+        if(currentVariantInfo.variant) {
             await addProductToCart();
             callback();
         }
@@ -208,11 +218,11 @@ export default function AddToCartModal() {
             return;
         }
 
-        if(currentVariantInfo.variant && !isCartLoading) {
+        if(currentVariantInfo.variant) {
             const res = await addProductToCart();
             callback();
-            if(res) {
-                const responseData = res.data?.createAddProductInCart?.addProductInCart;
+            if(res && !res.error) {
+                const responseData = res.data;
                 if(responseData && responseData.success) {
                     redirect('/checkout', RedirectType.push);
                 }
@@ -295,7 +305,6 @@ export default function AddToCartModal() {
                             <div>
                                 <FooterBtnInAddToCartModal 
                                     isAvailable={isCurrentSelectionAvailable} 
-                                    isLoading={isCartLoading}
                                     onAddToCart={()=> addToCartHandler(onClose)}
                                     onBuyNow={()=> buyNowHandler(onClose)}
                                 />

+ 3 - 5
src/components/common/AddToCartModal/FooterBtnInAddToCartModal.tsx

@@ -1,15 +1,13 @@
 "use client";
 
 import clsx from "clsx";
-import { LoadingSpinner } from "@components/common/LoadingSpinner";
+
 export default function FooterBtnInAddToCartModal({
     isAvailable,
-    isLoading,
     onBuyNow,
     onAddToCart
 }: {
     isAvailable: boolean; 
-    isLoading: boolean;
     onBuyNow: () => void,
     onAddToCart: () => void
 }) {
@@ -22,13 +20,13 @@ export default function FooterBtnInAddToCartModal({
                     <button className={clsx(btnClass,'bg-ly-deepgreen')}
                         onClick={onBuyNow}
                     >
-                        Buy Now {isLoading && <LoadingSpinner className="ml-2" />}
+                        Buy Now
                         
                     </button>
                     <button className={clsx(btnClass, 'bg-ly-middlegreen')}
                         onClick={onAddToCart}
                     >
-                        Add to Cart {isLoading && <LoadingSpinner className="ml-2" />}
+                        Add to Cart
                     </button>
                 </>)
             :

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

@@ -7,7 +7,7 @@ import Image from "next/image";
 import { useApolloClient } from "@apollo/client/react";
 // import { useCustomToast } from "@utils/hooks/useToast";
 import { useConfig } from "@utils/hooks/useConfig";
-import {switchCurrencyAction} from "@/actions/switchCurrencyAction";
+import {switchCurrencyAction} from "@/actions";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 
 function getCurrencyFlag(currencyCode: string) {

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

@@ -6,7 +6,7 @@ import { signIn } from "next-auth/react";
 import clsx from "clsx";
 import { useForm, Controller } from "react-hook-form";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE } from "@utils/constants";
-import {mergeCartAction} from "@/actions/mergeCartAction";
+import {mergeCartAction} from "@/actions";
 import { useAppDispatch } from "@/store/hooks";
 import { updateCart } from "@/store/slices/cart-slice";
 import { useCustomToast } from "@/utils/hooks/useToast";

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

@@ -1,12 +1,14 @@
+"use client";
 
-import { getIsGuest } from "@utils/cookie-tools";
+import { getSession } from "next-auth/react";
 import { useRouter } from "next/navigation";
 
 export const ReviewButton = ({ setShowForm, className }: { setShowForm: (show: boolean) => void, className?: string }) => {
-    const IsGuest = getIsGuest();
+
     const router = useRouter();
-    const handleAddReview = () => {
-        if (IsGuest) {
+    const handleAddReview = async () => {
+        const session = await getSession()
+        if (!session) {
             router.push("/customer/login");
         } else {
             setShowForm(true);

+ 2 - 2
src/components/common/icons/cart/DeleteItemButton.tsx

@@ -43,10 +43,10 @@ interface CartItemEdge {
 }
 
 export function DeleteItemButton({ item }: { item: CartItemEdge }) {
-  const { onAddToRemove, isRemoveLoading } = useAddProduct();
+  const { deleteProductFromCart, isRemoveLoading } = useAddProduct();
   const itemId = item?.node?.id;
   const handleRemoveCart = () => {
-    onAddToRemove(itemId);
+    deleteProductFromCart(Number(itemId));
   };
 
   return (

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

@@ -1,7 +1,7 @@
 "use client";
 
 import clsx from "clsx";
-import { getSession, signIn } from "next-auth/react";
+import { signIn } from "next-auth/react";
 import Image from "next/image";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
@@ -10,18 +10,9 @@ import { Button } from "@components/common/button/Button";
 import { EMAIL_REGEX, SIGNIN_IMG } from "@/utils/constants";
 import InputText from "@components/common/form/Input";
 import { useCustomToast } from "@/utils/hooks/useToast";
-import { useMergeCart } from "@utils/hooks/useMergeCart";
-
-import { 
-  setCookie, 
-  getGuestCartToken,
-  getGuestCartId,
-  deleteGuestCookie
-} from "@/utils/cookie-tools";
-
+import { mergeCartAction, deleteGuestCookieAction } from "@/actions";
 import { useAppSelector } from "@/store/hooks";
 import { useCartDetail } from "@utils/hooks/useCartDetail";
-import { IS_GUEST } from "@/utils/constants";
 
 type LoginFormInputs = {
   username: string;
@@ -32,7 +23,7 @@ export default function LoginForm() {
   const router = useRouter();
   const { showToast } = useCustomToast();
   const { getCartDetail } = useCartDetail()
-  const { mergeCart } = useMergeCart();
+
   const cart = useAppSelector((state) => state.cartDetail.cart);
   const {
     register,
@@ -45,14 +36,7 @@ export default function LoginForm() {
 
   const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
     try {
-      // First, handle cart merging before sign in
-      const guestCartId = getGuestCartId();
-      const guestCartToken = getGuestCartToken();
-
-      /**
-       * @todo 使用 signInAuth 重写
-       * result: {error: null,ok: true,status: 200,url: "http://localhost:3001/"}
-       */
+
       const result = await signIn("credentials", {
         redirect: false,
         ...data,
@@ -67,38 +51,17 @@ export default function LoginForm() {
       showToast("Welcome! Successfully logged in.", "success");
 
 
-      const session = await getSession();
-      const userToken: string | undefined = session?.user?.accessToken;
-
-      if (!userToken) {
-        console.warn("No API token available in session after login");
-        console.error('userToken is required');
-      }
-
-
-      // Only merge cart if user had a guest cart before login
-      if (userToken && guestCartId && guestCartToken) {
-        try {
-          if(cart) {
-            await mergeCart({ variables: { token: userToken, cartId: parseInt(guestCartId, 10) } });
-          }
-          
-        } catch (err) {
-          console.error("mergeCart failed:", err);
+        if(cart) {
+          await mergeCartAction();
+        } else {
+          await deleteGuestCookieAction();
+          await getCartDetail();
         }
-        // 登录成功后不应该保存用户的token
-        setCookie(IS_GUEST, "false");
-        deleteGuestCookie();
-        await getCartDetail();
-      } else if (userToken) {
-        // User logged in without a guest cart, just set the token
-        setCookie(IS_GUEST, "false");
-        deleteGuestCookie();
-      }
-      setTimeout(() => {
-        router.push("/");
-        router.refresh();
-      }, 100);
+
+        setTimeout(() => {
+          router.push("/");
+          router.refresh();
+        }, 100);
 
 
     } catch (error) {

+ 4 - 8
src/components/customer/credentials/CredentialModal.tsx

@@ -13,14 +13,10 @@ import { useBodyScrollLock } from "@utils/hooks/useBodyScrollLock";
 import OpenAuth from "../OpenAuth";
 import { isObject } from '@/utils/type-guards';
 import LoadingDots from "@components/common/icons/LoadingDots";
-import { logoutAction } from "@utils/actions";
+import { logoutAction } from "@/actions";
 import { useAppDispatch } from "@/store/hooks";
 import { clearCart } from "@/store/slices/cart-slice";
-import { 
-  setCookie, 
-  deleteGuestCookie
-} from "@utils/cookie-tools";
-import { IS_GUEST } from "@/utils/constants";
+
 
 export default function CredentialModal({
   children,
@@ -65,10 +61,12 @@ export default function CredentialModal({
 
   const onSubmit = async () => {
     try {
+  
       const res = await logoutAction();
 
       if (!res.success) {
         showToast(res.message, "danger");
+        return;
       }
 
       await signOut({
@@ -76,8 +74,6 @@ export default function CredentialModal({
         redirect: false,
       });
 
-      deleteGuestCookie(); // 这里本质是要删除登录用户的token
-      setCookie(IS_GUEST, 'true');
       dispatch(clearCart());
       showToast("You are logged out successfully!", "success");
       setTimeout(() => {

+ 2 - 2
src/graphql/cart/mutations/AddProductToCart.ts

@@ -1,7 +1,7 @@
 import { gql, TypedDocumentNode } from "@apollo/client";
-import { AddToCartData } from "@/types/cart/type";
+import { AddToCartData,AddToCartVariables } from "@/types/cart/type";
 
-export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData> = gql`
+export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData,AddToCartVariables> = gql`
   mutation createAddProductInCart(
     $cartId: Int
     $productId: Int!

+ 3 - 2
src/graphql/cart/mutations/CreateCartToken.ts

@@ -1,6 +1,7 @@
-import { gql } from "@apollo/client";
+import { gql, TypedDocumentNode } from "@apollo/client";
+import { CreateCartTokenData } from "@/types/cart/type";
 
-export const CREATE_CART_TOKEN = gql`
+export const CREATE_CART_TOKEN: TypedDocumentNode<CreateCartTokenData> = gql`
   mutation CreateCart {
     createCartToken(input: {}) {
       cartToken {

+ 0 - 11
src/providers/NextAuthProvider.tsx

@@ -1,11 +0,0 @@
-"use client";
-import { SessionProvider } from "next-auth/react";
-import { ReactNode } from "react";
-
-export const NextAuthProvider = ({
-  children,
-}: {
-  children: ReactNode;
-}) => {
-  return <SessionProvider>{children}</SessionProvider>;
-}

+ 0 - 8
src/providers/SessionProvider.tsx

@@ -1,8 +0,0 @@
-"use client";
-
-import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
-import { ReactNode } from "react";
-
-export function SessionProvider({ children }: { children: ReactNode }) {
-  return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
-}

+ 2 - 2
src/providers/index.ts

@@ -1,11 +1,11 @@
 export { ApolloWrapper } from "./ApolloWrapper";
 export { GlobalContextProvider } from "./GlobalContextProvider";
 export { GlobalProviders } from "./GlobalProviders";
-export { NextAuthProvider } from "./NextAuthProvider";
+
 export { ReduxProvider } from "./ReduxProvider";
 export { SessionManager } from "./SessionManager";
 export { ThemeProvider } from "./ThemeProvider";
 export { ToastProvider, useToast } from "./ToastProvider";
-export { SessionProvider } from "./SessionProvider";
+
 export { PayPalWrapper } from "./PaypalWrapper";
 export { ConfigProvider } from "./ConfigProvider";

+ 103 - 0
src/server-service/guestCartTokenService.ts

@@ -0,0 +1,103 @@
+import {cookies} from "next/headers";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@utils/auth";
+import { BagistoSession } from "@/types/types";
+import { serverGraphqlFetch } from "@utils/bagisto";
+import { CREATE_CART_TOKEN } from "@/graphql";
+import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST, GUEST_COOKIE_OPTION } from "@/utils/constants";
+import { CreateCartTokenData } from "@/types/cart/type";
+
+
+export async function deleteGuestCookie() {
+    const authSession = (await getServerSession(authOptions)) as BagistoSession | null;
+    const accessToken = authSession?.user?.accessToken;
+    const isGuest = accessToken ? false : true;
+    const cookieStore = await cookies();
+    if(isGuest) {
+        cookieStore.delete(GUEST_CART_TOKEN);
+        cookieStore.delete(GUEST_CART_ID);
+        cookieStore.set(IS_GUEST, "true");
+    } else {
+        cookieStore.set(IS_GUEST, "false");
+    }
+
+}
+
+
+export async function setGuestCookie({
+    guestToken,
+    guestCartId,
+    isGuest
+}: {
+    guestToken: string;
+    guestCartId: string;
+    isGuest: boolean;
+}) {
+    const cookieOptions = {
+        httpOnly: GUEST_COOKIE_OPTION.httpOnly,
+        maxAge: GUEST_COOKIE_OPTION.maxAge,
+        path: GUEST_COOKIE_OPTION.path,
+        secure: GUEST_COOKIE_OPTION.secure,
+        sameSite: GUEST_COOKIE_OPTION.sameSite,
+    };
+
+    const cookieStore = await cookies();
+    cookieStore.set(GUEST_CART_TOKEN,guestToken,cookieOptions);
+    cookieStore.set(GUEST_CART_ID,guestCartId,cookieOptions);
+    cookieStore.set(IS_GUEST,isGuest ? "true" : "false");
+}
+
+export async function createGuestCartToken() {
+
+    const cookieStore = await cookies();
+
+    const existingToken = cookieStore.get(GUEST_CART_TOKEN)?.value;
+    const existingCartId = cookieStore.get(GUEST_CART_ID)?.value;
+
+    if (existingToken && existingCartId) {
+        return {
+            success: true,
+            cartToken: existingToken,
+            cartId: existingCartId,
+            msg: ''
+        };
+    }
+
+    try{
+         
+        const response = await serverGraphqlFetch<CreateCartTokenData>({
+            query: CREATE_CART_TOKEN,
+            cache: "no-store",
+            takeAuthorization: false
+        });
+        const success = response.error ? false : true;
+        let guestToken = null;
+        let guestCartId = null;
+        const msg = response.error ? response.error.message : '';
+        if(success && response.data.createCartToken) {
+            guestToken = response.data.createCartToken.cartToken.sessionToken;
+            guestCartId = response.data.createCartToken.cartToken.id;
+
+            await setGuestCookie({
+                guestToken,
+                guestCartId,
+                isGuest: true
+            });
+        }
+
+        
+        return {
+            success: success,
+            cartToken: guestToken,
+            cartId: guestCartId,
+            msg: msg
+        };
+    } catch(e: any) {
+        return {
+            success:false,
+            cartToken: null,
+            cartId: null,
+            msg: e.message || 'Create guest cart token failed!'
+        };
+    }
+}

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

@@ -126,6 +126,7 @@ export interface AddToCartVariables {
   cartId?: number | null;
   productId: number;
   quantity: number;
+  variantId?: number;
 }
 
 

+ 8 - 1
src/types/types.ts

@@ -1,7 +1,14 @@
 import { SVGProps } from "react";
 import { Session } from "next-auth";
 
-
+export interface LogoutData { 
+  createLogout: { 
+    logout: { 
+      success: boolean; 
+      message: string 
+    } 
+  } 
+}
 export interface BagistoSession extends Session {
   user: {
     id: string;

+ 0 - 4
src/utils/actions.ts

@@ -3,7 +3,6 @@
 import { redirect } from "next/navigation";
 import {
   createUserToLogin,
-  logoutUser,
   recoverUserLogin,
   subscribeUser,
 } from '@/utils/bagisto';
@@ -139,6 +138,3 @@ export async function userSubscribe(
 }
 
 
-export async function logoutAction() {
-  return await logoutUser();
-}

+ 19 - 51
src/utils/bagisto/index.ts

@@ -13,7 +13,6 @@ import {
 } from "../constants";
 import { getServerSession } from "next-auth";
 import {
-  CUSTOMER_LOGOUT,
   CUSTOMER_REGISTRATION,
   FORGET_PASSWORD,
 } from "@/graphql/customer/mutations";
@@ -211,6 +210,7 @@ export async function serverGraphqlFetch<
   tags,
   variables,
   revalidate = 0,
+  takeAuthorization = true, // 是否携带token,默认携带
 }: {
   cache?: RequestCache;
   headers?: HeadersInit | Record<string, string>;
@@ -218,17 +218,20 @@ export async function serverGraphqlFetch<
   tags?: string[];
   variables?: TVariables;
   revalidate?: number;
+  takeAuthorization?: boolean;
 }): Promise<FetchGraphqlResult<TData>> {
   try {
     const queryString = typeof query === "string" ? query : print(query);
 
-    const tokenRes = await getAuthorizationToken();
+    
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
-
-
-    if (tokenRes.token) {
-      baseHeaders['Authorization'] = `Bearer ${tokenRes.token}`;
+    
+    if(takeAuthorization) {
+      const tokenRes = await getAuthorizationToken();
+      if (tokenRes.token) {
+        baseHeaders['Authorization'] = `Bearer ${tokenRes.token}`;
+      }
     }
 
     if (headers) {
@@ -276,6 +279,7 @@ export async function bagistoFetch<T>({
   variables,
   // isCookies = true,
   // guestToken,
+  takeAuthorization = true, // 是否携带token,默认携带
   revalidate = 60,
 }: {
   cache?: RequestCache;
@@ -285,13 +289,14 @@ export async function bagistoFetch<T>({
   variables?: ExtractVariables<T>;
   // isCookies?: boolean;
   // guestToken?: string;
+  takeAuthorization?: boolean;
   revalidate?: number;
 }): Promise<{ status: number; body: ExtractGraphqlData<T> } | never> {
   try {
     const queryString =
       typeof query === "string" ? query : print(query);
 
-    const tokenRes = await getAuthorizationToken();
+    
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
     /*
@@ -311,9 +316,14 @@ export async function bagistoFetch<T>({
       baseHeaders.Authorization = `Bearer ${guestToken}`;
     }
     */
-    if (tokenRes.token) {
-      baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+    if(takeAuthorization) {
+      const tokenRes = await getAuthorizationToken();
+      if (tokenRes.token) {
+        baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+      }
+
     }
+    
 
     
 
@@ -432,48 +442,6 @@ export async function createUserToLogin(
   }
 }
 
-export async function logoutUser() {
-  try {
-    const session = await getServerSession(authOptions);
-    const token = session?.user?.accessToken;
-
-    if (!token) {
-      return {
-        success: false,
-        message: "User token missing",
-      };
-    }
-    /**
-     * @todo CUSTOMER_LOGOUT 接口会报错,待修复(确定是php后端接口报错)
-     */
-    const res = await bagistoFetch<{
-      data: { createLogout: { logout: { success: boolean; message: string } } };
-      variables: { input: { token: string } };
-    }>({
-      query: CUSTOMER_LOGOUT,
-      // isCookies: true,
-      revalidate: 3600,
-    });
-
-    const success = res?.body?.data?.createLogout?.logout?.success ?? false;
-
-    const message =
-      res?.body?.data?.createLogout?.logout?.message ?? "Logout executed";
-
-
-
-
-    return {
-      success,
-      message,
-    };
-  } catch (error: unknown) {
-    return {
-      success: false,
-      message: error instanceof Error ? error.message : "Something went wrong",
-    };
-  }
-}
 
 export async function recoverUserLogin(
   input: Record<string, unknown>,

+ 4 - 2
src/utils/constants.ts

@@ -107,13 +107,15 @@ export const SortByFields: SortOrderTypes[] = [
 ];
 
 export const DEFAULT_EXPIRES_DAYS = 7; // cookie里的 GUEST_CART_TOKEN GUEST_CART_ID IS_GUEST 的过期时间
+const sameSite: boolean | "lax" | "none" | "strict" | undefined = "lax";
 export const GUEST_COOKIE_OPTION = {
-    days: DEFAULT_EXPIRES_DAYS,
+    httpOnly: true,
+    maxAge: DEFAULT_EXPIRES_DAYS * 24 * 3600,
     encode: true,          // 默认编码,更加安全
     path: '/',
     // domain:,
     secure: process.env.NEXT_PUBLIC_APP_ENV !== 'development',
-    sameSite: 'lax',
+    sameSite: sameSite,
 };
 
 export const GUEST_CART_TOKEN = "guest_cart_token";

+ 4 - 23
src/utils/cookie-tools.ts

@@ -34,12 +34,12 @@ export const deleteCookie = (name: string) => {
   document.cookie = `${name}=; Max-Age=0; path=/`;
 };
 */
-import {GUEST_COOKIE_OPTION,GUEST_CART_TOKEN,IS_GUEST,GUEST_CART_ID} from "@/utils/constants";
+import {GUEST_COOKIE_OPTION} from "@/utils/constants";
 // cookie.ts
 
 export interface CookieOptions {
   /** 过期天数(从当前时间起算),默认 7 天 */
-  days?: number;
+  maxAge?: number;
   /** 是否需要对值进行编码(encodeURIComponent),默认 false */
   encode?: boolean;
   /** Cookie 路径,默认 '/' */
@@ -84,7 +84,7 @@ export function setCookie(
   if (typeof document === 'undefined') return;
   const setOption = Object.assign({},GUEST_COOKIE_OPTION,options);
   const {
-    days,
+    maxAge,
     encode,          // 默认编码,更加安全
     path,
     domain,
@@ -93,7 +93,7 @@ export function setCookie(
   } = setOption;
 
   // 处理过期时间
-  const expires = new Date(Date.now() + days * 864e5).toUTCString();
+  const expires = new Date(Date.now() + maxAge * 1000).toUTCString();
 
   // 处理值:编码或直接转换
   const val = encode
@@ -135,22 +135,3 @@ export function deleteCookie(
   document.cookie = cookie;
 }
 
-export function getGuestCartId(): string | null {
-  const res = getCookie(GUEST_CART_ID);
-  return res;
-}
-export function getIsGuest(): boolean {
-  const res = getCookie(IS_GUEST);
-  return res === 'true' || res === null;
-}
-export const getGuestCartToken = (): string | null => {
-  const raw = getCookie(GUEST_CART_TOKEN);
-  if (!raw) return null;
-
-  return raw;
-};
-
-export function deleteGuestCookie() {
-    deleteCookie(GUEST_CART_ID);
-    deleteCookie(GUEST_CART_TOKEN);
-}

+ 0 - 55
src/utils/fetch-handler.ts

@@ -1,55 +0,0 @@
-type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
-
-interface FetchHandlerOptions<TBody = unknown> {
-  url: string; // API route, e.g., "addToCart"
-  method?: Method;
-  body?: TBody;
-  headers?: Record<string, string>;
-  contentType?: boolean;
-}
-
-export async function fetchHandler({
-  url,
-  method = "GET",
-  body,
-  headers = {},
-  contentType = true,
- 
-}: FetchHandlerOptions): Promise<any> {
-  try {
-    const defaultHeaders: Record<string, string> = {
-      ...(contentType ? { "Content-Type": "application/json" } : {}),
-      ...headers,
-    };
-
-    const response = await fetch(`/api/${url}`, {
-      method,
-      headers: defaultHeaders,
-      body: body ? JSON.stringify(body) : undefined,
-    });
-   
-     
-    const result = await response.json();
-    if (!response.ok) {
-      return {
-        data: null,
-        error: {
-          status: response?.status,
-          message: result?.error || "Something went wrong",
-        },
-      };
-    }
-    return {
-      ...result,
-    };
-  } catch (err) {
-    const error = err instanceof Error ? err.message : "Unknown error";
-
-    return {
-      data: null,
-      error: {
-        message: error,
-      },
-    };
-  }
-}

+ 46 - 158
src/utils/hooks/useAddToCart.ts

@@ -1,128 +1,59 @@
 "use client";
 
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
 import { useCustomToast } from "./useToast";
 import { useAppDispatch } from "@/store/hooks";
 import { addItem, clearCart } from "@/store/slices/cart-slice";
 import { isObject } from "@utils/type-guards";
-import { 
-  setCookie, 
-  getIsGuest,
-  getGuestCartToken,
-  deleteGuestCookie
-} from "@utils/cookie-tools";
-import { useGuestCartToken } from "./useGuestCartToken";
-import { IS_GUEST,GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
 import { useMutation, useApolloClient } from "@apollo/client/react";
 import {
-  CREATE_ADD_PRODUCT_IN_CART,
-  REMOVE_CART_ITEM,
   UPDATE_CART_ITEM,
 } from "@/graphql";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
-
+import {addProductToCartAction, deleteCartProductAction} from '@/actions';
+import { AddToCartVariables, AddProductInCart } from "@/types/cart/type";
 
 export const useAddProduct = () => {
   const dispatch = useAppDispatch();
-  const { createGuestToken } = useGuestCartToken();
   const { showToast } = useCustomToast();
   const apolloClient = useApolloClient();
-
-  const [mutateAsync, { loading: isCartLoading }] = useMutation(
-    CREATE_ADD_PRODUCT_IN_CART,
-    {
-      onCompleted: (res) => {
-        console.log('useAddToCart onCompleted run ----- 0');
-        const responseData = res?.createAddProductInCart?.addProductInCart;
-
-        if (!responseData?.success) {
-          showToast(responseData?.message || "Error adding to cart", "danger");
-          return;
-        }
-        if (responseData) {
-          if (responseData.success) {
-            /** 兜底代码 start*/
-            // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
-            const isGuest = getIsGuest();// true 是游客
-            // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
-            const responseCartId = responseData.id;
-            const responseCartToken = responseData.cartToken;
-            if(isGuest && responseCartId !== responseCartToken) {
-                setCookie(GUEST_CART_TOKEN, responseCartToken,{encode: false});
-                setCookie(GUEST_CART_ID, responseCartId);
-                setCookie(IS_GUEST, String(isGuest));
+  const [isRemoveLoading, setIsRemoveLoading] = useState(false);
+
+  const appendProductToCart = async (param: AddToCartVariables) => {
+      const res = await addProductToCartAction(param);
+      const result: {
+        data: AddProductInCart | null;
+        error: boolean;
+      } = {
+        data: null,
+        error: true
+      };
+      if(res.success) {
+          const responseData = res.cartData?.createAddProductInCart?.addProductInCart;
+          if (responseData) {
+            if (responseData.success) {
+                const cartDetail = formatCartDetail(responseData);
+                dispatch(addItem(cartDetail));
+                showToast("Product added to cart successfully", "success");
+                result.data = responseData;
+                result.error = false;
             }
-            /** 兜底代码 end*/
-
-            const cartDetail = formatCartDetail(responseData);
-            dispatch(addItem(cartDetail));
-            showToast("Product added to cart successfully", "success");
           }
-        }
-      },
-
-      onError: (err) => {
-        handleApolloBusinessError(err,(error) => {
-          showToast(error.message, "danger");
-        });
-      },
-    },
-  );
-
-  const onAddToCart = async ({
-    productId,
-    quantity,
-    variantId
-  }: {
-    productId: string;
-    quantity: number;
-    variantId?: number;
-    token?: string;
-    cartId?: number | string;
-  }) => {
-    
-    // Ensure token exists - create if needed
-    let token = getGuestCartToken(); // 从cookie获取token
-    const isGuest = getIsGuest();
-
-    if (!token && isGuest) {
-      // 没有token,则创建一个并写入cookie
-      token = await createGuestToken();
+          
 
-      if (!token) {
-        showToast("Failed to create cart session", "danger");
-        return;
+      } else {
+          showToast(res.msg, "danger");
       }
-    }
-    
-    const param : { productId: number; quantity:number; variantId?: number; } = {
-      productId: parseInt(productId),
-      quantity,
-    };
-    if(variantId) {
-      param.variantId = variantId;
-    }
-
-    const result = await mutateAsync({
-      variables: param,
-    });
-
-    return {
-      data: result.data,
-      error: result.error,
-    };
+      return result;
   };
 
   // 删除购物车中的产品
-  const deleteProductFromCart = useCallback((cartItemId: number) => {
-      return apolloClient.mutate({
-          mutation: REMOVE_CART_ITEM,
-          variables: {
-            cartItemId: cartItemId
-          },
-      }).then((res) => {
-          const resCatData =  res.data?.createRemoveCartItem?.removeCartItem ?? null;
+  const deleteProductFromCart = useCallback(async (cartItemId: number) => {
+      setIsRemoveLoading(true);
+      const deleteRes = await deleteCartProductAction(cartItemId);
+      if(deleteRes.success) {
+          const resCatData =  deleteRes.cartData?.createRemoveCartItem?.removeCartItem ?? null;
           if(resCatData && resCatData.itemsQty) {
             const cartDetail = formatCartDetail(resCatData);
             dispatch(addItem(cartDetail));
@@ -130,27 +61,25 @@ export const useAddProduct = () => {
           } 
           
           if(!resCatData || !resCatData?.itemsQty) {
-              dispatch(clearCart());
-              // @todo 可以优化为通过接口或者server action 删除cookie
-              const isGuest = getIsGuest();
-              if (isGuest) {
-                deleteGuestCookie();
-              }
+             dispatch(clearCart());
           }
+          setIsRemoveLoading(false);
           return {
               data: resCatData?.itemsQty ? formatCartDetail(resCatData) : null,
               error: false,
               msg: ""
           };
+
+      } else {
+        setIsRemoveLoading(false);
+        return {
+            data: null,
+            error: true,
+            msg: deleteRes.msg,
+        }
+      }
       
-      }).catch((err) => {
-          return {
-              data: null,
-              error: true,
-              msg: err.message,
-          }
-      });
-  },[apolloClient]);
+  },[]);
   
   // 修改购物车中产品的数量
   const editProductQtyFromCart = useCallback((cartItemId: number,quantity:number) => {
@@ -189,45 +118,6 @@ export const useAddProduct = () => {
       });
   },[apolloClient]);
 
-  //--------Remove Cart Product Quantity--------//
-  const [removeFromCart, { loading: isRemoveLoading }] = useMutation(
-    REMOVE_CART_ITEM,
-    {
-      onCompleted: async (response) => {
-        const responseData = response?.createRemoveCartItem?.removeCartItem;
-        if (isObject(responseData)) {
-          const message = "Cart item removed successfully";
-          const cartDetail = formatCartDetail(responseData);
-          dispatch(addItem(cartDetail));
-          showToast(message as string, "warning");
-
-          if (!responseData?.itemsQty) {
-            dispatch(clearCart());
-
-            const isGuest = getIsGuest();
-            if (isGuest) {
-              deleteGuestCookie();
-            }
-          }
-        } else {
-          showToast("Something went wrong", "warning");
-        }
-      },
-      onError: (error) => {
-        handleApolloBusinessError(error,(err) => {
-          showToast(err.message, "danger");
-        });
-      },
-    },
-  );
-
-  const onAddToRemove = async (productId: string) => {
-    await removeFromCart({
-      variables: {
-        cartItemId: parseInt(productId),
-      },
-    });
-  };
 
   //---------Update Cart Product Quantity--------//
   const [updateCartItem, { loading: isUpdateLoading }] = useMutation(
@@ -273,13 +163,11 @@ export const useAddProduct = () => {
   };
 
   return {
-    isCartLoading,
-    onAddToCart,
     isRemoveLoading,
-    onAddToRemove,
     onUpdateCart,
     isUpdateLoading,
     deleteProductFromCart,
-    editProductQtyFromCart
+    editProductQtyFromCart,
+    appendProductToCart
   };
 };

+ 0 - 110
src/utils/hooks/useGuestCartToken.ts

@@ -1,110 +0,0 @@
-"use client";
-
-import { useState, useRef } from "react";
-import { fetchHandler } from "../fetch-handler";
-import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
-import { 
-  setCookie, 
-  getGuestCartToken,
-  getGuestCartId,
-  deleteGuestCookie
-} from "@/utils/cookie-tools";
-import { CREATE_CART_TOKEN } from "@/graphql";
-
-// ---------------------------
-// Main Hook
-// ---------------------------
-export const useGuestCartToken = () => {
-
-  const [token, setToken] = useState(() => {
-      const guestCartToken = getGuestCartToken();
-      return guestCartToken;
-  });
-  const [cartId, setCartId] = useState(() => {
-    const guestCartId = getGuestCartId();
-    
-    return guestCartId ? Number(guestCartId) : null;
-  });
-  // const [isReady, setIsReady] = useState(true);
-
-  const isResettingRef = useRef(false);
-  const tokenCreatedRef = useRef(false);
-  const tokenPromiseRef = useRef<Promise<string | null> | null>(null);
-
-  const createGuestToken = async (): Promise<string | null> => {
-    if (tokenPromiseRef.current) return tokenPromiseRef.current;
-
-    tokenPromiseRef.current = (async () => {
-      if (tokenCreatedRef.current) {
-        // Return existing raw token from cookie
-        const cookieVal = getGuestCartToken();
-        if (cookieVal) {
-          return cookieVal;
-        }
-        return null;
-      }
-      tokenCreatedRef.current = true;
-
-      try {
-        const query = CREATE_CART_TOKEN;
-        const queryString = typeof query === "string" ? query : (query.loc?.source?.body ?? "");
-        const res = await fetchHandler({
-          url: "graphql",
-          method: "POST",
-          body: { operationName: "CreateCart",query: queryString },
-          contentType: true,
-        });
-
-        const cart = res?.data?.createCartToken?.cartToken;
-        if (!cart) {
-          tokenCreatedRef.current = false;
-          return null;
-        }
-
-        const newCartId = Number(cart.id);
-
-        setCookie(GUEST_CART_TOKEN, cart.sessionToken,{encode: false});
-        setCookie(GUEST_CART_ID, String(newCartId));
-        setCookie(IS_GUEST, String(cart?.isGuest));
-
-        // State and return should be the RAW token
-        setToken(cart.sessionToken);
-        setCartId(newCartId);
-        return cart.sessionToken;
-      } catch (e) {
-        console.error("Error creating guest token:", e);
-        tokenCreatedRef.current = false;
-        return null;
-      } finally {
-        tokenPromiseRef.current = null;
-      }
-    })();
-
-    return tokenPromiseRef.current;
-
-
-  };
-
-  const resetGuestToken = async () => {
-    if (isResettingRef.current) return;
-    isResettingRef.current = true;
-
-    tokenCreatedRef.current = false;
-
-    // delete old
-    deleteGuestCookie();
-
-    await createGuestToken();
-
-    isResettingRef.current = false;
-  };
-
-
-  return {
-    token,
-    cartId,
-    // isReady,
-    createGuestToken,
-    resetGuestToken,
-  };
-};

+ 0 - 37
src/utils/hooks/useMergeCart.ts

@@ -1,37 +0,0 @@
-"use client";
-
-import { useMutation } from "@apollo/client/react";
-import { useAppDispatch } from "@/store/hooks";
-import { addItem } from "@/store/slices/cart-slice";
-import { CREATE_MERGE_CART } from "@/graphql";
-import { GUEST_CART_ID } from "@/utils/constants";
-import { setCookie } from "@utils/cookie-tools";
-import { formatCartDetail } from "@/utils/cartDetailTools";
-
-export function useMergeCart() {
-  const dispatch = useAppDispatch();
-
-  const [mergeCart, { loading: isLoading }] = useMutation(CREATE_MERGE_CART, {
-    onCompleted: (response) => {
-      const responseData = response?.createMergeCart?.mergeCart;
-      if (!responseData) {
-        return;
-      }
-       const cartId = responseData?.id ?? null;
-
-      if (cartId !== null && typeof cartId !== "undefined") {
-        setCookie(GUEST_CART_ID, String(cartId));
-      }
-      const cartDetail = formatCartDetail(responseData);
-
-      dispatch(addItem(cartDetail));
-    },
-    onError: (_error) => {
-    },
-  });
-
-  return {
-    mergeCart,
-    isLoading,
-  };
-}

+ 0 - 36
src/utils/signInAuth.ts

@@ -1,36 +0,0 @@
-
-
-import { bagistoFetch, /*restApiFetch*/} from "@/utils/bagisto";
-import { CUSTOMER_LOGIN } from "@/graphql/customer/mutations";
-
-
-type LoginFormInputs = {
-  username: string;
-  password: string;
-};
-export default async function signInAuth(loginData:LoginFormInputs) {
-    const input = {
-        email: loginData.username,
-        password: loginData.password,
-    };
-    const res = await bagistoFetch<any>({
-        query: CUSTOMER_LOGIN,
-        variables: { input },
-        cache: "no-store",
-    });
-    const data = res?.body?.data?.createCustomerLogin?.customerLogin;
-        
-
-    if (!data || !data.success || !data.token) {
-        throw new Error(data?.message || "Invalid credentials.");
-    }
-
-    return {
-        id: data.id, // required by NextAuth
-        email: loginData.username,
-        name: loginData.username, // Using email as name since firstName/lastName are missing in response
-        apiToken: data.apiToken,
-        accessToken: data.token, // Sanctum token
-        role: "customer",
-    };
-}