Jelajahi Sumber

合并dev-vipplus

fogwind 2 minggu lalu
induk
melakukan
aa9911f3cf
59 mengubah file dengan 1557 tambahan dan 558 penghapusan
  1. 2 0
      README.md
  2. 54 0
      src/actions/mergeCartAction.ts
  3. 14 6
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  4. 45 20
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  5. 8 5
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  6. 1 4
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx
  7. 24 8
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  8. 222 0
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  9. 37 17
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  10. 0 3
      src/app/api/gift/add/route.ts
  11. 2 5
      src/app/api/gift/lists/route.ts
  12. 0 3
      src/app/api/gift/my-gift-cards/route.ts
  13. 6 9
      src/app/api/graphql/route.ts
  14. 5 0
      src/app/globals.css
  15. 4 3
      src/components/cart/CartModal.tsx
  16. 473 0
      src/components/common/LoginModal/LoginModal.tsx
  17. 4 4
      src/components/common/button/ReviewButton.tsx
  18. 14 13
      src/components/customer/LoginForm.tsx
  19. 9 10
      src/components/customer/credentials/CredentialModal.tsx
  20. 18 9
      src/components/home/HomeImageBanner.tsx
  21. 59 18
      src/components/theme/ui/CommonModal.tsx
  22. 4 2
      src/components/theme/ui/InputText.tsx
  23. 74 0
      src/components/theme/ui/PasswordInput.tsx
  24. 6 6
      src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx
  25. 1 0
      src/graphql/cart/mutations/AddProductToCart.ts
  26. 70 0
      src/graphql/cart/mutations/CreateApplyCoupon.ts
  27. 1 0
      src/graphql/cart/mutations/CreateMergeCart.ts
  28. 1 0
      src/graphql/cart/mutations/GetCartItem.ts
  29. 1 0
      src/graphql/cart/mutations/RemoveCartItem.ts
  30. 1 0
      src/graphql/cart/mutations/UpdateCartItems.ts
  31. 2 1
      src/graphql/cart/mutations/index.ts
  32. 1 0
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  33. 3 2
      src/graphql/customer/mutations/CustomerRegistration.ts
  34. 1 0
      src/graphql/index.ts
  35. 2 19
      src/lib/Airwallex/airwallexInit.ts
  36. 62 76
      src/lib/Airwallex/useAirwallexCard.ts
  37. 1 32
      src/lib/ApolloClientBrowser.ts
  38. 1 26
      src/lib/restApiClient.ts
  39. 4 1
      src/providers/GlobalProviders.tsx
  40. 103 0
      src/providers/LoginModalProvider.tsx
  41. 0 2
      src/providers/SessionManager.tsx
  42. 0 21
      src/providers/SessionSync.tsx
  43. 0 1
      src/providers/index.ts
  44. 0 30
      src/store/slices/user-slice.ts
  45. 0 2
      src/store/store.ts
  46. 1 0
      src/types/cart/type.ts
  47. 15 1
      src/types/customer/type.ts
  48. 13 9
      src/types/types.ts
  49. 5 1
      src/utils/auth.ts
  50. 56 49
      src/utils/bagisto/index.ts
  51. 1 0
      src/utils/cartDetailTools.ts
  52. 10 0
      src/utils/constants.ts
  53. 33 8
      src/utils/cookie-tools.ts
  54. 0 13
      src/utils/getCartToken.ts
  55. 14 17
      src/utils/hooks/useAddToCart.ts
  56. 10 13
      src/utils/hooks/useCartDetail.ts
  57. 14 47
      src/utils/hooks/useGuestCartToken.ts
  58. 45 0
      src/utils/hooks/useUserRegister.ts
  59. 0 42
      src/utils/jwt-cookie.ts

+ 2 - 0
README.md

@@ -256,6 +256,7 @@ apollo client 中的错误分类:
 
 next-auth只负责登录和校验用户
 
+关于next-auth: https://juejin.cn/post/7532344864815022115
 
 
 ## 关于cookie的携带
@@ -348,6 +349,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
+46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054

+ 54 - 0
src/actions/mergeCartAction.ts

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

+ 14 - 6
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -39,6 +39,7 @@ import CommonModal from "@/components/theme/ui/CommonModal";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
 import CheckoutGiftCardGate from "./GiftCard/CheckoutGiftCardGate";
 import PromotionsDetails from "./PromotionsDetails";
+import CheckoutVipPlus from "./VipPlus/CheckoutVipPlus";
 
 /***
  * 不用useForShipping字段了
@@ -566,7 +567,7 @@ export default function CheckoutWrapper({
             return resultData;
     }
 
-    const onChangeGiftcard = async (paylod: CartDetail) => {
+    const onCartDataChange = async (paylod: CartDetail) => {
         dispatchCartStore('update',paylod);
         setCartData({...paylod});
         // @todo 还要请求支付方式和运输方式
@@ -663,12 +664,19 @@ export default function CheckoutWrapper({
                     onShipMethodChange={handleShippingMethodChange}  
                 />
             </div>
-
+            {!cartData.isGuest &&
+                <div className="mt-6 box-border px-4">
+                    <CheckoutGiftCardGate 
+                        giftcardNumber={cartData.giftcardNumber || ''}
+                        formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
+                        onChangeGiftcard={onCartDataChange}
+                    />
+                </div>
+            }
             <div className="mt-6 box-border px-4">
-                <CheckoutGiftCardGate 
-                    giftcardNumber={cartData.giftcardNumber || ''}
-                    formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
-                    onChangeGiftcard={onChangeGiftcard}
+                <CheckoutVipPlus 
+                    cartData={cartData} 
+                    onSwitchVip={onCartDataChange}
                 />
             </div>
             

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

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

+ 8 - 5
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -41,9 +41,12 @@ export function PaymentMethodContinueTpPay({
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
     const {loadSdk} = usePaymentSDKContext();
-    const {store} = useConfig();
+    const {store,currencies} = useConfig();
     const currentCurrencyCode  = store.currentCurrency;
 
+    const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
+    const orderCurrencyCode = orderCurrency?.code || currentCurrencyCode;
+
     const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
         cardNumber: 'airwallex_cardNumber', 
         cvc: 'airwallex_cvc', 
@@ -223,13 +226,13 @@ export function PaymentMethodContinueTpPay({
                 const afterpayToken = res.data.gatewayOrderId;
                 const webOrderId = res.data.orderId;
                 let countryCode = "US";
-                if(currentCurrencyCode === "CAD") {
+                if(orderCurrencyCode === "CAD") {
                     countryCode = "CA";
-                } else if(currentCurrencyCode === "AUD") {
+                } else if(orderCurrencyCode === "AUD") {
                     countryCode = "AU";
-                } else if(currentCurrencyCode === "NZD") {
+                } else if(orderCurrencyCode === "NZD") {
                     countryCode = "NZ";
-                } else if(currentCurrencyCode === "GBP") {
+                } else if(orderCurrencyCode === "GBP") {
                     countryCode = "GB";
                 }
                 window.AfterPay.initialize({countryCode: countryCode});

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

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

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

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

File diff ditekan karena terlalu besar
+ 222 - 0
src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx


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

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

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

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

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

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

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

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

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

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

+ 5 - 0
src/app/globals.css

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

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

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

File diff ditekan karena terlalu besar
+ 473 - 0
src/components/common/LoginModal/LoginModal.tsx


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -45,6 +45,7 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
         discountAmount
         giftcardAmount
         vipPlusAmount
+        vipExpireDate
         taxAmount
         taxTotal
         shippingAmount

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

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

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

@@ -50,6 +50,7 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData>
                 discountAmount
                 giftcardAmount
                 vipPlusAmount
+                vipExpireDate
                 taxAmount
                 taxTotal
                 shippingAmount

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

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

+ 1 - 0
src/graphql/index.ts

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

+ 2 - 19
src/lib/Airwallex/airwallexInit.ts

@@ -47,15 +47,6 @@ class AirwallexManager {
   private elements: AirwallexCardElements = {};
   private createPromise: Promise<AirwallexCardElements> | null = null;
 
-  private listeners = new Map<ElementType, Map<Payment.SplitElementEventCode, boolean>>();
-
-  // private created = false;
-
-  // async init() {
-  //   if (this.created) return;
-  //   this.created = true;
-  // }
-
   async createElements() {
 
     if (this.elements.cardNumber) {
@@ -110,18 +101,8 @@ class AirwallexManager {
   }
 
   on<EventCode extends Payment.SplitElementEventCode>(el: ElementType, event: EventCode, handler: Payment.SplitElementEventHandler<EventCode>) {
-    let eventMap = this.listeners.get(el);
-    
-    if (!eventMap) {
-      eventMap = new Map<Payment.SplitElementEventCode, boolean>([[event, false]]);
-      this.listeners.set(el, eventMap);
-    }
-    if(eventMap.get(event)) return; // 注册过就返回
 
     el.on(event, handler);
-
-    eventMap.set(event,true);
-    
   }
 
 
@@ -136,6 +117,8 @@ class AirwallexManager {
     });
 
     this.elements = {};
+    this.createPromise=null;
+
   }
 
   getElements() {

+ 62 - 76
src/lib/Airwallex/useAirwallexCard.ts

@@ -22,47 +22,47 @@ function airwallexCardReducer(state: AirwallexCardStateType, action: Partial<Air
     return {...newState};
 }
 
+const defaultAirwallexCardState: AirwallexCardStateType = {
+    cardNumberReady: false,
+    cvcReady: false,
+    expiryReady: false,
 
+    cardNumberValid: false,
+    cvcValid: false,
+    expiryValid: false,
+
+    cardNumberErrorMsg: "",
+    cvcErrorMsg: "",
+    expiryErrorMsg: "",
+};
 
 export function useAirwallexCard(
   enabled: boolean, 
   mountId: { cardNumber: string; cvc: string; expiry: string; },
   // callback: EventCallback
 ) {
-
-  const defaultAirwallexCardState: AirwallexCardStateType = {
-      cardNumberReady: false,
-      cvcReady: false,
-      expiryReady: false,
-
-      cardNumberValid: false,
-      cvcValid: false,
-      expiryValid: false,
-
-      cardNumberErrorMsg: "",
-      cvcErrorMsg: "",
-      expiryErrorMsg: "",
-  };
-  const [airwallexCardState, airwallexCardDispatch] = useReducer(airwallexCardReducer, defaultAirwallexCardState);
-  const readyAirwallexElement = useEffectEvent((event: any): void => {
-      console.log('----------------------------onReady---------------------');
-      const { type } = event.detail;
-      if (type === 'cardNumber') {
-          airwallexCardDispatch({
-              cardNumberReady: true
-          });
-      }
-      if (type === 'cvc') {
-          airwallexCardDispatch({
-              cvcReady: true
-          });
-      }
-      if (type === 'expiry') {
-          airwallexCardDispatch({
-              expiryReady: true
-          });
-      }
-  });
+  
+    
+    const [airwallexCardState, airwallexCardDispatch] = useReducer(airwallexCardReducer, defaultAirwallexCardState);
+    const readyAirwallexElement = useEffectEvent((event: any): void => {
+        console.log('----------------------------onReady---------------------');
+        const { type } = event.detail;
+        if (type === 'cardNumber') {
+            airwallexCardDispatch({
+                cardNumberReady: true
+            });
+        }
+        if (type === 'cvc') {
+            airwallexCardDispatch({
+                cvcReady: true
+            });
+        }
+        if (type === 'expiry') {
+            airwallexCardDispatch({
+                expiryReady: true
+            });
+        }
+    });
 
 
     const changeAirwallexElement = useEffectEvent((event: any) => {
@@ -143,34 +143,37 @@ export function useAirwallexCard(
   }>({});
 
   useEffect(() => {
+    console.log('enabled ------- ',enabled);
     if (!enabled) return;
 
     let destroyed = false;
 
     const init = async () => {
-      await airwallexInit();
-    //   await airwallexManager.init();
-
-      const els = await airwallexManager.createElements();
-      elementRef.current = els;
-console.log('useAirwallexCard +++++++++++++++++++++++++++++',destroyed);
-      if (destroyed) return;
-
-      airwallexManager.mount({
-        cardNumber: mountId.cardNumber,
-        cvc: mountId.cvc,
-        expiry: mountId.expiry
-      });
-
-      mountedRef.current = true;
-
-      // register events ONLY ONCE
-      bindEvents(els,{
-        focusCallback: focusAirwallexElement,
-        blurCallback: blurAirwallexElement, 
-        changeCallback: changeAirwallexElement, 
-        readyCallback: readyAirwallexElement
-      });
+        await airwallexInit();
+        //   await airwallexManager.init();
+
+        const els = await airwallexManager.createElements();
+        elementRef.current = els;
+            console.log('useAirwallexCard +++++++++++++++++++++++++++++',destroyed);
+        if (destroyed) return;
+        
+
+        airwallexManager.mount({
+            cardNumber: mountId.cardNumber,
+            cvc: mountId.cvc,
+            expiry: mountId.expiry
+        });
+
+        // register events ONLY ONCE
+        bindEvents(els,{
+            focusCallback: focusAirwallexElement,
+            blurCallback: blurAirwallexElement, 
+            changeCallback: changeAirwallexElement, 
+            readyCallback: readyAirwallexElement
+        });
+        mountedRef.current = true;
+
+      
     };
 
     init();
@@ -179,11 +182,11 @@ console.log('useAirwallexCard +++++++++++++++++++++++++++++',destroyed);
         console.log('useAirwallexCard ___________________________________',mountedRef.current)
       destroyed = true;
       if (mountedRef.current) {
-        airwallexManager.unmount();// 解除挂载
+        airwallexManager.destroy();// 解除挂载
         mountedRef.current = false;
       }
     };
-  }, [enabled]);
+  }, [enabled,mountId.cardNumber,mountId.cvc,mountId.expiry]);
 
 
 
@@ -212,21 +215,4 @@ function bindEvents(els: any,callback:EventCallback) {
   airwallexManager.on(expiry,"blur", blurCallback);
   airwallexManager.on(expiry,"change", changeCallback);
   airwallexManager.on(expiry,"ready", readyCallback);
-
-  // ⚠️ 关键:绑定只发生一次(manager内部防重复)
-//   cardNumber.on("focus", focusCallback);
-//   cardNumber.on("blur", blurCallback);
-//   cardNumber.on("change", changeCallback);
-//   cardNumber.on("ready", readyCallback);
-
-
-//   cvc.on("focus", focusCallback);
-//   cvc.on("blur", blurCallback);
-//   cvc.on("change", changeCallback);
-//   cvc.on("ready", readyCallback);
-
-//   expiry.on("focus", focusCallback);
-//   expiry.on("blur", blurCallback);
-//   expiry.on("change", changeCallback);
-//   expiry.on("ready", readyCallback);
 }

+ 1 - 32
src/lib/ApolloClientBrowser.ts

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

+ 1 - 26
src/lib/restApiClient.ts

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

+ 4 - 1
src/providers/GlobalProviders.tsx

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

+ 103 - 0
src/providers/LoginModalProvider.tsx

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

+ 0 - 2
src/providers/SessionManager.tsx

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

+ 0 - 21
src/providers/SessionSync.tsx

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

+ 0 - 1
src/providers/index.ts

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

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

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

+ 0 - 2
src/store/store.ts

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

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

@@ -54,6 +54,7 @@ export interface CartDetail {
   // baseDiscountAmount: number;
   giftcardAmount: number;
   vipPlusAmount: number;
+  vipExpireDate: string;
   taxAmount: number;
   taxTotal: number;
   shippingAmount: number;

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

@@ -8,4 +8,18 @@ export type {
     AddressListInOrderDetails,
     AddressInOrderDetails,
     OrderAddressType
-} from "./order";
+} from "./order";
+
+export interface CustomerLoginData{
+    id: string;
+    apiToken: string;
+    token: string;
+    message: string;
+    success: boolean;
+}
+export interface CustomerLoginResponse{
+    createCustomerLogin:{
+        customerLogin: CustomerLoginData
+    }
+    
+}

+ 13 - 9
src/types/types.ts

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

+ 5 - 1
src/utils/auth.ts

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

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

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

+ 1 - 0
src/utils/cartDetailTools.ts

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

+ 10 - 0
src/utils/constants.ts

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

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

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

+ 0 - 13
src/utils/getCartToken.ts

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

+ 14 - 17
src/utils/hooks/useAddToCart.ts

@@ -4,9 +4,12 @@ import { useCustomToast } from "./useToast";
 import { useAppDispatch } from "@/store/hooks";
 import { addItem, clearCart } from "@/store/slices/cart-slice";
 import { isObject } from "@utils/type-guards";
-import { getCartToken } from "@utils/getCartToken";
-import { getCookie, setCookie, deleteCookie } from "@utils/cookie-tools";
-import { encodeJWT } from "@/utils/jwt-cookie";
+import { 
+  setCookie, 
+  getIsGuest,
+  getGuestCartToken,
+  deleteGuestCookie
+} from "@utils/cookie-tools";
 import { useGuestCartToken } from "./useGuestCartToken";
 import { IS_GUEST,GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
 import { useMutation } from "@apollo/client/react";
@@ -39,18 +42,12 @@ export const useAddProduct = () => {
           if (responseData.success) {
             /** 兜底代码 start*/
             // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
-            const isGuest = getCookie(IS_GUEST) !== "false";// 'true' 是游客
+            const isGuest = getIsGuest();// true 是游客
             // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
             const responseCartId = responseData.id;
             const responseCartToken = responseData.cartToken;
             if(isGuest && responseCartId !== responseCartToken) {
-                const newToken = encodeJWT({
-                  sessionToken: responseCartToken,
-                  cartId: responseCartId,
-                  isGuest: isGuest,
-                });
-        
-                setCookie(GUEST_CART_TOKEN, newToken,{encode: false});
+                setCookie(GUEST_CART_TOKEN, responseCartToken,{encode: false});
                 setCookie(GUEST_CART_ID, responseCartId);
                 setCookie(IS_GUEST, String(isGuest));
             }
@@ -84,9 +81,10 @@ export const useAddProduct = () => {
   }) => {
     
     // Ensure token exists - create if needed
-    let token = getCartToken(); // 从cookie获取token
+    let token = getGuestCartToken(); // 从cookie获取token
+    const isGuest = getIsGuest();
 
-    if (!token) {
+    if (!token && isGuest) {
       // 没有token,则创建一个并写入cookie
       token = await createGuestToken();
 
@@ -129,10 +127,9 @@ export const useAddProduct = () => {
           if (!responseData?.itemsQty) {
             dispatch(clearCart());
 
-            const isGuest = getCookie(IS_GUEST);
-            if (isGuest === "true") {
-              deleteCookie(GUEST_CART_TOKEN);
-              deleteCookie(GUEST_CART_ID);
+            const isGuest = getIsGuest();
+            if (isGuest) {
+              deleteGuestCookie();
             }
           }
         } else {

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

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

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

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

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

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

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

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