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

结账页支付流程 --- paypal

fogwind преди 1 седмица
родител
ревизия
4c8f985a47
променени са 76 файла, в които са добавени 3219 реда и са изтрити 542 реда
  1. 56 4
      README.md
  2. 19 19
      src/app/(checkout)/checkout/_components/AddressResultDisplay.tsx
  3. 8 8
      src/app/(checkout)/checkout/_components/BillingAddressCheckout.tsx
  4. 46 3
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  5. 236 52
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  6. 190 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  7. 141 0
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  8. 121 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  9. 1 1
      src/app/(checkout)/checkout/_components/LoadingPaymentMethod.tsx
  10. 292 0
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx
  11. 48 14
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  12. 17 0
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  13. 6 6
      src/app/(checkout)/checkout/_components/ShippingAddressCheckout.tsx
  14. 89 122
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  15. 90 0
      src/app/(checkout)/checkout/continuetopay/page.tsx
  16. 10 20
      src/app/(checkout)/checkout/page.tsx
  17. 2 2
      src/app/(checkout)/layout.tsx
  18. 1 1
      src/app/(public)/product/_components/ProductAddToCart.tsx
  19. 2 2
      src/app/(public)/product/_components/ProductInformation.tsx
  20. 206 0
      src/app/(public)/success/_components/OrderDetailWrapper.tsx
  21. 124 0
      src/app/(public)/success/_components/SuccessPageSkeleton.tsx
  22. 32 16
      src/app/(public)/success/page.tsx
  23. 14 6
      src/app/api/graphql/route.ts
  24. 35 6
      src/app/layout.tsx
  25. 18 58
      src/components/cart/CartModal.tsx
  26. 1 1
      src/components/checkout/index.tsx
  27. 10 10
      src/components/checkout/stepper/GuestAddAdressForm.tsx
  28. 1 1
      src/components/common/AddToCartModal/FooterBtnInAddToCartModal.tsx
  29. 67 0
      src/components/common/ConfirmModal.tsx
  30. 74 0
      src/components/common/CountDown.tsx
  31. 1 1
      src/components/layout/navbar/MobileMenuTrigger.tsx
  32. 1 1
      src/components/layout/navbar/index.tsx
  33. 7 5
      src/components/theme/ui/CommonModal.tsx
  34. 0 3
      src/components/theme/ui/InputText.tsx
  35. 0 3
      src/components/theme/ui/Select.tsx
  36. 22 20
      src/components/theme/ui/kernel/confirm/host.tsx
  37. 2 0
      src/graphql/cart/mutations/AddProductToCart.ts
  38. 2 0
      src/graphql/cart/mutations/CreateMergeCart.ts
  39. 2 0
      src/graphql/cart/mutations/GetCartItem.ts
  40. 2 0
      src/graphql/cart/mutations/RemoveCartItem.ts
  41. 2 0
      src/graphql/cart/mutations/UpdateCartItems.ts
  42. 22 0
      src/graphql/checkout/mutations/CreateCancelOrder.ts
  43. 31 3
      src/graphql/checkout/mutations/CreateCheckoutOrder.ts
  44. 70 0
      src/graphql/checkout/mutations/CreatePaymentCallback.ts
  45. 25 0
      src/graphql/checkout/mutations/CreatePaymentReplay.ts
  46. 67 0
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  47. 6 2
      src/graphql/checkout/mutations/index.ts
  48. 19 0
      src/graphql/currency/query/GetCurrencies.ts
  49. 3 0
      src/graphql/currency/query/index.ts
  50. 90 0
      src/graphql/customer/query/GetOrderDetails.ts
  51. 3 0
      src/graphql/customer/query/index.ts
  52. 2 0
      src/graphql/index.ts
  53. 28 0
      src/lib/ApolloErrorHandler.ts
  54. 58 8
      src/lib/graphql-fetch.ts
  55. 0 3
      src/providers/GlobalProviders.tsx
  56. 21 3
      src/providers/ReduxProvider.tsx
  57. 14 6
      src/store/hooks.ts
  58. 54 56
      src/store/slices/cart-slice.ts
  59. 76 0
      src/store/slices/currency-slice.ts
  60. 33 10
      src/store/store.ts
  61. 15 0
      src/types/cart/type.ts
  62. 120 0
      src/types/checkout/type.ts
  63. 15 0
      src/types/currency/type.ts
  64. 119 0
      src/types/customer/order.ts
  65. 3 0
      src/types/customer/type.ts
  66. 4 0
      src/types/graphqlFetch/type.ts
  67. 2 2
      src/utils/bagisto/index.ts
  68. 38 1
      src/utils/cartDetailTools.ts
  69. 8 10
      src/utils/hooks/getAddress.ts
  70. 2 1
      src/utils/hooks/useCache.ts
  71. 11 39
      src/utils/hooks/useCartDetail.ts
  72. 28 12
      src/utils/hooks/useCheckoutPaymentMethod.ts
  73. 50 1
      src/utils/hooks/useCheckoutShippingMethod.ts
  74. 31 0
      src/utils/hooks/useCurrencyList.ts
  75. 109 0
      src/utils/hooks/usePlaceOrder.ts
  76. 44 0
      src/utils/hooks/useSaveCheckoutCart.ts

+ 56 - 4
README.md

@@ -292,14 +292,40 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
   file: "/var/www/html/nshop/packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php"
   line: 913
 
-8. apollo client 升级后需要用新的方法设置返回数据的类型
+8. apollo client 升级后需要用新的方法设置返回数据的类型 -- 已完成
 9. Apollo client的错误处理机制 (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
 10. 产品详情页代码优化 --- 完成
-11. 代码eslint检查修改
-12. 下单成功后需要重新创建购物车token
+11. 代码eslint检查修改 -- 已完成
+12. 下单成功后需要重新创建购物车token -- 无需处理
 13. 详情页缓存策略(不要缓存吧)
 14. getSession  过期提示重新登录
-15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined
+15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined -- 已解决
+16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux
+17. redux 中 cartDetail 异步获取数据 https://chat.deepseek.com/share/76kj30h2cws9jcl4jt  -- 已完成
+18. 需要一个遮挡整个视口的loading组件  -- 已完成
+19. 结账页地址模块还差两个功能:登录用户从地址列表选择地址;未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;
+20. 登录用户结账页支付失败后要跳转到支付失败页面提醒用户可以再次支付
+21. 游客结账页支付失败或者取消支付,先调paymentCallback 再调cancelOrder。但是不删除cookie里之前的cartToken --- 完成
+22. 顶部header 栏 优化:可设置显示标题还是通用
+23. 个人中心取的是localStorage里的email,即使cookie里的token删除了,localStorage里的email会一直存在, 需要废弃localStorage里的email;
+24. 删除老的checkout页面的组件
+25. resetGuestToken 有问题,改为退出登录后不调resetGuestToken。
+26. useAddProduct优化
+27. 下单成功页客服工具,分享
+28. cancelOrder接口报错   --  已解决
+29. 购物车数据管理方案 -- 已完成
+30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理;
+31. 当前货币等采用redux管理;
+
+## 关于购物车数据管理方案
+ssr + redux:
+第一次渲染时在服务端获取数据存入redux,后续在客户端操作时更新redux。
+购物车页和结账页不从redux中取数据,在服务端重新获取。
+但是在购物车页和结账页对购物车数据做的修改需要同步到redux。
+
+## 关于结账页地址
+如果用户在结账页新建一个地址,这个地址的type是 cart_billing/cart_shipping。
+用户点击支付按钮创建订单,订单创建成功后,根据购物车里的地址信息创建新的billing、shipping地址,地址type是order_billing/order_shipping。
 
 ## GetCartItem,CreateMergeCart 接口报500 (已解决)
 需要前端处理错误,不能直接报错。
@@ -312,3 +338,29 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 现在理一下逻辑: 用户登录时会返回一个token,这个token只是后端校验用户信息的token,并不是cartToken。
 也就是说虽然用户登录了,也创建了cartToken,但是购物车没激活,所以GetCartItem接口会报500。只有当用户进行加购操作的时候才会激活购物车。
+
+## 关于paypal
+
+文档: https://docs.paypal.ai/reference/sdk/react
+
+
+## 关于hook
+hook 的定义:
+
+Hook 是 React 在 render 过程中注册状态依赖的函数调用点,而不是函数命名或形式。
+不是“所有函数都是 hook”,而是“只有参与 React render 状态系统的函数才是 hook”。
+
+真正的hook 必须满足下面的条件:
+1. 以 use 开头(约定,不是本质)
+2. 在 render flow 中执行
+3. 调用 React 内部 Hook API
+4. 受 Hook 规则约束(顺序调用)
+
+> 要清楚hook的定义,不然遇到hook被叫烂的情况,你会很迷惑。
+
+## 关于 server actions
+1. https://juejin.cn/post/7540970969751617546
+2. https://developer.aliyun.com/article/1688619
+3. https://zh-hans.react.dev/reference/rsc/server-functions
+
+> useFormState 已弃用,重新命名为 useActionState 

+ 19 - 19
src/app/(checkout)/checkout/_components/AddressResultDisplay.tsx

@@ -1,18 +1,18 @@
 "use client";
 
-import { ShipAddressFormData } from "@/types/checkout/type";
+import {CartAddress} from "@/types/cart/type";
 
-function isAddressAvailable(address: ShipAddressFormData | null): boolean {
+function isAddressAvailable(address: CartAddress | null): boolean {
     if(address && 
-        address.shippingEmail && 
-        address.shippingFirstName && 
-        address.shippingLastName && 
-        address.shippingPhoneNumber && 
-        address.shippingAddress && 
-        address.shippingCountry && 
-        address.shippingState && 
-        address.shippingCity && 
-        address.shippingPostcode
+        address.email && 
+        address.firstName && 
+        address.lastName && 
+        address.phone && 
+        address.address && 
+        address.country && 
+        address.state && 
+        address.city && 
+        address.postcode
     ) {
         return true;
     }
@@ -23,7 +23,7 @@ export default function AddressResultDisplay({
     shippingAddress,
     onAddressModalOpenClick,
 } : {
-    shippingAddress: ShipAddressFormData | null;
+    shippingAddress: CartAddress | null;
     onAddressModalOpenClick: () => void;
 }) {
     const addressIsAvailable: boolean = isAddressAvailable(shippingAddress);
@@ -48,9 +48,9 @@ export default function AddressResultDisplay({
             >
                 <div className="border-b-1 flex h-11.25 items-center justify-between">
                     <p className="text-ly-13 font-medium">
-                        {shippingAddress.shippingFirstName} {shippingAddress.shippingLastName}
+                        {shippingAddress.firstName} {shippingAddress.lastName}
                         <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
-                        {shippingAddress.shippingPhoneNumber}
+                        {shippingAddress.phone}
                     </p>
                     <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="0" y="0" width="16" height="16"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M6.32031 4.27051L10.4202 8.37036L6.32031 12.4702"></path></svg>
                 </div>
@@ -61,11 +61,11 @@ export default function AddressResultDisplay({
                         <path d="M11.9983 11.3653C13.1666 11.3653 14.1138 10.4181 14.1138 9.24979C14.1138 8.08144 13.1666 7.13428 11.9983 7.13428C10.83 7.13428 9.88281 8.08144 9.88281 9.24979C9.88281 10.4181 10.83 11.3653 11.9983 11.3653Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
                     </svg>
                     <span className="text-ly-12 ml-2.5">
-                        {shippingAddress.shippingAddress}, 
-                        {shippingAddress.shippingCity}, 
-                        {shippingAddress.shippingState}, 
-                        {shippingAddress.shippingPostcode}, 
-                        {shippingAddress.shippingCountry}
+                        {shippingAddress.address}, 
+                        {shippingAddress.city}, 
+                        {shippingAddress.state}, 
+                        {shippingAddress.postcode}, 
+                        {shippingAddress.country}
                     </span>
                 </div>
             </div>       

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

@@ -151,8 +151,8 @@ export default function BillingAddressCheckout ({
         <section className="w-full mt-4"> 
             <div className="box-border w-full p-3 bg-ly-lightgray">
                 <div className="flex justify-between items-center">
-                    <span className="text-ly-12 flex-initial">Billing Address</span>
-                    <label className="group relative w-9 h-5.5 flex-initial">
+                    <span className="text-ly-12 flex-none">Billing Address</span>
+                    <label className="group relative w-9 h-5.5 flex-none">
                         <input className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
                             type="checkbox"
                             {...register("billingSameAsShipping")}
@@ -192,7 +192,7 @@ export default function BillingAddressCheckout ({
                         Name *
                     </label>
                     <div className="w-full flex justify-between">
-                        <div className="w-41.25 flex-initial">
+                        <div className="w-41.25 flex-none">
                             <InputText
                                 type="text"
                                 placeholder="First name"
@@ -206,7 +206,7 @@ export default function BillingAddressCheckout ({
                                 error={errors.billingFirstName && errors.billingFirstName.message as string}
                             />
                         </div> 
-                        <div className="w-41.25 flex-initial">
+                        <div className="w-41.25 flex-none">
                             <InputText
                                 type="text"
                                 placeholder="Last name"
@@ -240,7 +240,7 @@ export default function BillingAddressCheckout ({
                     <label className="text-ly-12 block mb-4 font-semibold">Country and State/Province  *</label>
                     <div className="w-full">
                         <div className="w-full flex justify-between">
-                            <div className="w-41.25 flex-initial">
+                            <div className="w-41.25 flex-none">
                                 <Select placeholder="Country/Region"
                                     {...register("billingCountry", {
                                         required: "Country/Region field is required",
@@ -259,7 +259,7 @@ export default function BillingAddressCheckout ({
                                     error={errors.billingCountry && errors.billingCountry.message as string}
                                 /> 
                             </div>
-                            <div className="w-41.25 flex-initial relative">
+                            <div className="w-41.25 flex-none relative">
                                 {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
                                 {statesOptions.length > 0 ?
                                 (
@@ -286,7 +286,7 @@ export default function BillingAddressCheckout ({
                             
                         </div>
                         <div className="w-full flex justify-between mt-4">
-                            <div className="w-41.25 flex-initial">
+                            <div className="w-41.25 flex-none">
                                 <InputText
                                     type="text"
                                     placeholder="Zip/Postal Code"
@@ -300,7 +300,7 @@ export default function BillingAddressCheckout ({
                                     error={errors.billingPostcode&& errors.billingPostcode.message as string}
                                 />
                             </div>
-                            <div className="w-41.25 flex-initial">
+                            <div className="w-41.25 flex-none">
                                 <InputText
                                     type="text"
                                     placeholder="City"

+ 46 - 3
src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx

@@ -1,9 +1,52 @@
-export default function CheckoutPlaceOrder() { 
+"use client";
+
+import { useState } from "react";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import PaypalButton from "./PaymentButton/PaypalButton";
+
+export default function CheckoutPlaceOrder({
+    paymentMethod,
+    clickPlaceOrder, // 校验运输方式,地址 
+}: {
+    paymentMethod: string;
+    clickPlaceOrder: () => Promise<boolean>;
+}) { 
+    const { createOrder } = usePlaceOrder(); 
+    
+    const handleCreateOrder = async () => {
+        const res = await createOrder();
+        console.log("createOrder res ---- ", res);
+        if(!res.error && res.data) {
+            return {
+                error: res.error,
+                msg: '',
+                orderId: res.data.gatewayOrderId,
+                webOrderId: res.data.orderId
+            };
+        } else {
+            return {
+                error: res.error,
+                msg: res.msg,
+                orderId: res.data?.gatewayOrderId ?? '',
+                webOrderId: res.data?.orderId ?? ''
+            };
+        }
+    };
+
     return (
         <>
-            <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">
+            <div className="relative w-full">
+                {paymentMethod === 'paypal_smart_button' && 
+                    <PaypalButton
+                        createOrder={handleCreateOrder}
+                        onPaymentClick={clickPlaceOrder}
+                    />
+                }
+            </div>
+           
+            {/* <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">
                 Place Order
-            </button>
+            </button> */}
         </>
     );
 }

+ 236 - 52
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState, useEffect, useRef, Suspense} from "react";
+import {useState, useEffect, useRef} from "react";
 
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
@@ -11,30 +11,34 @@ import {
     ShipAddressFormData,
     BillAddressFormData,
     FullAddressFormData,
-    CreateCheckoutAddressVariables
+    CreateCheckoutAddressVariables,
 } from "@/types/checkout/type";
 import AddressResultDisplay from "./AddressResultDisplay";
 import AddressResultDisplayLoading from "./AddressResultDisplayLoading";
-import CheckoutAddressModal from "./CheckoutAddressModal";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
 import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
 import {useCheckoutPaymentMethod} from  "@/utils/hooks/useCheckoutPaymentMethod";
-import { useAppSelector,useAppDispatch } from "@/store/hooks";
+import {useCheckoutShippingMethod} from  "@/utils/hooks/useCheckoutShippingMethod";
+import { useAppDispatch,useAppStore  } from "@/store/hooks";
 import { updateCart } from "@/store/slices/cart-slice";
 import { useCartDetail } from "@utils/hooks/useCartDetail";
-import PaymentMethodCheckout from "./PaymentMethodCheckout";
+import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
 import LoadingPaymentMethod from "./LoadingPaymentMethod";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
-
+import CommonModal from "@/components/theme/ui/CommonModal";
+import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
+import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { CartDetail } from "@/types/cart/type";
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
- * @todo 保存完地址之后要重新获取运输方式和支付方式
- * @todo 变更运输方式后重新获取支付方式
- * @todo redux本地存储购物车详情优化(slice定义修改;useCartDetail自定义hook修改;最后结账也统一优化购物车详情获取)
+ * 保存完地址之后要重新获取运输方式和支付方式
+ * 变更运输方式后重新获取支付方式
+ * @todo 购物车为空 跳转到空购物车页面
  */
 
 /**
@@ -94,41 +98,78 @@ function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateC
 
 export default function CheckoutWrapper({
     loginEmail,
-    countries
+    countries,
 }: {
     loginEmail: string;
-    countries: Country[]
+    countries: Country[],
 }) {
+
     const dispatch = useAppDispatch();
-    const { getCartDetail } = useCartDetail();
+    const { cartData } = useCartDetail();
+    const {saveCheckoutCart} = useSaveCheckoutCart();
+    
+    const {
+        data: shippingMethodDatas,
+        loading: shippingMethodLoading,
+        error: shippingMethodError,
+        getShippingMethod,
+    } = useCheckoutShippingMethod();
+
+
+    const selectedPaymentMethod = cartData?.paymentMethod ?? 'paypal_smart_button';
    
     const { 
         data: paymentMethodDatas,
         loading: paymentMethodLoading,
         error: paymentMethodError,
         getPaymentMethod, 
-        savePaymentMethod 
     } = useCheckoutPaymentMethod();
 
     const { showToast } = useCustomToast();
     const { getCheckoutAddress, saveCheckoutAddress } = useCheckoutAddress(loginEmail);
 
     const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
+    const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
 
 
     // useForm() 只会在组件初始化时读取一次 defaultValues
     const addressForm = useForm<FullAddressFormData>({
         mode: "onBlur",          // 或 "onChange"
-        reValidateMode: "onChange", 
+        reValidateMode: "onChange",
+        defaultValues: {
+            "shippingAddressId": "",
+            "shippingEmail": loginEmail,
+            "shippingFirstName": "",
+            "shippingLastName": "jack",
+            "shippingCompanyName": "",
+            "shippingAddress": "",
+            "shippingCountry": "US",
+            "shippingState": "",
+            "shippingCity": "",
+            "shippingPostcode": "",
+            "shippingPhoneNumber": "",
+            "billingAddressId": "",
+            "billingEmail": "",
+            "billingFirstName": "",
+            "billingLastName": "",
+            "billingCompanyName": "",
+            "billingAddress": "",
+            "billingCountry": "US",
+            "billingState": "",
+            "billingCity": "",
+            "billingPostcode": "",
+            "billingPhoneNumber": "",
+            "billingSameAsShipping": true
+        } 
     });
 
     const [addressModalOpen, setAddressModalOpen] = useState(false);
-    const [loadingData, setLoadingData] = useState(true);
-    const [serverShippingAddress, setServerShippingAddress] = useState<ShipAddressFormData | null>(null);
+    const [loadingAddress, setLoadingAddress] = useState(true); // 初始加载地址数据时的loading状态
 
+    const serverShippingAddress = useRef<ShipAddressFormData | null>(null);
     const serverBillingAddress = useRef<BillAddressFormData | null>(null);
 
-    const [addressSaving, setAddressSaving] = useState(false);
+    const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
 
     // 开发环境会执行两次 https://zh-hans.react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
     useEffect(() => { 
@@ -137,14 +178,13 @@ export default function CheckoutWrapper({
                 ...shippingAddress,
                 ...billingAddress,
             });
-            setServerShippingAddress({
-                ...shippingAddress
-            });
+
+            serverShippingAddress.current = shippingAddress;
             serverBillingAddress.current = billingAddress;
          
-            setLoadingData(false);
+            setLoadingAddress(false);
         }, ()=> {
-            setLoadingData(false);
+            setLoadingAddress(false);
         });
     }, []);
 
@@ -152,9 +192,9 @@ export default function CheckoutWrapper({
     const openAddressModal = () => {
         setAddressModalOpen(true);
 
-        if(serverShippingAddress && serverBillingAddress.current) {
+        if(serverShippingAddress.current && serverBillingAddress.current) {
             addressForm.reset({
-                ...serverShippingAddress,
+                ...serverShippingAddress.current,
                 ...serverBillingAddress.current,
             });
         }
@@ -167,19 +207,20 @@ export default function CheckoutWrapper({
     };
     const addressFormOnSubmit = async (formData: FullAddressFormData) => { 
         // console.log('addressFormOnSubmit ---- ',formData); return;
-        // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
-        // 还需要获取运输方式
-
+        if(addressSaving) {
+            return;
+        }
         const saveAddressParam = generateSaveCheckoutAddressParam(formData);
         setAddressSaving(true);
         try {
 
             const res = await saveCheckoutAddress(saveAddressParam);
             console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
-            if(methodShiippingRef.current) {
-                methodShiippingRef.current.reloadSHippingMethods();
-            }
+            // 地址保存成功后重新获取运输方式和支付方式 
+            getShippingMethod();
+            getPaymentMethod();
             
+            // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
             const queryAddressRes = await getCheckoutAddress();
             if(queryAddressRes !== null) {
                 addressForm.resetField('shippingAddressId',{
@@ -188,10 +229,14 @@ export default function CheckoutWrapper({
                 addressForm.resetField('billingAddressId',{
                     defaultValue: queryAddressRes.billingAddress.billingAddressId
                 });
-                setServerShippingAddress({
-                    ...queryAddressRes.shippingAddress
-                });
+
+                serverShippingAddress.current = queryAddressRes.shippingAddress;
                 serverBillingAddress.current = queryAddressRes.billingAddress;
+                // 同步地址到购物车详情
+                dispatch(updateCart({
+                    billingAddress: billingAddressToCartAddress(queryAddressRes.billingAddress),
+                    shippingAddress: shippingAddressToCartAddress(queryAddressRes.shippingAddress)
+                }));
 
             }
             setAddressModalOpen(false);
@@ -208,7 +253,27 @@ export default function CheckoutWrapper({
 
 
     const handlePaymentMethodChange =  async (method: string,title: string) => { 
-        
+        let shippingMethod = '-1';
+        if(methodShiippingRef.current) {
+            shippingMethod = methodShiippingRef.current.getSelectShipMethod();
+        }
+        overlayLoading.start();
+        const saveRes = await saveCheckoutCart({ 
+            shippingMethod: shippingMethod,
+            paymentMethod: method,
+            couponCode: '-1'
+        });
+        overlayLoading.stop();
+        if(!saveRes.error) {
+            if(saveRes.data) {
+                const newCartDetail = formatCartDetail(saveRes.data);
+                dispatch(updateCart(newCartDetail));
+            }
+        } else {
+            showToast(saveRes.msg, 'danger');
+        }
+
+        /*
         const res = await savePaymentMethod({ paymentMethod: method });
         console.log('savePaymentMethod res ====== ',res);
         if(!res.error) {
@@ -220,7 +285,95 @@ export default function CheckoutWrapper({
         } else {
             showToast(res.msg, 'danger');
         }
+        */
+    };
+    const handleShippingMethodChange =  async (method: string) => { 
+        overlayLoading.start();
+        const saveRes = await saveCheckoutCart({ 
+            shippingMethod: method,
+            paymentMethod: '-1',
+            couponCode: '-1'
+        });
+        overlayLoading.stop();
+        if(!saveRes.error) {
+            if(saveRes.data) {
+                const newCartDetail = formatCartDetail(saveRes.data);
+                dispatch(updateCart(newCartDetail));
+            }
+            // 保存成功之后需要重新获取支付方式
+            getPaymentMethod();
+        } else {
+            showToast(saveRes.msg, 'danger');
+        }
+        /*
+        const res = await saveShippingMethod({ shippingMethod: method });
+        if(!res.error) {
+            // 保存成功之后需要重新获取购物车详情和支付方式
+            getCartDetail();
+            getPaymentMethod();
+        } else {
+            showToast(res.msg, 'danger');
+        }*/
+    };
+
+    // 下单
+    // 1. 校验地址 运输方式,支付方式;
+    // 2. 根据购物车详情判断是否需要传下单参数: 地址,运输方式,支付方式
+    const handlePlaceOrder = async () => { 
+        // 校验地址
+        const addressValid = await addressForm.trigger();
+
+        if(!addressValid) {
+            showToast('Please fill in the address information correctly.', 'danger');
+            return false;
+        }
+        if(methodShiippingRef.current) {
+            const shippingMethodValid = await methodShiippingRef.current.validateShipMethod();
+            if(!shippingMethodValid) {
+                showToast('Please select a shipping method.', 'danger');
+                return false;
+            }
+        } else {
+            showToast('Please wait for shipping method loading.', 'danger');
+            return false;
+        }
+
+        if(methodPaymentRef.current) {
+            const paymentMethodValid = await methodPaymentRef.current.validatePaymentMethod();
+            if(!paymentMethodValid) {
+                showToast('Please select a payment method.', 'danger');
+                return false;
+            }
+        } else {
+            showToast('Please wait for payment method loading.', 'danger');
+            return false;
+        }
+        if(!cartData) {
+            showToast('Please add product to your cart.', 'danger');
+            return false;
+        }
+        
+        //如果购物车详情没有保存过支付方式,则需要保存支付方式
+        if(cartData?.paymentMethod === null) {
+            const pmethod = methodPaymentRef?.current.getSelectPaymentMethod();
+            const saveRes = await saveCheckoutCart({ 
+                shippingMethod: '-1',
+                paymentMethod: pmethod,
+                couponCode: '-1'
+            });
+            if(!saveRes.error) {
+                if(saveRes.data) {
+                    const newCartDetail = formatCartDetail(saveRes.data);
+                    dispatch(updateCart(newCartDetail));
+                }
+            }
+        }
+
+        return true;
     };
+    
+
+
     // function tt() {
     //     const arr = [
     //         ['US','3803800217'], 
@@ -244,37 +397,64 @@ export default function CheckoutWrapper({
         <section className="w-full">
             <div>
             {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
-                    {loadingData ? <AddressResultDisplayLoading /> 
-                    :<AddressResultDisplay shippingAddress={serverShippingAddress} 
-                        onAddressModalOpenClick={openAddressModal}
-                    />}
-                    <CheckoutAddressModal 
+                    {loadingAddress ? 
+                        <AddressResultDisplayLoading /> 
+                    :
+                        <AddressResultDisplay shippingAddress={cartData?.shippingAddress ?? null} 
+                            onAddressModalOpenClick={openAddressModal}
+                        />
+                    }
+                    <CommonModal
                         isOpen={addressModalOpen}
                         onClose={closeAddressModal}
+                        header={
+                            <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
+                                <span>Shipping Address</span>
+                                <button className="w-6 h-6" onClick={() => closeAddressModal(false)}>
+                                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
+                                </button>
+                            </div>
+                        }
                         body={
-                            <>
-                                <FormProvider {...addressForm}>
-                                    <form>
-                                        <ShippingAddressCheckout countries={countries} />
-                                        <BillingAddressCheckout countries={countries} />
-                                    </form>
-                                </FormProvider>
-                            </>
+                            <div className="box-border w-full h-full p-3.75">
+                                <p className="text-ly-12 leading-ly-20">
+                                    Select a billing address from your address book or enter a new address.
+                                </p>
+                                <div>
+                                    <FormProvider {...addressForm}>
+                                        <form>
+                                            <ShippingAddressCheckout countries={countries} />
+                                            <BillingAddressCheckout countries={countries} />
+                                        </form>
+                                    </FormProvider>
+                                </div>
+                            </div>
                         }
                         footer={
+                            <div className="box-border w-full p-3.75">
                             <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
                                 onClick={addressForm.handleSubmit(addressFormOnSubmit)}
                             >
                                 {addressSaving ? <LoadingSpinner /> : 'Save'}
                             </button>
+                            </div>
                         }
                     />
             </div>
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium">Shipping Method</h3>
-                {/* <Suspense fallback={<CheckoutShippingMethodLoading />}> */}
-                    <ShippingMethodCheckout ref={methodShiippingRef}  />
-                {/* </Suspense> */}
+                {shippingMethodLoading ?
+                
+                    <CheckoutShippingMethodLoading /> 
+                :
+                    <ShippingMethodCheckout 
+                        ref={methodShiippingRef}  
+                        shipMethods={shippingMethodDatas}
+                        errorMsg={shippingMethodError}  
+                        onShipMethodChange={handleShippingMethodChange}  
+                    />
+                }
+
             </div>
 
             <div className="mt-6 box-border px-4">
@@ -283,6 +463,7 @@ export default function CheckoutWrapper({
                     <LoadingPaymentMethod />
                 :   
                     <PaymentMethodCheckout 
+                        ref={methodPaymentRef}
                         errorMsg={paymentMethodError}
                         paymentMethods={paymentMethodDatas} 
                         onPaymentMethodChange={handlePaymentMethodChange}
@@ -293,10 +474,13 @@ export default function CheckoutWrapper({
             <div className="mt-3 box-border px-4">
                 <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
                 <div className="mt-6 w-full">
-                    <CheckoutPlaceOrder />
+                    <CheckoutPlaceOrder 
+                        paymentMethod={selectedPaymentMethod}
+                        clickPlaceOrder={handlePlaceOrder}
+                    />
                 </div>
             </div>
-           
+
         </section> 
         
     );

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


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


+ 121 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -0,0 +1,121 @@
+"use client";
+
+import { useState } from "react";
+import Image from "next/image";
+import CommonModal from "@/components/theme/ui/CommonModal";
+import {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";
+
+
+// 根据products 计算总价和总个数
+function caclculateTotal(products: ProductsInOrderDetails[]) {
+    let total = 0;
+    let totalItems = 0;
+
+    products.forEach((product) => {
+        total += product.total * 100;
+        totalItems += product.qtyOrdered;
+    });
+    let ss = String(total);
+    if(ss.length >= 3) {
+        ss = ss.slice(0, ss.length - 2) + "." + ss.slice(ss.length - 2);
+    } else if(ss.length === 2) {
+        ss = "0." + ss;
+    } else {
+        ss = "0.0" + ss;
+    }
+
+    return { 
+        subTotal: ss, 
+        totalItems: totalItems > 1 ? String(totalItems) + " Items" : String(totalItems) + " Item"
+    };
+}
+
+function getProductAdditionalInfo(productItem: ProductItemAdditional) { 
+    const attributeKeys = Object.keys(productItem.attributes);
+    let res = '';
+    attributeKeys.forEach(key => {
+      const attribute = productItem.attributes[key];
+      if (attribute) {
+        // option_label value_label
+
+        if(res) {
+            res = res + ', ' + attribute.option_label + ': ' + attribute.value_label;
+        } else {
+            res = attribute.option_label + ': ' + attribute.value_label;
+        }
+      }
+    });
+    return res;
+}
+
+export default function ProductsModalPendingOrder({
+    isOpen,
+    products,
+    currencySymbol,
+    onClose,
+}: {
+    isOpen: boolean;
+    products: ProductsInOrderDetails[],
+    currencySymbol: string;
+    onClose: () => void;
+}) {
+
+    const { subTotal, totalItems } = caclculateTotal(products);
+    
+
+    const closeModal = () => {
+        onClose();
+    };
+  return (
+    <CommonModal
+      isOpen={isOpen}
+      onClose={() => closeModal()}
+      header={
+        <div className="relative w-full box-border p-4">
+            <div className="flex justify-between items-center">
+                <span className="text-ly-16 flex-none">Order Summary</span>
+                <button className="flex-none w-6 h-6" onClick={() => closeModal()}>
+                    <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
+                </button>
+            </div>
+            
+            <div className="mt-6 flex justify-between items-center">
+                <span className="text-ly-12">{totalItems}</span>
+                <span className="text-ly-14 font-bold">{currencySymbol + subTotal}</span>
+            </div>
+        </div>
+      }
+      body={
+        <div className="w-full px-4 pb-6">
+            {products.map((product) => {
+                return (
+                    <div key={product.id} className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0">
+                        <div className="relative flex-none w-33 h-44 mr-2">
+                            <Image fill={true} src="https://cdn.asteriahair.com/media/catalog/product/6/_/6_3_2.png" alt={product.name} />
+                        </div>
+                        <div className="flex-auto flex flex-col justify-between ">
+                            <div>
+                                <p className="text-ly-12 leading-ly-20 line-clamp-2 text-ly-placeholder">
+                                    {product.name}
+                                </p>
+                                <p className="text-ly-12 leading-ly-20 mt-3">
+                                    {getProductAdditionalInfo(JSON.parse(product.additional))}
+                                </p>
+                            </div>
+                            <div className="flex justify-between items-center">
+                                <span className="text-ly-14 font-bold">
+                                    {currencySymbol + product.price}
+                                </span>
+                                <span className="text-ly-12">X {product.qtyOrdered}</span>
+                            </div>
+                        </div>
+                    </div>
+                )
+            })}
+            
+        </div>
+      }
+      
+    />
+  )
+}

+ 1 - 1
src/app/(checkout)/checkout/_components/LoadingPaymentMethod.tsx

@@ -2,7 +2,7 @@ export default function LoadingPaymentMethod() {
     return (
         <div className="mt-3 w-full animate-pulse px-0">
             <div className="group box-border flex items-center relative mt-3 p-3 rounded-sm">
-                <p className="pr-2 flex-initial h-8 w-12 bg-gray-300"></p>
+                <p className="pr-2 flex-none h-8 w-12 bg-gray-300"></p>
                 <p className="text-ly-12 text-gray-300 bg-gray-300 ml-1">Paypay Smart Button</p>
   
 

+ 292 - 0
src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx

@@ -0,0 +1,292 @@
+"use client";
+
+
+import {useRef} from "react";
+import { redirect, RedirectType } from 'next/navigation';
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { getCookie } from "@/utils/cookie-tools";
+import { IS_GUEST } from "@/utils/constants";
+import {
+  usePayPal,
+  useEligibleMethods,
+  INSTANCE_LOADING_STATE,
+  usePayPalOneTimePaymentSession,
+  usePayPalGuestPaymentSession,
+  usePayLaterOneTimePaymentSession,
+  type OnApproveDataOneTimePayments,
+  type OnErrorData,
+  type OnCompleteData,
+  type OnCancelDataOneTimePayments,
+} from "@paypal/react-paypal-js/sdk-v6";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+export default function PaypalButton({
+    createOrder,
+    onPaymentClick,
+    isRePay
+}:{
+  createOrder: () => Promise<{orderId: string;webOrderId: string;error: boolean;msg: string;}>;
+  onPaymentClick: () => Promise<boolean>;
+  isRePay?: boolean;
+}) { 
+    const dispatch = useAppDispatch();
+    const isGuest = getCookie(IS_GUEST) !== "false";// 'true' 是游客
+    const {createPaymentCallback,cancelOrder} = usePlaceOrder();
+
+    const gatewayOrderIdRef = useRef("");
+    const webOrderIdRef = useRef("");
+    // paypal sdk 是否加载完成
+    const { loadingStatus } = usePayPal();
+
+    // Fetch eligibility(资格) for one-time payment flow
+    const {
+        error: eligibilityError,
+        eligiblePaymentMethods,
+        isLoading: isEligibilityLoading,
+    } = useEligibleMethods({
+        payload: {
+        currencyCode: "USD",
+        paymentFlow: "ONE_TIME_PAYMENT",
+        },
+    });
+    const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
+    const isPayLaterEligible =
+        !isEligibilityLoading && eligiblePaymentMethods?.isEligible("paylater");
+    //   const isCreditEligible =
+    //     !isEligibilityLoading && eligiblePaymentMethods?.isEligible("credit");
+
+    const handlerPaypalClick = async (buttonHandlerClick: () => Promise<void | {
+        redirectURL?: string | undefined;
+    }>) => {
+        overlayLoading.start();
+        const vaild = await onPaymentClick();
+        console.log('789----', vaild);
+        if(vaild) {
+            const res = await createOrder();
+            if(res.error) {
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: res.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            } else {
+                //创建订单成功后清空redux 购物车
+                dispatch(clearCart());
+                gatewayOrderIdRef.current = res.orderId;
+                webOrderIdRef.current = res.webOrderId;
+                return buttonHandlerClick();
+            }
+            
+        } else {
+            overlayLoading.stop();
+        }
+    }
+
+    // paypal支付回调配置
+    const paypalOnHandlerConfig = {
+        createOrder: async () => {
+            // const res = await createOrder();
+            // gatewayOrderIdRef.current = res.orderId;
+            // webOrderIdRef.current = res.webOrderId;
+            const res = {
+                orderId: gatewayOrderIdRef.current, 
+                webOrderId: webOrderIdRef.current
+            };
+
+            return res;
+        },
+        
+        onApprove: async (data: OnApproveDataOneTimePayments) => {
+            console.log("Payment approved:", data);
+            const resCallback = await createPaymentCallback({
+                orderId: Number(webOrderIdRef.current),
+                gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
+                status: 'success',
+            });
+            if(!resCallback.error) {
+                // 支付成功,跳转到成功落地页 
+                confirmDialog({
+                    title: "Payment Success",
+                    content: "Your payment was successful. Will redirect to success page.",
+                    noCancel: true,
+                }).then(() => {
+                    redirect('/success?orderid=' + webOrderIdRef.current, RedirectType.replace);
+                });
+            } else {
+                // callback 失败,提示错误
+                confirmDialog({
+                    title: "Payment failed",
+                    content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderIdRef.current,
+                    noCancel: true,
+                }).then(() => { 
+                    redirect('/', RedirectType.replace);
+                });
+                
+            }
+
+        },
+
+        onCancel: async (data: OnCancelDataOneTimePayments) => {
+            /**
+             * {oederId: paypalOrderId}
+             */
+            console.log("Payment cancelled:", data);
+            await createPaymentCallback({
+                orderId: Number(webOrderIdRef.current),
+                gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
+                status: 'cancel',
+            });
+            if(!isRePay) {
+                if(isGuest) {
+                    
+                    await cancelOrder({
+                        orderId: Number(webOrderIdRef.current),
+                    });
+                    // deleteCookie(GUEST_CART_TOKEN);
+                    // deleteCookie(GUEST_CART_ID);
+                    confirmDialog({
+                        title: "Order Cancelled",
+                        content: "Your order has been cancelled. Will redirect to home page.",
+                        noCancel: true,
+                    }).then(() => { 
+                        redirect('/' , RedirectType.replace);
+                    });
+                    
+                } else {
+                    // 登录用户 跳转二次支付页面 提醒用户可以二次支付
+                    confirmDialog({
+                        title: "Payment cancelled",
+                        content: "Will redirect to unpaied page.",
+                        noCancel: true,
+                    }).then(() => { 
+                        redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
+                    });
+                }
+            }
+        },
+
+        onError: async (data: OnErrorData) => {
+            console.error("Payment error:", data);
+            /**
+             * code: string;
+                 name: string;
+                isRecoverable: boolean;
+            */
+            await createPaymentCallback({
+                orderId: Number(webOrderIdRef.current),
+                gatewayOrderId: gatewayOrderIdRef.current,
+                status: 'failure',
+            });
+            if(!isRePay) {
+                if(isGuest) {
+                    
+                    await cancelOrder({
+                        orderId: Number(webOrderIdRef.current),
+                    });
+                    // deleteCookie(GUEST_CART_TOKEN);
+                    // deleteCookie(GUEST_CART_ID);
+                    confirmDialog({
+                        title: "Payment Failed",
+                        content: data.message + " Will redirect to home page.",
+                        noCancel: true,
+                    }).then(() => { 
+                        redirect('/', RedirectType.replace);
+                    });
+                } else {
+                    // 弹出错误提示弹窗:显示错误信息
+                    // 如果是登录用户,跳转二次支付页面 提醒用户可以二次支付
+                    confirmDialog({
+                        title: "Payment Failed",
+                        content: data.message + " Will redirect to unpaied page.",
+                        noCancel: true,
+                    }).then(() => { 
+                        redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
+                    });
+                }
+            }
+        
+        },
+
+        onComplete: (data: OnCompleteData) => {
+            overlayLoading.stop();
+            console.log("Payment session completed");
+            console.log("On Complete data:", data);
+        },
+        // onShippingAddressChange: (data) => {},
+        // onShippingOptionsChange: (data) => {
+
+        // }
+    };
+
+    const { 
+        // isPending: paypalButtonPending, 
+        error: paypalButtonError, 
+        handleClick: paypalButtonHandleClick, 
+    } = usePayPalOneTimePaymentSession({
+        presentationMode: "auto",
+        ...paypalOnHandlerConfig
+    });
+
+    const { 
+        buttonRef: guestButtonRef, 
+        // isPending: guestButtonPending, 
+        error: guestButtonError, 
+        handleClick: guestButtonHandleClick,
+    } = usePayPalGuestPaymentSession(paypalOnHandlerConfig);
+
+    const payLaterDetails = eligiblePaymentMethods?.getDetails("paylater");
+    const { 
+        // isPending: paylaterButtonPending, 
+        // error: paylaterButtonError, 
+        handleClick: paylaterButtonHandleClick, 
+    } = usePayLaterOneTimePaymentSession({
+        presentationMode: "auto",
+        ...paypalOnHandlerConfig
+    });
+
+    return (
+        <div className="w-full">
+            {
+                isLoading ? (
+                    <div className="test-ly-12 font-medium text-center">
+                        Loading paypal payment methods...
+                    </div>
+                ) : eligibilityError ? (
+                    <div className="test-ly-12 font-medium text-center text-ly-errorcolor">
+                        Failed to load paypal payment options. Please refresh the page.
+                    </div>
+                ) : (
+                    <>
+                        {paypalButtonError ? 
+                            <p className="text-ly-errorcolor text-ly-12">Error: {paypalButtonError.message}</p>
+                        :
+                            <paypal-button onClick={() => {
+                                return handlerPaypalClick(paypalButtonHandleClick);
+                            }} type="pay" className="w-full" />
+                        }
+                        {guestButtonError ?
+                            <p className="text-ly-errorcolor text-ly-12">Error: {guestButtonError.message}</p>
+                        :
+                            <paypal-basic-card-container className="w-full mt-3">
+                                <paypal-basic-card-button ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
+                            </paypal-basic-card-container>
+                        }
+                     
+                        {isPayLaterEligible && 
+                            <paypal-pay-later-button className="w-full mt-3"
+                                onClick={() => handlerPaypalClick(paylaterButtonHandleClick)}
+                                countryCode={payLaterDetails?.countryCode}
+                                productCode={payLaterDetails?.productCode}
+                            />
+                        }
+                        
+                    </>
+                )
+            }
+  
+        </div>
+    );
+}

+ 48 - 14
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,37 +1,46 @@
 "use client";
 
 import Image from "next/image";
-import clsx from "clsx";
-import { useEffect, useState } from "react";
+import { useEffect, Ref, useImperativeHandle, useRef } from "react";
 import { useForm, get } from "react-hook-form";
 import {CheckoutPaymentMethod} from "@/types/checkout/type";
-import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 import { useAppSelector } from "@/store/hooks";
-import { useCustomToast } from "@/utils/hooks/useToast";
-import { useCartDetail } from "@utils/hooks/useCartDetail";
-import {useCheckoutPaymentMethod} from  "@/utils/hooks/useCheckoutPaymentMethod";
+
 /**
  * 关于如何确定默认选中哪个支付方式:
  * 从购物车详情中获取支付方式 A,支付方式列表中有A,那么把A标记为默认选中;如果支付方式列表中没有A,把paypal标记为默认选中;
  */
-
-export default function PaymentMethodCheckout({
+export interface RefPaymentMethodsHandle {
+  getSelectPaymentMethod: () => string;
+  getSelectPaymentTitle: () => string;
+  validatePaymentMethod: () => Promise<boolean>;
+}
+export function PaymentMethodCheckout({
+    ref,
     errorMsg,
     paymentMethods,
     onPaymentMethodChange
 }: {
+    ref: Ref<RefPaymentMethodsHandle>;
     errorMsg: string | null;
     paymentMethods: CheckoutPaymentMethod[];
     onPaymentMethodChange: (value: string,title:string) => void;
 }) {
-    // const { getCartDetail } = useCartDetail();
+    const selectPaymentTitle = useRef('');
     const cartDetail = useAppSelector((state) => state.cartDetail);
-    const defaultPaymentMethod = cartDetail.cart?.paymentMethod ?? '';
+    const defaultPaymentMethod = cartDetail.cart?.paymentMethod ?? 'paypal_smart_button';
 
+    paymentMethods.forEach((item) => {
+        if(item.method === defaultPaymentMethod) {
+            selectPaymentTitle.current = item.title;
+        }
+    });
   
     const {
         register,
         reset,
+        getValues,
+        trigger,
         formState: { errors }
     } = useForm({
         mode: "onChange",
@@ -41,19 +50,43 @@ export default function PaymentMethodCheckout({
     // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
     const paymentMethodError = get(errors, "paymentMethod");
 
+
+    useImperativeHandle(ref, () => {
+        return {
+            // 获取用户选择的支付方式
+            getSelectPaymentMethod: () => {
+                return getValues('paymentMethod');
+            },
+            getSelectPaymentTitle: () => {
+                return selectPaymentTitle.current;
+            },
+            // 触发校验
+            validatePaymentMethod: () => {
+                return trigger();
+            }
+        }
+        
+    },[]);
+
     useEffect(() => {
         let defaultValue = '';
-        if(paymentMethods.find((item) => item.method === defaultPaymentMethod)) {
+        let title = '';
+        const findItem = paymentMethods.find((item) => item.method === defaultPaymentMethod);
+        if(findItem !== undefined) {
             defaultValue = defaultPaymentMethod;
+            title = findItem.title;
         } else {
             paymentMethods.forEach((item) => {
                 if(item.method === 'paypal_smart_button') {
                     defaultValue = item.method;
+                    title = item.title;
                 }
             });
         }
         console.log('default payment effect ===================',defaultValue);
         reset({ paymentMethod: defaultValue });
+        
+        // onPaymentMethodChange(defaultValue,title);
     }, [paymentMethods,defaultPaymentMethod]);
 
     
@@ -68,8 +101,8 @@ export default function PaymentMethodCheckout({
                             <label htmlFor={"paymentmethod-" + item.method} key={item.method}
                                 className="group box-border flex items-center relative bg-ly-lightgray mt-3 p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                             >
-                                <p className="pr-2 flex-initial">
-                                    <Image className="block h-8 w-auto" src={item.icon || ''} alt="paypal" width={48}  height={32} />
+                                <p className="pr-2 flex-none relative">
+                                    <Image className="block h-8 w-auto" width={32} height={32} src={item.icon || ''} alt={"Payment Method " + item.title} />
                                 </p>
                                 <p className="text-ly-12 text-[#666666] ml-1">{item.description}</p>
 
@@ -82,9 +115,10 @@ export default function PaymentMethodCheckout({
                                     type="radio"
                                     value={item.method}
                                     {...register('paymentMethod', {
-                                        required: 'Please select a shipping method.',
+                                        required: 'Please select a payment method.',
                                         onChange: (event) => {
                                             console.log(event.target.value);
+                                            selectPaymentTitle.current = item.title;
                                             onPaymentMethodChange(event.target.value,item.title);
                                         },
                                     })}

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

@@ -0,0 +1,17 @@
+"use client";
+
+import { useAppSelector,useAppDispatch } from "@/store/hooks";
+
+
+// 金额明细
+export default function PromotionsDetails() { 
+
+    const cartDetail = useAppSelector((state) => state.cartDetail);
+    const {cart, loading, error} = cartDetail;
+
+    return (
+        <div className="">
+
+        </div>
+    );
+}

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

@@ -114,7 +114,7 @@ export default function ShippingAddressCheckout ({
                     Name *
                 </label>
                 <div className="w-full flex justify-between">
-                    <div className="w-41.25 flex-initial">
+                    <div className="w-41.25 flex-none">
                         <InputText
                             type="text"
                             placeholder="First name"
@@ -128,7 +128,7 @@ export default function ShippingAddressCheckout ({
                             error={errors.shippingFirstName && errors.shippingFirstName.message as string}
                         />
                     </div> 
-                    <div className="w-41.25 flex-initial">
+                    <div className="w-41.25 flex-none">
                         <InputText
                             type="text"
                             placeholder="Last name"
@@ -162,7 +162,7 @@ export default function ShippingAddressCheckout ({
                 <label className="text-ly-12 block mb-4 font-semibold">Country and State/Province  *</label>
                 <div className="w-full">
                     <div className="w-full flex justify-between">
-                        <div className="w-41.25 flex-initial">
+                        <div className="w-41.25 flex-none">
                             <Select placeholder="Country/Region"
                                 {...register("shippingCountry", {
                                     required: "Country/Region field is required",
@@ -182,7 +182,7 @@ export default function ShippingAddressCheckout ({
                                 error={errors.shippingCountry && errors.shippingCountry.message as string}
                             /> 
                         </div>
-                        <div className="w-41.25 flex-initial relative">
+                        <div className="w-41.25 flex-none relative">
                             {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
                             {statesOptions.length > 0 ?
                             (
@@ -209,7 +209,7 @@ export default function ShippingAddressCheckout ({
                         
                     </div>
                     <div className="w-full flex justify-between mt-4">
-                        <div className="w-41.25 flex-initial">
+                        <div className="w-41.25 flex-none">
                             <InputText
                                 type="text"
                                 placeholder="Zip/Postal Code"
@@ -223,7 +223,7 @@ export default function ShippingAddressCheckout ({
                                 error={errors.shippingPostcode&& errors.shippingPostcode.message as string}
                             />
                         </div>
-                        <div className="w-41.25 flex-initial">
+                        <div className="w-41.25 flex-none">
                             <InputText
                                 type="text"
                                 placeholder="City"

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


+ 90 - 0
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -0,0 +1,90 @@
+import {Suspense} from "react";
+import { getServerSession } from "next-auth";
+import { redirect, RedirectType } from 'next/navigation';
+import { authOptions } from "@utils/auth";
+import { authorizationGraphqlQuery, fullCacheGraphqlRequest} from "@/lib/graphql-fetch";
+import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
+import { GET_ORDER_DETAILS, GET_CHECKOUT_PAYMENT_METHODS, GET_CURRENCIES } from "@/graphql";
+// import { Country } from "@/types/checkout/type";
+import { OrderDetailsData } from "@/types/customer/type";
+import { CurrenciesData } from "@/types/currency/type";
+import { CheckoutPaymentMethodsData } from "@/types/checkout/type";
+import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
+
+// 只能存在一个待支付订单
+export const dynamic = "force-dynamic";
+// searchParams orderid
+// 游客不能进入该页面
+// 24小时倒计时
+// 地址信息
+// 购买产品
+// 金额明细
+// 支付方式
+// 支付按钮
+
+
+// express: false
+// gatewayOrderId: "76B005629R5441508"
+// id: "2602"
+// message: "Payment initiated successfully"
+// newCartToken: null
+// orderId:  "2602"
+// status: "pending"
+// success: true
+export default async function ContinueToPay({searchParams}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+    
+    const session = await getServerSession(authOptions);// 游客是null
+    // 游客跳转首页
+    if(session === null) {
+        redirect('/', RedirectType.replace);
+    }
+
+    const query = await searchParams;
+    const orderid = query?.orderid;
+    const orderDetailResponse = await authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
+        id: orderid,// `/api/shop/customer-orders/${orderid}`
+    });
+    console.log("orderDetailPromise ==========",orderDetailResponse); 
+    const orderDetailData = orderDetailResponse.data?.customerOrder;
+    if(orderDetailData === undefined || orderDetailData?.status !== "pending")  {
+        redirect('/', RedirectType.replace);
+    }
+
+    const currencyRes = await fullCacheGraphqlRequest<CurrenciesData>(GET_CURRENCIES);
+    console.log("currencyRes ==========",currencyRes); 
+    const currencyList = currencyRes.data?.currencies?.edges.map((item) => item.node) ?? [];
+    // 获取支付方式列表
+    const paymentMethodsResponse = authorizationGraphqlQuery<CheckoutPaymentMethodsData>(GET_CHECKOUT_PAYMENT_METHODS);
+    
+    const currencySymbol = currencyList.find(item => item.code === orderDetailData.orderCurrencyCode)?.symbol ?? '';
+    
+    // const {data: countriesData} = await fullCacheGraphqlRequest<{
+    //     countries: Country[] 
+    // }>(GET_COUNTRIES);
+
+    
+        
+
+    return (
+        <div className="w-full pb-8">
+        
+            <ContinueToPayOrderInfo orderDetail={orderDetailData} currencySymbol={currencySymbol}/>
+           
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-24 font-medium">Payment Method</h3>
+
+            </div>
+            <Suspense fallback={<div>Loading...</div>}> 
+                <PaymentMethodContinueTpPay 
+                    orderId={orderDetailData.incrementId}
+                    paymentMethodsDataPromise={paymentMethodsResponse}
+                    defaultPaymentMethod={orderDetailData.payment}
+                />
+            </Suspense>
+            
+        </div>
+    );
+  
+}

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

@@ -1,9 +1,11 @@
 import { getServerSession } from "next-auth";
 import { authOptions } from "@utils/auth";
-import { fullCacheGraphqlRequest} from "@/lib/graphql-fetch";
+import { redirect, RedirectType } from 'next/navigation';
+import { fullCacheGraphqlRequest, authorizationGraphqlMutate} from "@/lib/graphql-fetch";
 import CheckoutWrapper from "./_components/CheckoutWrapper";
-import { GET_COUNTRIES } from "@/graphql";
+import { GET_COUNTRIES, GET_CART_ITEM } from "@/graphql";
 import { Country } from "@/types/checkout/type";
+import { GetCartItemData } from "@/types/cart/type";
 
 /**
  * 进入结账页 
@@ -15,23 +17,9 @@ import { Country } from "@/types/checkout/type";
  *  
  * 
  */
+export const dynamic = "force-dynamic";
 
-
-export default async function Information({
-    searchParams,
-}: {
-    searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
-    const { step = "email" } = (await searchParams) as { [key: string]: string };
-    // const {data: addressData, error: addressError} = await authorizationGraphqlRequest<GetCheckoutAddressesData>(GET_CHECKOUT_ADDRESSES);
-    // console.log('address -- ',addressData,addressError);
-    // const address =  addressData?.collectionGetCheckoutAddresses?.edges?.map(
-    //         (edge: { node: CheckoutAddressNode }) => edge.node
-    //     ) || [];
-    // const preBillingAddress = address.find((a: CheckoutAddressNode) => a?.addressType === "cart_billing") || null;
-    // const preShippingAddress = address.find((a: CheckoutAddressNode) => a?.addressType === "cart_shipping") || null;
-
-    // const countries = await getCountries();
+export default async function CheckoutPage() {
 
     const session = await getServerSession(authOptions);
 
@@ -39,11 +27,13 @@ export default async function Information({
         countries: Country[] 
     }>(GET_COUNTRIES);
 
+    const cartDetails = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
 
-  
+    if(cartDetails.data === null || cartDetails.data.createReadCart === null) {
+        redirect('/', RedirectType.replace);
+    }
     return (
         <>
-            {/* {addressError && <p className="text-ly-errorcolor text-ly-12">{addressError}</p>} */}
             <CheckoutWrapper 
                 loginEmail={session?.user?.email || ""}
                 countries={countriesData?.countries || []}

+ 2 - 2
src/app/(checkout)/layout.tsx

@@ -10,9 +10,9 @@ export default async function RootLayout({
 
       <TitleHeader title="Checkout" />
 
-      <main className="mx-auto w-full">
+      <div className="mx-auto w-full">
         {children}
-      </main>
+      </div>
     </>
   );
 }

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

@@ -13,7 +13,7 @@ export function ProductAddToCart({
     onAddToCart: () => void;
     onBuyNow: () => void;
 }) { 
-    const btnClass = 'flex-initial flex items-center justify-center w-40 h-11.5 rounded-4xl text-ly-16 font-bold text-white';
+    const btnClass = 'flex-none flex items-center justify-center w-40 h-11.5 rounded-4xl text-ly-16 font-bold text-white';
     return (
         <div className="fixed w-full bottom-0 left-0 bg-white box-border pt-3 pl-3 pr-3 pb-5 border-t border-solid border-ly-middlegray z-1000">
             {isAvailable ? (

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

@@ -272,13 +272,13 @@ export function ProductInformation({
             <div className="text-ly-12 font-medium mt-4">Quantity*</div>
             <div className="w-full flex mt-3">
                 <div className="flex border border-solid rounded-lg">
-                    <button onClick={() => handlerProductQty('decrease')} type="button" className="flex-initial w-7.5 h-9 flex items-center justify-center">
+                    <button onClick={() => handlerProductQty('decrease')} type="button" className="flex-none w-7.5 h-9 flex items-center justify-center">
                         <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
                             <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M3.33325 8L12.6666 8" />
                         </svg>
                     </button>
                     <input type="text" className="w-15 h-9 leading-ly-36 text-center" value={productQty} readOnly />
-                    <button onClick={() => handlerProductQty('increase')} type="button" className="flex-initial w-7.5 h-9 flex items-center justify-center">
+                    <button onClick={() => handlerProductQty('increase')} type="button" className="flex-none w-7.5 h-9 flex items-center justify-center">
                         <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
                             <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M8.02026 3.3335L8.00806 12.6668" />
                             <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M3.33325 8L12.6666 8" />

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


+ 124 - 0
src/app/(public)/success/_components/SuccessPageSkeleton.tsx

@@ -0,0 +1,124 @@
+
+export default function SuccessPageSkeleton() { 
+    return (
+        <div className="w-full animate-pulse px-0">
+
+            <div className="w-full">
+                <div className="box-border px-4 w-full">
+                    <div className="w-12 h-12 mx-auto mt-9 bg-gray-300"></div>
+                    <p className="text-ly-16 text-center mt-6">
+                        <span className="text-gray-300 bg-gray-300">Your Order Has Been Received</span>
+                    </p>
+                    <p className="text-ly-16 mt-4 flex justify-center">
+                        <span className="px-3.75 leading-ly-28 text-gray-300 bg-gray-300">Order #600000000</span>
+                    </p>
+                    <p className="text-ly-12 text-center mt-4 leading-ly-22">
+                        <span className="text-gray-300 bg-gray-300">Hello, lala</span>
+                    </p>
+                    <p className="text-ly-12 text-center leading-ly-22">
+                        <span className="text-gray-300 bg-gray-300">Thank you so much for your order!</span>
+                    </p>
+                    <p className="text-ly-12 text-center leading-ly-22">
+                        <span className="text-gray-300 bg-gray-300">We’ll get started on your order very soon!</span>
+                    </p>
+                    <p className="text-ly-12 text-center leading-ly-22">
+                        <span className="text-gray-300 bg-gray-300">You’ll receive an order confirmation email with details of your order in an hour, please be sure to check your email and confirm your order, that is very important for us to process your order.</span>
+                    </p>
+                    <div className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                        <span className="text-gray-300 bg-gray-300">Continue Shopping</span>
+                    </div>
+                </div>
+                <div className="box-border px-4 w-full mt-6">
+                    <div className="relative w-full bg-ly-lightgray box-border p-3">
+                        <button className="absolute top-3 right-3 w-6 h-6 bg-gray-300"></button>
+                        <p className="text-ly-13"><span className="text-gray-300 bg-gray-300">Faqs</span></p>
+                        <p className="text-ly-12 text-[#a6a6a6] mt-2">
+                            <span className="text-gray-300 bg-gray-300">Pls contact customer service if need any help</span>
+                        </p>
+                    </div>
+                </div>
+                <div className="box-border px-4 w-full mt-6">
+                    <h4 className="text-ly-24 font-medium">
+                        <span className="text-gray-300 bg-gray-300">Order Information</span>
+                    </h4>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Ship to:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            Alessandro  6263378809
+                        </div>
+                    </div>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Order number:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            600465290
+                        </div>
+                    </div>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Status:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            Processing
+                        </div>
+                    </div>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Order date:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            September 24,2020
+                        </div>
+                    </div>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Shipping method:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            Same day delivery
+                        </div>
+                    </div>
+                    <div className="mt-6 flex justify-between">
+                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Payment method:</div>
+                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
+                            Paypal-express
+                        </div>
+                    </div>
+
+                </div>
+
+                {/* <div className="box-border px-4 w-full mt-6">
+                    <div className="w-full flex gap-2 pb-4 border-b-1 border-[#f0f0f0] last:border-0 last:pb-0">
+                        <div className="relative w-33 h-44 flex-none">
+                            <Image fill src="https://cdn.alipearlhair.com/media/catalog/product/2/_/2_104_20.jpg" alt="logo" />
+                        </div>
+                        <div className="flex-none w-50.75 flex flex-col justify-between">
+                            <div>
+                                <p className="text-ly-12 leading-ly-20 line-clamp-2">
+                                    Deep Wave Wig 100% Human Hair Deep Wave Wig 100% Human Hair  100% Human Hair
+                                </p>
+                                <p className="mt-2 text-ly-12 leading-ly-20 text-ly-placeholder">14/4*4 HD Lace/150%</p>
+                            </div>
+                            <div className="flex justify-between">
+                                <p className="text-ly-14 leading-ly-20 font-bold">$10.00</p>
+                                <p className="text-ly-12 leading-ly-20">x 1</p>
+                            </div>
+                        </div>
+                    </div>
+                </div> */}
+
+                {/* <div className="box-border px-4 w-full mt-6 pt-6 border-t-1 border-black">
+                    <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">0.00</p>
+                    </div>
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20">Shipping Method</p>
+                        <p className="text-ly-12 leading-ly-20">0.00</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">0.00</p>
+                    </div>
+                </div> */}
+            </div>
+        </div>
+    );
+}

+ 32 - 16
src/app/(public)/success/page.tsx

@@ -1,17 +1,33 @@
-import { ClearCartButton } from "@components/checkout/success/EmptyCart";
-import OrderDetail from "@components/cart/OrderDetail";
-import CheckSign from "@components/common/icons/CheckSign";
+import {Suspense} from "react";
+import { cookies } from 'next/headers';
+import { redirect, RedirectType } from 'next/navigation'
+import {authorizationGraphqlQuery} from "@/lib/graphql-fetch";
+import {GET_ORDER_DETAILS} from "@/graphql";
+import { OrderDetailsData } from "@/types/customer/type";
+import SuccessPageSkeleton from "./_components/SuccessPageSkeleton";
+import OrderDetailWrapper from "./_components/OrderDetailWrapper";
+import { GUEST_CART_TOKEN,GUEST_CART_ID } from "@/utils/constants";
 
-const SuccessPage = () => {
-  return (
-    <div className="flex min-h-[calc(100vh-450px)] items-center px-4">
-      <div className="flex w-full flex-col items-center justify-center overflow-hidden">
-        <CheckSign className="h-28 w-28 sm:h-38 sm:w-38" />
-        <OrderDetail />
-        <ClearCartButton buttonName="Continue shopping" redirect="/" />
-      </div>
-    </div>
-  );
-};
-
-export default SuccessPage;
+// 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
+export default async function SuccessPage({searchParams}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+})  {
+    const cookieStore = await cookies();
+    // 登录用户cookie里没有GUEST_CART_ID
+    if(!cookieStore.get(GUEST_CART_TOKEN)) {
+        redirect('/', RedirectType.replace);
+    }
+    const query = await searchParams;
+    const orderid = query?.orderid;
+    const orderDetailPromise = authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
+        id: orderid,// `/api/shop/customer-orders/${orderid}`
+    });
+    return (
+        
+        <div className="w-full">
+            <Suspense fallback={<SuccessPageSkeleton />}>
+                <OrderDetailWrapper orderDetailPromise={orderDetailPromise} />
+            </Suspense>
+        </div>
+    );
+}

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

@@ -17,6 +17,10 @@ import {
     CREATE_CHECKOUT_PAYMENT_METHODS,
     CREATE_CHECKOUT_ORDER,
     CREATE_PRODUCT_REVIEW,
+    CREATE_PAYMENT_INITIATE,
+    CREATE_SAVE_CHECKOUT_CART,
+    GET_ORDER_DETAILS,
+    CREATE_CANCEL_ORDER,
     // GET_PRODUCT_BY_URL_KEY,
 } from "@/graphql";
 
@@ -35,6 +39,10 @@ const ALLOWED_OPERATIONS: Record<string, any> = {
     CreateCheckoutPaymentMethod: CREATE_CHECKOUT_PAYMENT_METHODS,
     CreateCheckoutOrder: CREATE_CHECKOUT_ORDER,
     CreateProductReview: CREATE_PRODUCT_REVIEW,
+    CreatePaymentInitiate: CREATE_PAYMENT_INITIATE, // 创建订单
+    CreateSaveCheckoutCart: CREATE_SAVE_CHECKOUT_CART,
+    GetOrderDetails: GET_ORDER_DETAILS, // 获取订单详情
+    CreateCancelOrder: CREATE_CANCEL_ORDER
 };
 // const QUERY_OPERATIONS: Record<string, any> = {
 //     GetProductById: GET_PRODUCT_BY_URL_KEY,
@@ -55,7 +63,7 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
     const query = ALLOWED_OPERATIONS[operationName];
     let finalVariables = variables;
 
-    if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
+    /*if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
         finalVariables = { ...variables };
     }
 
@@ -66,9 +74,9 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
             failureUrl: variables?.failureUrl ?? `payment/failure`,
             cancelUrl: variables?.cancelUrl ?? `payment/cancel`
         };
-    }
+    }*/
 
-    if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
+    /*if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
         finalVariables = {
             billingFirstName: body.billingFirstName,
             billingLastName: body.billingLastName,
@@ -94,15 +102,15 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
                 shippingCompanyName: body.shippingCompanyName,
             })
         };
-    }
+    }*/
 
-    if (operationName === 'createAddProductInCart' && body.productId) {
+    /*if (operationName === 'createAddProductInCart' && body.productId) {
         finalVariables = {
             cartId: body.cartId ?? null,
             productId: body.productId,
             quantity: body.quantity,
         };
-    }
+    }*/
     return {
         query,
         variables: finalVariables,

+ 35 - 6
src/app/layout.tsx

@@ -1,13 +1,17 @@
 import { Outfit } from "next/font/google";
 import "./globals.css";
-import { GlobalProviders } from "@/providers";
+import { GlobalProviders,ReduxProvider } from "@/providers";
 import { generateMetadataForPage } from "@utils/helper";
 import { staticSeo } from "@utils/metadata";
 import { SpeculationRules } from "@components/theme/SpeculationRules";
 import { ErrorBoundary } from "@/components/error/ErrorBoundary";
 import { AddToCartModalWrapper } from "@components/common/AddToCartModal/AddToCartModalWrapper";
 import clsx from "clsx";
+import { PayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
 import {KernelProvider} from "@components/theme/ui/kernel/KernelProvider";
+import { GET_CART_ITEM } from "@/graphql";
+import { authorizationGraphqlMutate} from "@/lib/graphql-fetch";
+import { GetCartItemData } from "@/types/cart/type";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -36,11 +40,15 @@ export async function generateMetadata() {
   return generateMetadataForPage("", staticSeo.default);
 }
 
-export default function RootLayout({
+export default async function RootLayout({
   children,
 }: Readonly<{
   children: React.ReactNode;
 }>) {
+
+  const cartDetailsRes = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
+  
+  const cartDetails = cartDetailsRes.data?.createReadCart?.readCart ?? null;
   return (
     <html lang="en" suppressHydrationWarning>
       <head>
@@ -53,10 +61,31 @@ export default function RootLayout({
         
         <main>
           <ErrorBoundary>
-              <GlobalProviders>
-                {children}
-                <AddToCartModalWrapper />
-              </GlobalProviders>
+            <PayPalProvider
+              clientId="AcI7EUfRlGbMDeK94cJvJ46Iu14Z-Ms1_pQRxABQe6lMdt03eaL42BuGocIDIBnm-9F9IpxUke0CazrZ"
+              components={[
+                "paypal-payments",
+                "venmo-payments",
+                "paypal-guest-payments",
+                "paypal-subscriptions",
+                "card-fields",
+                "paypal-messages",
+              ]}
+              pageType="checkout"
+            >
+              <ReduxProvider preloadedState={{
+                cartDetail:{
+                  cart: cartDetails,
+                  loading: false,
+                  error: null
+                }
+              }}>
+                <GlobalProviders>
+                  {children}
+                  <AddToCartModalWrapper />
+                </GlobalProviders>
+              </ReduxProvider>
+            </PayPalProvider>
             <SpeculationRules />
           </ErrorBoundary>
         </main>

+ 18 - 58
src/components/cart/CartModal.tsx

@@ -13,16 +13,14 @@ import { EditItemQuantityButton } from "../common/icons/cart/EditItemQuantityBut
 import { useCartDetail } from "@utils/hooks/useCartDetail";
 import Image from "next/image";
 import { NOT_IMAGE } from "@utils/constants";
-import { isObject } from "@utils/type-guards";
 import LoadingDots from "@components/common/icons/LoadingDots";
 import { useFormStatus } from "react-dom";
 import { redirectToCheckout } from "@/utils/actions";
-import { EMAIL, getLocalStorage } from "@/store/local-storage";
 import Link from "next/link";
-import { createUrl, isCheckout, safeParse } from "@utils/helper";
+import { createUrl, safeParse } from "@utils/helper";
 import { useBodyScrollLock } from "@utils/hooks/useBodyScrollLock";
-import { useSyncExternalStore } from "react";
-import { useAddressesFromApi } from "@utils/hooks/getAddress";
+
+
 
 type MerchandiseSearchParams = {
   [key: string]: string;
@@ -51,19 +49,13 @@ export default function CartModal({
   const finalOnOpen = isControlled ? onOpen : internalOnOpen;
   const finalOnClose = isControlled ? onClose : internalOnClose;
 
-  const { isLoading } = useCartDetail();
-  const cartDetail = useAppSelector((state) => state.cartDetail);
-  const { billingAddress } = useAddressesFromApi(false);
-  const cart = Array.isArray(cartDetail?.cart?.items?.edges)
-    ? cartDetail?.cart?.items?.edges
+  const { isLoading, cartData: cartDetail } = useCartDetail();
+  // const cartDetail = useAppSelector((state) => state.cartDetail);
+  const cart = Array.isArray(cartDetail?.items?.edges)
+    ? cartDetail?.items?.edges
     : [];
-  const cartObj: any = cartDetail?.cart ?? {};
 
-  const mounted = useSyncExternalStore(
-    () => () => { },
-    () => true,
-    () => false,
-  );
+ 
 
   useBodyScrollLock(finalIsOpen);
 
@@ -80,15 +72,15 @@ export default function CartModal({
         aria-label="Open cart"
         className={clsx(
           className,
-          mounted && isLoading ? "cursor-wait" : "cursor-pointer",
+          isLoading ? "cursor-wait" : "cursor-pointer",
         )}
-        disabled={mounted ? isLoading : false}
+        disabled={isLoading}
         onClick={finalOnOpen}
       >
         {children ? (
           children
         ) : (
-          <OpenCart quantity={cartDetail?.cart?.itemsQty} />
+          <OpenCart quantity={cartDetail?.itemsQty} />
         )}
       </button>
 
@@ -246,13 +238,13 @@ export default function CartModal({
                       </ul>
 
                       <div className="border-0 border-t border-solid border-neutral-200 dark:border-dark-grey py-4 text-sm text-neutral-500 dark:text-neutral-400">
-                        {(cartDetail as any)?.cart?.taxAmount > 0 && (
+                        {(cartDetail as any)?.taxAmount > 0 && (
                           <div className="mb-3 flex items-center justify-between">
                             <p className="text-base font-normal text-black/[60%] dark:text-white">
                               Taxes
                             </p>
                             <Price
-                              amount={(cartDetail as any)?.cart?.taxAmount}
+                              amount={(cartDetail as any)?.taxAmount}
                               className="text-right text-base font-medium text-black dark:text-white"
                               currencyCode={"USD"}
                             />
@@ -263,25 +255,14 @@ export default function CartModal({
                             Total
                           </p>
                           <Price
-                            amount={(cartDetail as any)?.cart?.grandTotal}
+                            amount={(cartDetail as any)?.grandTotal}
                             className="text-right text-base font-medium text-black dark:text-white"
                             currencyCode={"USD"}
                           />
                         </div>
 
                         <form action={redirectToCheckout}>
-                          <CheckoutButton
-                            cartDetails={cartObj?.items?.edges ?? []}
-                            isGuest={cartObj?.isGuest}
-                            isEmail={
-                              cartObj?.customerEmail ?? getLocalStorage(EMAIL)
-                            }
-                            isSelectShipping={
-                              cartObj?.selectedShippingRate != null
-                            }
-                            isSeclectAddress={isObject(billingAddress)}
-                            isSelectPayment={cartObj?.paymentMethod != null}
-                          />
+                          <CheckoutButton />
                         </form>
                       </div>
                     </div>
@@ -298,37 +279,16 @@ export default function CartModal({
   );
 }
 
-function CheckoutButton({
-  cartDetails,
-  isGuest,
-  isEmail,
-  isSeclectAddress,
-  isSelectShipping,
-  isSelectPayment,
-}: {
-  cartDetails: Array<any>;
-  isGuest: boolean;
-  isEmail: string;
-  isSeclectAddress: boolean;
-  isSelectShipping: boolean;
-  isSelectPayment: boolean;
-}) {
+function CheckoutButton() {
   const { pending } = useFormStatus();
-  const email = isEmail;
+
 
   return (
     <>
       <input
         name="url"
         type="hidden"
-        value={isCheckout(
-          cartDetails,
-          isGuest,
-          email,
-          isSeclectAddress,
-          isSelectShipping,
-          isSelectPayment,
-        )}
+        value="/checkout"
       />
       <button
         className={clsx(

+ 1 - 1
src/components/checkout/index.tsx

@@ -17,7 +17,7 @@ interface CheckOutProps {
 
 const CheckOut = ({ step }: CheckOutProps) => {
   const { isLoading, getCartDetail } = useCartDetail();
-  const { billingAddress, shippingAddress } = useAddressesFromApi(true);
+  const { billingAddress, shippingAddress } = useAddressesFromApi();
   const cartDetail = useAppSelector((state) => state.cartDetail);
   const cartItems = cartDetail?.cart;
   const selectedShippingRate = cartItems?.selectedShippingRate;

+ 10 - 10
src/components/checkout/stepper/GuestAddAdressForm.tsx

@@ -10,7 +10,7 @@ import InputText from "@components/common/form/Input";
 import { ProceedToCheckout } from "./ProceedToCheckout";
 import CheckBox from "@components/theme/ui/element/Checkbox";
 import { useDispatch } from "react-redux";
-import { setCheckoutAddresses } from "@/store/slices/cart-slice";
+import { updateCart } from "@/store/slices/cart-slice";
 
 
 export const GuestAddAdressForm: FC<{
@@ -161,15 +161,15 @@ export const GuestAddAdressForm: FC<{
       try {
         await saveCheckoutAddress(payload as any);
         dispatch(
-          setCheckoutAddresses({
-            billing: {
-              ...billing,
-              email: billing.email ?? email ?? "",
-            },
-            shipping: {
-              ...shippingSource,
-              email: shippingSource.email ?? email ?? "",
-            },
+          updateCart({
+              billingAddress: {
+                ...billing,
+                email: billing.email ?? email ?? "",
+              },
+              shippingAddress: {
+                ...shippingSource,
+                email: shippingSource.email ?? email ?? "",
+              },
           })
         );
         setBillingAddress({

+ 1 - 1
src/components/common/AddToCartModal/FooterBtnInAddToCartModal.tsx

@@ -14,7 +14,7 @@ export default function FooterBtnInAddToCartModal({
     onAddToCart: () => void
 }) {
 
-    const btnClass = 'flex-initial flex items-center justify-center w-40 h-11.5 rounded-4xl text-ly-16 font-bold text-white';
+    const btnClass = 'flex-none flex items-center justify-center w-40 h-11.5 rounded-4xl text-ly-16 font-bold text-white';
     return (
         <div className="flex justify-between">
             {isAvailable ? 

+ 67 - 0
src/components/common/ConfirmModal.tsx

@@ -0,0 +1,67 @@
+"use client";
+
+import {useState} from "react";
+import { AnimatePresence , motion } from "framer-motion";
+
+export interface ConfirmModalProps { 
+    isShow: boolean;
+    title: string; 
+    content: string;
+    okHandler?: () => void; 
+    cancelHandler?: () => void;
+    noOk?: boolean; 
+    noCancel?: boolean; 
+}
+
+export function ConfirmModal({
+    isShow,
+    title,
+    content,
+    okHandler,
+    cancelHandler,
+    noOk,
+    noCancel,
+}: ConfirmModalProps) {
+
+    const close = (result: boolean) => {
+        if(result) {
+            okHandler && okHandler();
+        } else { 
+            cancelHandler && cancelHandler();
+        }
+    };
+
+  return (
+    <AnimatePresence >
+        {isShow && 
+            <motion.div
+                layout
+                initial={{ opacity: 0, }}
+                animate={{ opacity: 1 }}
+                exit={{ opacity: 0 }}
+                className="fixed z-1000 flex justify-center items-center bg-black/50 inset-0"
+                onClick={(e) => e.stopPropagation()}
+            >
+                {/* <div className="fixed z-1000 flex justify-center items-center bg-black/40 inset-0"> */}
+                    <div className="w-4/5 p-4 bg-white rounded-lg">
+                        <h3 className="text-center text-ly-18 font-bold mb-6">{title}</h3>
+                        <p className="text-ly-14 font-medium">{content}</p>
+                        <div className="w-full flex justify-end gap-3 mt-6">
+                            {!noOk && 
+                                <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-green text-ly-12 font-medium text-white" 
+                                    onClick={() => close(true)}
+                                >OK</button>
+                            }
+                            {!noCancel && 
+                                <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-gray text-ly-12 font-medium" 
+                                    onClick={() => close(false)}
+                                >Cancel</button>}
+                        </div>
+                    </div>
+                {/* </div> */}
+            </motion.div>
+        }
+    </AnimatePresence>
+  );
+}
+

+ 74 - 0
src/components/common/CountDown.tsx

@@ -0,0 +1,74 @@
+"use client";
+
+import { useEffect, useState, useRef, useCallback } from "react";
+
+
+export default function CountDown({
+    endTime,
+    boxClassName,
+    timerClassName,
+    onCountDownEnd
+}: {
+    endTime: number;
+    boxClassName?: string;
+    timerClassName?: string;
+    onCountDownEnd: () => void;
+}) { 
+    const defaultBoxClassName = boxClassName || "flex items-center gap-2 flex-none text-ly-14 text-white";
+    const defaultTimerClassName = timerClassName || "flex items-center justify-center w-6 h-6 bg-white rounded-xs text-ly-12 font-medium text-black";
+    
+    const timerRef = useRef<number | null>(null);
+    const [seconds, setSeconds] = useState(() => {
+        return Math.max(0, Math.floor((endTime - Date.now()) / 1000));
+    }); // 倒计时的总秒数
+
+    const clearTimer = () => {
+        if (timerRef.current !== null) {
+            clearInterval(timerRef.current);
+            timerRef.current = null;
+        }
+    };
+
+    const start = useCallback(() => {
+        if (timerRef.current) return;
+
+        timerRef.current = window.setInterval(() => {
+            setSeconds((prev) => {
+                if (prev <= 1) {
+                clearTimer();
+                onCountDownEnd();
+                return 0;
+                }
+                return prev - 1;
+            });
+        }, 1000);
+    }, [onCountDownEnd]);
+
+    const format = (n: number) => {
+        const h = Math.floor(n / 3600);
+        const m = Math.floor((n % 3600) / 60);
+        const s = n % 60;
+        return {
+            hour: String(h).padStart(2, "0"),
+            minute: String(m).padStart(2, "0"),
+            second: String(s).padStart(2, "0"),
+        }
+    };
+
+    useEffect(() => {
+        start();
+        return () => clearTimer();
+    }, [start]);
+
+    const { hour, minute, second } = format(seconds);
+
+    return (
+        <div className={defaultBoxClassName}>
+            <span className={defaultTimerClassName}>{hour}</span>
+            :
+            <span className={defaultTimerClassName}>{minute}</span>
+            :
+            <span className={defaultTimerClassName}>{second}</span>
+        </div>
+    );
+}

+ 1 - 1
src/components/layout/navbar/MobileMenuTrigger.tsx

@@ -11,7 +11,7 @@ export default function MobileMenuTrigger({ menuData }: { menuData: any[] }) {
   return (
     <>
       {/* 汉堡按钮 */}
-      <button className="flex-initial w-6 h-6" onClick={toggleMenu}>
+      <button className="flex-none w-6 h-6" onClick={toggleMenu}>
         <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
           <rect x="0" y="0" width="24" height="24" fill="#FFFFFF" fillOpacity="0" />
           <path stroke="currentColor" strokeWidth="1.5" d="M4 5L20 5" />

+ 1 - 1
src/components/layout/navbar/index.tsx

@@ -55,7 +55,7 @@ export default async function Navbar() {
                 <MobileMenuTrigger menuData={menuData} />
               </Suspense>
               <Link
-                className="flex-initial h-9 w-36.5"
+                className="flex-none h-9 w-36.5"
                 href="/"
                 aria-label="Go to homepage"
               >

+ 7 - 5
src/components/theme/ui/CommonModal.tsx

@@ -21,9 +21,9 @@ export default function CommonModal({
     contentClassName?: string;
     clickOutsideClose?: boolean;
     onClose: (e: boolean) => void;
-    header: React.ReactNode;
+    header?: React.ReactNode;
     body: React.ReactNode;
-    footer: React.ReactNode;
+    footer?: React.ReactNode;
 }) {
     // show - 显示
     // outing - 隐藏动画中
@@ -114,7 +114,7 @@ export default function CommonModal({
                     }}
                 >
                     <div ref={headerRef} className="w-full box-border absolute left-0 top-0">
-                        {header}
+                        {header && header}
                     </div>
                     
                     <div className="w-full box-border h-full overflow-y-auto relative">
@@ -123,8 +123,10 @@ export default function CommonModal({
                         
                     </div>
 
-                    <div ref={footerRef} className="w-full box-border absolute bottom-0 left-0 border-t border-gray-200">
-                        {footer}
+                    <div ref={footerRef} className={clsx("w-full box-border absolute bottom-0 left-0",{
+                        "border-t border-gray-200": footer !==null && footer !== undefined
+                    })}>
+                        {footer && footer}
                     </div>
                 </div>
                 

+ 0 - 3
src/components/theme/ui/InputText.tsx

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

+ 0 - 3
src/components/theme/ui/Select.tsx

@@ -6,7 +6,6 @@ interface Option {
     id: string;
 }
 export default function Select({
-    defaultValue,
     placeholder,
     options,
     onChange,
@@ -15,7 +14,6 @@ export default function Select({
     error,
     ref
 }: {
-    defaultValue?: string;
     placeholder?: string;
     options: Option[];
     onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
@@ -28,7 +26,6 @@ export default function Select({
     return (
         <div className="w-full relative">
             <select className="appearance-none ly-input"
-                defaultValue={defaultValue}
                 onChange={onChange}
                 onBlur={onBlur}
                 name={name}

+ 22 - 20
src/components/theme/ui/kernel/confirm/host.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { motion } from "framer-motion";
+import { AnimatePresence, motion } from "framer-motion";
 import { confirmStore } from "./store";
 import { useConfirmQueue } from "./useConfirm";
 
@@ -9,7 +9,7 @@ export function ConfirmHost() {
 
   const current = queue[0];
 
-  if (!current) return null;
+//   if (!current) return null;
 
   const close = (result: boolean) => {
     current.resolve(result);
@@ -20,25 +20,27 @@ export function ConfirmHost() {
   };
 
   return (
-    <motion.div
-      layout
-      initial={{ opacity: 0, }}
-      animate={{ opacity: 1 }}
-      exit={{ opacity: 0 }}
-      className="fixed z-1000 flex justify-center items-center bg-black/50 inset-0"
-      onClick={(e) => e.stopPropagation()}
-    >
-        {/* <div className="fixed z-1000 flex justify-center items-center bg-black/40 inset-0"> */}
-            <div className="w-4/5 p-4 bg-white rounded-lg">
-                <h3 className="text-center text-ly-18 font-bold mb-6">{current.title}</h3>
-                <p className="text-ly-14 font-medium">{current.content}</p>
-                <div className="w-full flex justify-end gap-3 mt-6">
-                    {!current.noOk && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-green text-ly-12 font-medium text-white" onClick={() => close(true)}>OK</button>}
-                    {!current.noCancel && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-gray text-ly-12 font-medium" onClick={() => close(false)}>Cancel</button>}
+    <AnimatePresence>
+        {current && <motion.div
+        layout
+        initial={{ opacity: 0, }}
+        animate={{ opacity: 1 }}
+        exit={{ opacity: 0 }}
+        className="fixed z-1000 flex justify-center items-center bg-black/50 inset-0"
+        onClick={(e) => e.stopPropagation()}
+        >
+            {/* <div className="fixed z-1000 flex justify-center items-center bg-black/40 inset-0"> */}
+                <div className="w-4/5 p-4 bg-white rounded-lg">
+                    <h3 className="text-center text-ly-18 font-bold mb-6">{current.title}</h3>
+                    <p className="text-ly-14 font-medium">{current.content}</p>
+                    <div className="w-full flex justify-end gap-3 mt-6">
+                        {!current.noOk && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-green text-ly-12 font-medium text-white" onClick={() => close(true)}>OK</button>}
+                        {!current.noCancel && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-gray text-ly-12 font-medium" onClick={() => close(false)}>Cancel</button>}
+                    </div>
                 </div>
-            </div>
-        {/* </div> */}
-    </motion.div>
+            {/* </div> */}
+        </motion.div>}
+    </AnimatePresence>
   );
 }
 

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

@@ -63,6 +63,8 @@ export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData> = gql`
         paymentMethod
         paymentMethodTitle
         haveStockableItems
+        billingAddress
+        shippingAddress
         success
         message
       }

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

@@ -55,6 +55,8 @@ export const CREATE_MERGE_CART: TypedDocumentNode<CreateMergeCartData> = gql`
         paymentMethod
         paymentMethodTitle
         haveStockableItems
+        billingAddress
+        shippingAddress
       }
     }
   }

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

@@ -49,6 +49,8 @@ export const GET_CART_ITEM: TypedDocumentNode<GetCartItemData> = gql`
         paymentMethod
         paymentMethodTitle
         haveStockableItems
+        billingAddress
+        shippingAddress
       }
     }
   }

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

@@ -49,6 +49,8 @@ export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData> = gql`
         paymentMethod
         paymentMethodTitle
         haveStockableItems
+        billingAddress
+        shippingAddress
       }
     }
   }

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

@@ -58,6 +58,8 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData> = gql
         paymentMethod
         paymentMethodTitle
         haveStockableItems
+        billingAddress
+        shippingAddress
       }
     }
   }

+ 22 - 0
src/graphql/checkout/mutations/CreateCancelOrder.ts

@@ -0,0 +1,22 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { CreateCancelOrderData } from "@/types/checkout/type";
+
+export const CREATE_CANCEL_ORDER: TypedDocumentNode<CreateCancelOrderData> = gql`
+  mutation CreateCancelOrder(
+      $orderId: Int
+  ) {
+      createCancelOrder(
+        input: {
+          orderId: $orderId
+        }
+      ) {
+          cancelOrder {
+            success
+            message
+            orderId
+            status
+            id
+          }
+      }
+  }
+`;

+ 31 - 3
src/graphql/checkout/mutations/CreateCheckoutOrder.ts

@@ -1,6 +1,33 @@
-import { gql } from "@apollo/client";
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { CreateCheckoutOrderData, CreatePaymentInitiateData } from "@/types/checkout/type";
 
-export const CREATE_CHECKOUT_ORDER = gql`
+export const CREATE_PAYMENT_INITIATE: TypedDocumentNode<CreatePaymentInitiateData> = gql`
+  mutation CreatePaymentInitiate(
+      $expressCheckout: Boolean
+      $paymentMethod: String
+  ) {
+      createPaymentInitiate(
+        input: {
+          expressCheckout: $expressCheckout
+          paymentMethod: $paymentMethod
+        }
+      ) {
+          paymentInitiate {
+            id
+            success
+            message
+            orderId
+            status 
+            gatewayOrderId 
+            newCartToken 
+            express 
+          }
+      }
+  }
+`;
+
+// 这个不用了
+export const CREATE_CHECKOUT_ORDER: TypedDocumentNode<CreateCheckoutOrderData> = gql`
   mutation CreateCheckoutOrder {
     createCheckoutOrder(
       input: {}
@@ -10,7 +37,8 @@ export const CREATE_CHECKOUT_ORDER = gql`
         orderId
         message
         success
+        paymentTransactionId
       }
     }
   }
-`;
+`;

+ 70 - 0
src/graphql/checkout/mutations/CreatePaymentCallback.ts

@@ -0,0 +1,70 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { CreatePaymentCallbackData } from "@/types/checkout/type";
+
+export const CREATE_PAYMENT_CALLBACK: TypedDocumentNode<CreatePaymentCallbackData> = gql`
+  mutation CreatePaymentCallback(
+      $orderId: Int
+      $gatewayOrderId: String
+      $status: String
+      $paymentSignature: String
+      
+      $shippingFirstName: String
+      $shippingLastName: String
+      $shippingEmail: String
+      $shippingAddress: String
+      $shippingCountry: String
+      $shippingState: String
+      $shippingCity: String
+      $shippingPostcode: String
+      $shippingPhoneNumber: String
+
+      $billingFirstName: String
+      $billingLastName: String
+      $billingEmail: String
+      $billingAddress: String
+      $billingCountry: String
+      $billingState: String
+      $billingCity: String
+      $billingPostcode: String
+      $billingPhoneNumber: String
+  ) {
+      createPaymentCallback(
+        input: {
+          orderId: $orderId
+          gatewayOrderId: $gatewayOrderId
+          status: $status
+          paymentSignature: $paymentSignature
+          
+          shippingFirstName: $shippingFirstName
+          shippingLastName: $shippingLastName
+          shippingEmail: $shippingEmail
+          shippingAddress: $shippingAddress
+          shippingCountry: $shippingCountry
+          shippingState: $shippingState
+          shippingCity: $shippingCity
+          shippingPostcode: $shippingPostcode
+          shippingPhoneNumber: $shippingPhoneNumber
+
+          billingFirstName: $billingFirstName
+          billingLastName: $billingLastName
+          billingEmail: $billingEmail
+          billingAddress: $billingAddress
+          billingCountry: $billingCountry
+          billingState: $billingState
+          billingCity: $billingCity
+          billingPostcode: $billingPostcode
+          billingPhoneNumber: $billingPhoneNumber
+        }
+      ) {
+          paymentCallback {
+            id
+            success
+            message
+            orderId
+            status
+            orderStatus
+            gatewayStatus
+          }
+      }
+  }
+`;

+ 25 - 0
src/graphql/checkout/mutations/CreatePaymentReplay.ts

@@ -0,0 +1,25 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { CreatePaymentReplayData } from "@/types/checkout/type";
+
+export const CREATE_PAYMENT_REPLAY: TypedDocumentNode<CreatePaymentReplayData> = gql`
+  mutation CreatePaymentReplay(
+      $orderId: Int
+      $paymentMethod: String
+  ) {
+      createPaymentReplay(
+        input: {
+          orderId: $orderId
+          paymentMethod: $paymentMethod
+        }
+      ) {
+          paymentReplay{
+            id
+            success
+            message
+            orderId
+            status
+            gatewayOrderId
+          }
+      }
+  }
+`;

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

@@ -0,0 +1,67 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { SaveCheckoutCartData } from "@/types/checkout/type";
+
+export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData> = gql`
+    mutation CreateSaveCheckoutCart(
+        $shippingMethod: String
+        $paymentMethod: String
+        $couponCode: String
+    ) {
+      createSaveCheckoutCart(
+        input: {
+            shippingMethod: $shippingMethod
+            paymentMethod: $paymentMethod
+            couponCode: $couponCode
+        }
+        ) {
+            saveCheckoutCart {
+                id
+                appliedTaxes
+                itemsQty
+                isGuest
+                itemsCount
+                items {
+                    edges {
+                        node {
+                            id
+                            cartId
+                            productId
+                            name
+                            price
+                            baseImage
+                            sku
+                            quantity
+                            type
+                            productUrlKey
+                            canChangeQty
+                        }
+                    }
+                }
+                subtotal
+                subTotalInclTax
+                discountAmount
+                taxAmount
+                taxTotal
+                shippingAmount
+                shippingAmountInclTax
+                grandTotal
+                formattedSubtotal
+                formattedSubTotalInclTax
+                formattedDiscountAmount
+                formattedTaxAmount
+                formattedTaxTotal
+                formattedShippingAmount
+                formattedShippingAmountInclTax
+                formattedGrandTotal
+                couponCode
+                selectedShippingRate
+                selectedShippingRateTitle
+                paymentMethod
+                paymentMethodTitle
+                haveStockableItems
+                billingAddress
+                shippingAddress
+            }
+        }
+    }
+`;

+ 6 - 2
src/graphql/checkout/mutations/index.ts

@@ -1,4 +1,8 @@
 export {CREATE_CHECKOUT_ADDRESS} from "./CreateCheckoutAddress" ;
-export {CREATE_CHECKOUT_ORDER} from "./CreateCheckoutOrder" ;
+export {CREATE_CHECKOUT_ORDER,CREATE_PAYMENT_INITIATE} from "./CreateCheckoutOrder" ;
 export {CREATE_CHECKOUT_PAYMENT_METHODS} from "./CreateCheckoutPaymentMethod";
-export {CREATE_CHECKOUT_SHIPPING_METHODS} from "./CreateCheckoutShippingMethod";
+export {CREATE_CHECKOUT_SHIPPING_METHODS} from "./CreateCheckoutShippingMethod";
+export {CREATE_PAYMENT_CALLBACK} from "./CreatePaymentCallback";
+export {CREATE_CANCEL_ORDER} from "./CreateCancelOrder";
+export {CREATE_SAVE_CHECKOUT_CART} from "./CreateSaveCheckoutCart";
+export {CREATE_PAYMENT_REPLAY} from "./CreatePaymentReplay";

+ 19 - 0
src/graphql/currency/query/GetCurrencies.ts

@@ -0,0 +1,19 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { CurrenciesData } from "@/types/currency/type";
+
+
+export const GET_CURRENCIES: TypedDocumentNode<CurrenciesData> = gql`
+  query allCurrency {
+  currencies {
+    edges {
+      node {
+        id
+        _id
+        code
+        name
+        symbol
+      }
+    }
+  }
+}
+`;

+ 3 - 0
src/graphql/currency/query/index.ts

@@ -0,0 +1,3 @@
+export {
+    GET_CURRENCIES
+} from "./GetCurrencies";

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

@@ -0,0 +1,90 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { OrderDetailsData } from "@/types/customer/type";
+
+
+export const GET_ORDER_DETAILS: TypedDocumentNode<OrderDetailsData> = gql`
+  query GetCustomerOrder($id: ID!) {
+  customerOrder(id: $id) { 
+    incrementId
+    status
+    channelName
+    customerEmail
+    customerFirstName
+    customerLastName
+    payment {
+      method
+      methodTitle
+    }
+    shippingMethod
+    shippingTitle
+    couponCode
+    totalItemCount
+    totalQtyOrdered
+    grandTotal
+    baseGrandTotal
+    grandTotalInvoiced
+    grandTotalRefunded
+    subTotal
+    baseSubTotal
+    taxAmount
+    baseTaxAmount
+    discountAmount
+    baseDiscountAmount
+    shippingAmount
+    baseShippingAmount
+    baseCurrencyCode
+    channelCurrencyCode
+    orderCurrencyCode
+    items {
+      edges {
+        node {
+          id
+          sku
+          type
+          name
+          additional
+          qtyOrdered
+          qtyShipped
+          qtyInvoiced
+          qtyCanceled
+          qtyRefunded
+          price
+          basePrice
+          total
+          baseTotal
+          discountPercent
+          discountAmount
+          baseDiscountAmount
+        }
+      }
+    }
+    addresses {
+      edges {
+        node {
+          id
+          _id
+          addressType
+          orderId
+          name
+          firstName
+          lastName
+          companyName
+          address
+          city
+          state
+          country
+          postcode
+          useForShipping
+          email
+          phone
+          defaultAddress
+          createdAt
+          updatedAt
+        }
+      }
+    }
+    createdAt
+    updatedAt
+  }
+}
+`;

+ 3 - 0
src/graphql/customer/query/index.ts

@@ -0,0 +1,3 @@
+export {
+    GET_ORDER_DETAILS
+} from "./GetOrderDetails";

+ 2 - 0
src/graphql/index.ts

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

+ 28 - 0
src/lib/ApolloErrorHandler.ts

@@ -0,0 +1,28 @@
+import {
+  CombinedGraphQLErrors,
+  CombinedProtocolErrors,
+  LocalStateError,
+  ServerError,
+  ServerParseError,
+  UnconventionalError,
+} from "@apollo/client/errors";
+
+// about apollo client error handle https://www.apollographql.com/docs/react/data/error-handling
+// Comprehensive error handling example.
+function handleError(error: unknown) {
+  if (CombinedGraphQLErrors.is(error)) {
+    // Handle GraphQL errors
+  } else if (CombinedProtocolErrors.is(error)) {
+    // Handle multipart subscription protocol errors
+  } else if (LocalStateError.is(error)) {
+    // Handle errors thrown by the `LocalState` class
+  } else if (ServerError.is(error)) {
+    // Handle server HTTP errors
+  } else if (ServerParseError.is(error)) {
+    // Handle JSON parse errors
+  } else if (UnconventionalError.is(error)) {
+    // Handle errors thrown by irregular types
+  } else {
+    // Handle other errors
+  }
+}

+ 58 - 8
src/lib/graphql-fetch.ts

@@ -4,6 +4,7 @@ import {
   type OperationVariables,
   ApolloClient
 } from "@apollo/client";
+import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
 import {getClient} from "@/lib/ApolloClientServer";
 import {IS_GUEST,GUEST_CART_TOKEN} from "@/utils/constants";
 import { decodeJWT } from "@/utils/jwt-cookie";
@@ -17,11 +18,6 @@ declare module "@apollo/client" {
 */
 // Comprehensive error handling example. https://www.apollographql.com/docs/react/data/error-handling
 
-export interface GraphqlRequestResult<TData = unknown> {
-  data: TData | null;
-  error: string | undefined;
-}
-
 export type CacheLifePreset =
   | "seconds"
   | "minutes"
@@ -180,8 +176,62 @@ export async function graphqlRequestNoCache<
   );
 }
 
+export async function authorizationGraphqlMutate<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+    mutation: DocumentNode,
+    variables?: TVariables,
+    operationName?: string
+): Promise<GraphqlRequestResult<TData>> {
+    const cookieStore = await cookies();
+    const isGuest = cookieStore.get(IS_GUEST);
+    const authToken = cookieStore.get(GUEST_CART_TOKEN);
+    let token = '';
+    if(!authToken) {
+        return {data: null, error: 'Authorization token not found!'};
+    } 
+    if(isGuest?.value === 'false') { // 登录用户
+        token = authToken.value;
+    } else {
+        // 游客
+        const jwtRes = decodeJWT<{
+            sessionToken: string;
+            cartId: number;
+            isGuest: boolean;
+        }>(authToken.value, true);
+        token = jwtRes?.sessionToken || '';
+    }
+    const client = getClient();
+    try {
+        const mutateOption: ApolloClient.MutateOptions<TData> = {
+            mutation,
+            variables,
+            fetchPolicy: "no-cache",
+            context: {
+                headers: {
+                    "Authorization": "Bearer " + token,
+                },
+                fetchOptions: {
+                    cache: 'no-store',
+                },
+            }
+        };
+        const result: ApolloClient.MutateResult<TData> = await client.mutate(mutateOption);
+        console.log('authorizationGraphqlMutate result ---- ', result);
+        const resData = result.data || null;
+        return {data: resData, error: result.error? result.error.message : '' };
+    } catch (error: any) {
+        console.log('authorizationGraphqlMutate error ---- ', error.message);
+        if(operationName === 'GetCartItem' && error.message === 'Cart not found') {
+          return { data: null, error: 'Cart not found' };
+        }
+        throw error;
+    }
+    
+}
 
-export async function authorizationGraphqlRequest<
+export async function authorizationGraphqlQuery<
   TData = unknown,
   TVariables extends OperationVariables = OperationVariables
 >(
@@ -222,7 +272,7 @@ export async function authorizationGraphqlRequest<
             }
         };
         const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
-        console.log('authorizationGraphqlRequest result ---- ', result);
+        console.log('authorizationGraphqlQuery result ---- ', result);
         const resData = result.data || null;
         return {data: resData, error: result.error? result.error.message : '' };
     } catch (error) {
@@ -253,7 +303,7 @@ export async function fullCacheGraphqlRequest<
             }
         };
         const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
-        console.log('authorizationGraphqlRequest result ---- ', result);
+        console.log('fullCacheGraphqlRequest result ---- ', result);
         const resData = result.data || null;
         return {data: resData, error: result.error? result.error.message : '' };
     } catch (error) {

+ 0 - 3
src/providers/GlobalProviders.tsx

@@ -2,20 +2,17 @@
 
 import { ReactNode } from "react";
 import { ThemeProvider } from "./ThemeProvider";
-import { ReduxProvider } from "./ReduxProvider";
 import { ToastProvider } from "./ToastProvider";
 import { ApolloWrapper } from "./ApolloWrapper";
 
 export function GlobalProviders({ children }: { children: ReactNode }) {
   return (
     <ThemeProvider>
-      <ReduxProvider>
         <ToastProvider>
           <ApolloWrapper>
             {children}
           </ApolloWrapper>
         </ToastProvider>
-      </ReduxProvider>
     </ThemeProvider>
   );
 }

+ 21 - 3
src/providers/ReduxProvider.tsx

@@ -2,8 +2,26 @@
 
 import { Provider } from "react-redux";
 
-import { store } from "@/store/store";
+// import { store } from "@/store/store";
 
-export const ReduxProvider = ({ children }: { children: React.ReactNode }) => {
-  return <Provider store={store}>{children}</Provider>;
+import { useRef } from 'react'
+
+import { makeStore, AppStore,PreloadedStateType } from '@/store/store'
+
+export const ReduxProvider = ({ 
+  children,
+  preloadedState
+}: { 
+  children: React.ReactNode,
+  preloadedState: PreloadedStateType
+}) => {
+  // return <Provider store={store}>{children}</Provider>;
+
+  const storeRef = useRef<AppStore>(undefined)
+  if (!storeRef.current) {
+    // Create the store instance the first time this renders
+    storeRef.current = makeStore(preloadedState)
+  }
+
+  return <Provider store={storeRef.current}>{children}</Provider>
 }

+ 14 - 6
src/store/hooks.ts

@@ -1,9 +1,17 @@
-import type { AppDispatch, RootState } from "./store";
+// import type { AppDispatch, RootState } from "./store";
 
-import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
+// import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
 
-// Create a typed version of useDispatch
-export const useAppDispatch = () => useDispatch<AppDispatch>();
+// // Create a typed version of useDispatch
+// export const useAppDispatch = () => useDispatch<AppDispatch>();
 
-// Create a typed version of useSelector
-export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+// // Create a typed version of useSelector
+// export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+
+import { useDispatch, useSelector, useStore } from 'react-redux'
+import type { RootState, AppDispatch, AppStore } from './store'
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
+export const useAppSelector = useSelector.withTypes<RootState>()
+export const useAppStore = useStore.withTypes<AppStore>()

+ 54 - 56
src/store/slices/cart-slice.ts

@@ -1,21 +1,44 @@
-import { AddressDataTypes } from "@/types/types";
-import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+import makeClient from "@/lib/ApolloClientBrowser";
+import { PayloadAction, createSlice, createAsyncThunk } from "@reduxjs/toolkit";
 import {CartDetail} from "@/types/cart/type";
-import { 
-    ShipAddressFormData,
-    BillAddressFormData,
-} from "@/types/checkout/type";
+import { GET_CART_ITEM } from "@/graphql";
 
-interface CartState {
-  cart?: CartDetail;
-  billingAddress: AddressDataTypes | null;
-  shippingAddress: AddressDataTypes | null;
+import { formatCartDetail } from "@/utils/cartDetailTools";
+export interface CartState {
+  cart: CartDetail | null;
+  loading: boolean;
+  error: string | null;
 }
 
+// 定义异步 action
+export const fetchCartDetail = createAsyncThunk(
+  'cartDetail/fetchCartDetail',
+  async (_, { rejectWithValue }) => { //_ 占位,表示不需要参数
+    try {
+      const apolloClient = makeClient();
+      const res = await apolloClient.mutate({
+        mutation: GET_CART_ITEM,
+        fetchPolicy: 'network-only', // 按需调整
+      });
+      const responseData = res.data?.createReadCart?.readCart;
+      return responseData ? formatCartDetail(responseData) : null;
+    } catch (error) {
+      let errorMessage: string;
+      if (error instanceof Error) {
+        errorMessage = error.message;
+      } else {
+        errorMessage = String(error);
+      }
+      return rejectWithValue(errorMessage);
+    }
+  }
+);
+
+
 const initialState: CartState = {
-  cart: undefined,
-  billingAddress: null,
-  shippingAddress: null,
+  cart: null,
+  loading: false,
+  error: null,
 };
 
 const cartSlice = createSlice({
@@ -31,58 +54,33 @@ const cartSlice = createSlice({
       }
     },
     clearCart(state) {
-      state.cart = undefined;
-      state.billingAddress = null;
-      state.shippingAddress = null;
-    },
-
-    setBillingAddress(state, action: PayloadAction<AddressDataTypes>) {
-      state.billingAddress = action.payload;
-    },
-    clearBillingAddress(state) {
-      state.billingAddress = null;
-    },
-
-    setShippingAddress(state, action: PayloadAction<AddressDataTypes>) {
-      state.shippingAddress = action.payload;
-    },
-    clearShippingAddress(state) {
-      state.shippingAddress = null;
-    },
-
-    setCheckoutAddresses(
-      state,
-      action: PayloadAction<{
-        billing: AddressDataTypes;
-        shipping: AddressDataTypes;
-      }>
-    ) {
-      state.billingAddress = action.payload.billing;
-      state.shippingAddress = action.payload.shipping;
-    },
-
-    resetAddressStep(_state) {
-      // Intentionally kept empty to retain previous selections while allowing re-entry to the step
-    },
-
-    resetShippingStep(_state) {
-      // Intentionally kept empty to retain previous selections while allowing re-entry to the step
+      state.cart = null;
+      state.loading = false;
+      state.error = null;
     },
 
   },
+  extraReducers: (builder) => {
+    builder
+      .addCase(fetchCartDetail.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(fetchCartDetail.fulfilled, (state, action) => {
+        state.loading = false;
+        state.cart = action.payload;
+      })
+      .addCase(fetchCartDetail.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.payload as string;
+      });
+  },
 });
 
 export const {
   addItem,
   updateCart,
   clearCart,
-  setBillingAddress,
-  clearBillingAddress,
-  setShippingAddress,
-  clearShippingAddress,
-  setCheckoutAddresses,
-  resetAddressStep,
-  resetShippingStep,
 } = cartSlice.actions;
 
 export default cartSlice.reducer;

+ 76 - 0
src/store/slices/currency-slice.ts

@@ -0,0 +1,76 @@
+import makeClient from "@/lib/ApolloClientBrowser";
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import {CurrencyItem} from "@/types/currency/type";
+import { GET_CURRENCIES } from "@/graphql";
+
+interface CurrencyState {
+  currencies: CurrencyItem[]
+  loading: boolean;
+  error: string | null;
+}
+
+// 定义异步 action
+export const fetchCurrencyList = createAsyncThunk(
+  'currencyList/fetchCurrencyList',
+  async (_, { rejectWithValue }) => { //_ 占位,表示不需要参数
+    try {
+      const apolloClient = makeClient();
+      //@TODO 后期要改成缓存
+      const res = await apolloClient.query({
+        query: GET_CURRENCIES,
+        fetchPolicy: 'network-only', // 按需调整
+        context: {
+            fetchOptions: {
+                cache: 'no-store',
+            },
+
+        }
+      });
+      const responseData = res.data?.currencies?.edges;
+      if(responseData) {
+        return responseData.map((item) =>  item.node );
+      } else {
+        return [];
+      }
+      
+    } catch (error) {
+      let errorMessage: string;
+      if (error instanceof Error) {
+        errorMessage = error.message;
+      } else {
+        errorMessage = String(error);
+      }
+      return rejectWithValue(errorMessage);
+    }
+  }
+);
+
+
+const initialState: CurrencyState = {
+  currencies: [],
+  loading: false,
+  error: null,
+};
+
+const currencySlice = createSlice({
+  name: "currencyList",
+  initialState,
+  reducers: {},
+  extraReducers: (builder) => {
+    builder
+      .addCase(fetchCurrencyList.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(fetchCurrencyList.fulfilled, (state, action) => {
+        state.loading = false;
+        state.currencies = action.payload;
+      })
+      .addCase(fetchCurrencyList.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.payload as string;
+      });
+  },
+});
+
+export default currencySlice.reducer;

+ 33 - 10
src/store/store.ts

@@ -1,16 +1,39 @@
 import { configureStore } from "@reduxjs/toolkit";
 
-import cartSlice from "./slices/cart-slice";
+import cartSlice, {CartState} from "./slices/cart-slice";
 import userSlice from "./slices/user-slice";
 import addToCartDialogReducer from "./slices/addToCartDialogSlice";
+import currencySlice from "./slices/currency-slice";
+
+// export const store = configureStore({
+//   reducer: {
+//     cartDetail: cartSlice,
+//     user: userSlice,
+//     addToCartDialog: addToCartDialogReducer,
+//     currencyList: currencySlice
+//   },
+// });
+
+// export type RootState = ReturnType<typeof store.getState>;
+// export type AppDispatch = typeof store.dispatch;
+export interface PreloadedStateType { 
+  cartDetail: CartState;  
+}
+export const makeStore = (preloadedState: PreloadedStateType) => {
+    return configureStore({
+      reducer: {
+        cartDetail: cartSlice,
+        user: userSlice,
+        addToCartDialog: addToCartDialogReducer,
+        currencyList: currencySlice
+      },
+      preloadedState
+    });
+}
+// Infer the type of makeStore
+export type AppStore = ReturnType<typeof makeStore>
+// Infer the `RootState` and `AppDispatch` types from the store itself
+export type RootState = ReturnType<AppStore['getState']>
+export type AppDispatch = AppStore['dispatch']
 
-export const store = configureStore({
-  reducer: {
-    cartDetail: cartSlice,
-    user: userSlice,
-    addToCartDialog: addToCartDialogReducer,
-  },
-});
 
-export type RootState = ReturnType<typeof store.getState>;
-export type AppDispatch = typeof store.dispatch;

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

@@ -22,6 +22,19 @@ export interface CartItemEdge {
 export interface CartItemsConnection {
   edges: CartItemEdge[];
 } 
+
+export interface CartAddress{
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+  address: string;
+  city: string;
+  state: string;
+  country: string;
+  postcode: string;
+  phone: string;
+}
 // CartDetail
 export interface CartDetail {
   id: string;
@@ -57,6 +70,8 @@ export interface CartDetail {
   paymentMethodTitle: string | null;
 
   haveStockableItems: boolean;
+  billingAddress: CartAddress | null;
+  shippingAddress: CartAddress | null;
 }
 
 // createReadCart payload

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

@@ -1,3 +1,5 @@
+import {CartDetail} from "@/types/cart/type";
+
 export type AddressType = "cart_billing" | "cart_shipping";
 
 
@@ -179,6 +181,20 @@ export interface GetCheckoutShippingRatesOperation {
   variables: GetCheckoutShippingRatesVariables;
 }
 
+// save checkout cart
+export type SaveCheckoutCart = CartDetail;
+
+export interface SaveCheckoutCartPayload {
+  saveCheckoutCart: SaveCheckoutCart;
+}
+export interface SaveCheckoutCartData {
+  createSaveCheckoutCart: SaveCheckoutCartPayload;
+}
+export interface SaveCheckoutCartVariables {
+  shippingMethod: string;
+  paymentMethod: string;
+  couponCode: string;
+}
 // Checkout Place Order
 
 export interface CheckoutOrder {
@@ -186,6 +202,7 @@ export interface CheckoutOrder {
   orderId: string;
   message: string;
   success: boolean;
+  paymentTransactionId: string;
 }
 export interface CreateCheckoutOrderPayload {
   checkoutOrder: CheckoutOrder;
@@ -197,6 +214,94 @@ export interface CreateCheckoutOrderOperation {
   data: CreateCheckoutOrderData;
 }
 
+
+export interface CreatePaymentInitiate {
+  id: string;
+  success: boolean;
+  message: string;
+  orderId: string;
+  status: string;
+  gatewayOrderId: string;
+  newCartToken: string;
+  express: boolean;
+}
+
+export interface CreatePaymentInitiatePayload {
+  paymentInitiate: CreatePaymentInitiate;
+}
+export interface CreatePaymentInitiateData {
+  createPaymentInitiate: CreatePaymentInitiatePayload;
+}
+export interface CreatePaymentInitiateVariables {
+    expressCheckout?: boolean;
+    paymentMethod?: string;
+}
+
+// create payment callback
+export interface CreatePaymentCallback {
+  id: string;
+  success: boolean;
+  message: string;
+  orderId: string;
+  status: string;
+  orderStatus: string;
+  gatewayStatus: string;
+}
+
+export interface CreatePaymentCallbackPayload {
+  paymentCallback: CreatePaymentCallback;
+}
+export interface CreatePaymentCallbackData {
+  createPaymentCallback: CreatePaymentCallbackPayload;
+}
+export interface CreatePaymentCallbackVariables {
+  orderId: number;
+  gatewayOrderId: string;
+  status: "success" | "cancel" | "failure";
+  paymentSignature?: string;
+  
+  // 快捷支付需要
+  shippingFirstName?: string;
+  shippingLastName?: string;
+  shippingEmail?: string;
+  shippingAddress?: string;
+  shippingCountry?: string;
+  shippingState?: string;
+  shippingCity?: string;
+  shippingPostcode?: string;
+  shippingPhoneNumber?: string;
+
+  billingFirstName?: string;
+  billingLastName?: string;
+  billingEmail?: string;
+  billingAddress?: string;
+  billingCountry?: string;
+  billingState?: string;
+  billingCity?: string;
+  billingPostcode?: string;
+  billingPhoneNumber?: string;
+}
+
+// calcel order
+export interface CreateCancelOrder {
+  id: number;
+  success: boolean;
+  message: string;
+  orderId: number;
+  status: string;
+}
+
+export interface CreateCancelOrderPayload {
+  cancelOrder: CreateCancelOrder;
+}
+export interface CreateCancelOrderData {
+  createCancelOrder: CreateCancelOrderPayload;
+}
+export interface CreateCancelOrderVariables {
+  orderId: number;
+}
+
+
 // Checkout Get Address
 export interface CheckoutAddress {
   id: string;
@@ -334,4 +439,19 @@ export interface BillAddressFormData {
     billingCity: string,
     billingPostcode: string,
     billingPhoneNumber: string,
+}
+
+export interface CreatePaymentReplay{
+    id: number;
+    success: boolean;
+    message: string;
+    orderId: string;
+    status: string;
+    gatewayOrderId: string;
+}
+export interface CreatePaymentReplayPayload {
+  paymentReplay: CreatePaymentReplay;
+}
+export interface CreatePaymentReplayData {
+  createPaymentReplay: CreatePaymentReplayPayload;
 }

+ 15 - 0
src/types/currency/type.ts

@@ -0,0 +1,15 @@
+
+
+export interface CurrencyItem {
+    id: string;
+    _id: number;
+    code: string;
+    name: string;
+    symbol: string;
+}
+export interface CurrenciesList {
+    edges:{node:CurrencyItem}[];
+}
+export interface CurrenciesData {
+    currencies:CurrenciesList;
+}

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

@@ -0,0 +1,119 @@
+export interface ProductsInOrderDetails {
+    id: string;
+    sku: string;
+    type: string;
+    name: string;
+    additional: string;
+    qtyOrdered: number;
+    qtyShipped: number;
+    qtyInvoiced: number;
+    qtyCanceled: number;
+    qtyRefunded: number;
+    price: number;
+    basePrice: number;
+    total: number;
+    baseTotal: number;
+    discountPercent: number;
+    discountAmount: number;
+    baseDiscountAmount: number;
+}
+
+interface ItemsInOrderDetails { 
+    edges: {
+        node: ProductsInOrderDetails;
+    }[];
+}
+
+interface AddressInOrderDetails { 
+    id: string;
+    _id: number;
+    addressType: string;
+    orderId: number;
+    name: string;
+    firstName: string;
+    lastName: string;
+    companyName: string;
+    address: string;
+    city: string;
+    state: string;
+    country: string;
+    postcode: string;
+    useForShipping: '0' | '1'; // '0' | '1'
+    email: string;
+    phone: string;
+    defaultAddress: boolean;
+    createdAt: string;
+    updatedAt: string;
+}
+
+interface AddressListInOrderDetails { 
+    edges: {
+        node: AddressInOrderDetails;
+    }[];
+}
+interface OptionAttribute{
+    value_id: number;
+    option_id: number;
+    value_code: string;
+    option_code: string;
+    value_label: string;
+    option_label: string;
+    attribute_name: string;
+}
+export interface ProductItemAdditional { 
+    
+    locale: string;
+    cart_id: number;
+    quantity: number;
+    attributes: {
+        [key: string]: OptionAttribute;  
+    },
+    is_buy_now: number;
+    product_id: number;
+    variant_id: number;
+    variant_sku: string;
+    variant_name: string | null;
+
+}
+
+export interface OrderDetails {
+    incrementId: string;
+    status: string;
+    channelName: string;
+    customerEmail: string;
+    customerFirstName: string;
+    customerLastName: string;
+    shippingMethod: string;
+    shippingTitle: string;
+    payment: {
+        method: string;
+        methodTitle: string;
+    };
+    couponCode: string | null;
+    totalItemCount: number;
+    totalQtyOrdered: number;
+    grandTotal: number;
+    baseGrandTotal: number;
+    grandTotalInvoiced: number;
+    grandTotalRefunded: number;
+    subTotal: number;
+    baseSubTotal: number;
+    taxAmount: number;
+    baseTaxAmount: number;
+    discountAmount: number;
+    baseDiscountAmount: number;
+    shippingAmount: number;
+    baseShippingAmount: number;
+    baseCurrencyCode: string | null;
+    channelCurrencyCode: string | null;
+    orderCurrencyCode: string;
+    items: ItemsInOrderDetails;
+    addresses: AddressListInOrderDetails;
+    createdAt: string;
+    updatedAt: string;
+  
+}
+
+export interface OrderDetailsData {
+  customerOrder: OrderDetails;
+}

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

@@ -0,0 +1,3 @@
+
+
+export type {ProductsInOrderDetails,OrderDetails, OrderDetailsData, ProductItemAdditional} from "./order";

+ 4 - 0
src/types/graphqlFetch/type.ts

@@ -0,0 +1,4 @@
+export interface GraphqlRequestResult<TData = unknown> {
+  data: TData | null;
+  error: string | undefined;
+}

+ 2 - 2
src/utils/bagisto/index.ts

@@ -194,7 +194,7 @@ export async function bagistoFetch<T>({
     // let cc = await cookies();
     // console.log('bagistoFetch --- url:', GRAPHQL_URL,isCookies);
     // console.log('bagistoFetch --- queryString:', queryString);
-    // console.log('bagistoFetch --- baseHeaders:', baseHeaders);
+    console.log('bagistoFetch --- baseHeaders:', baseHeaders);
     // console.log('bagistoFetch --- cookies:', cc.getAll());
     // console.log('bagistoFetch --- variables:', variables);
     const result = await fetch(GRAPHQL_URL, {
@@ -212,7 +212,7 @@ export async function bagistoFetch<T>({
     });
 
     const body = await result.json();
-    // console.log('bagistoFetch --- body:',body);
+    console.log('bagistoFetch --- body:',body);
     if (body.errors) {
       if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
         return { status: result.status, body };

+ 38 - 1
src/utils/cartDetailTools.ts

@@ -1,4 +1,5 @@
-import { CartDetail } from "@/types/cart/type";
+import { CartDetail, CartAddress } from "@/types/cart/type";
+import {ShipAddressFormData,BillAddressFormData} from "@/types/checkout/type";
 export function formatCartDetail(cart:CartDetail): CartDetail { 
     return {
         id: cart.id,
@@ -33,5 +34,41 @@ export function formatCartDetail(cart:CartDetail): CartDetail {
         paymentMethodTitle: cart.paymentMethodTitle,
     
         haveStockableItems: cart.haveStockableItems,
+        billingAddress: cart.billingAddress,
+        shippingAddress: cart.shippingAddress
     };
+}
+
+export function shippingAddressToCartAddress(address:ShipAddressFormData ): CartAddress { 
+    const res: CartAddress = {
+        id: String(address.shippingAddressId),
+        firstName: address.shippingFirstName,
+        lastName: address.shippingLastName,
+        email: address.shippingEmail,
+        address: address.shippingAddress,
+        city: address.shippingCity,
+        state: address.shippingState,
+        country: address.shippingCountry,
+        postcode: address.shippingPostcode,
+        phone: address.shippingPhoneNumber,
+
+    }
+    return res;
+   
+}
+
+export function billingAddressToCartAddress(address: BillAddressFormData):CartAddress {
+    const res: CartAddress = {
+        id: String(address.billingAddressId),
+        firstName: address.billingFirstName,
+        lastName: address.billingLastName,
+        email: address.billingEmail,
+        address: address.billingAddress,
+        city: address.billingCity,
+        state: address.billingState,
+        country: address.billingCountry,
+        postcode: address.billingPostcode,
+        phone: address.billingPhoneNumber
+    }
+    return res;
 }

+ 8 - 10
src/utils/hooks/getAddress.ts

@@ -2,29 +2,27 @@
 
 import { CheckoutAddressNode, MappedCheckoutAddress } from "@/types/checkout/type";
 import { useAddress } from "./useAddress";
-import { useAppSelector } from "@/store/hooks";
 import { useEffect } from "react";
 import { AddressDataTypes } from "@/types/types";
 
-export const useAddressesFromApi = (autoFetch: boolean = false): {
+export const useAddressesFromApi = (): {
   billingAddress: MappedCheckoutAddress | null;
   shippingAddress: MappedCheckoutAddress | null;
   getAddresses: () => Promise<void>;
 } => {
   const { addresses: data, getAddresses } = useAddress();
-  const cartDetail = useAppSelector((state) => state.cartDetail);
+  
   const address = data;
 
-  const reduxBilling = cartDetail?.billingAddress;
-  const reduxShipping = cartDetail?.shippingAddress;
+
 
   useEffect(() => {
-    if (autoFetch && !address.length && !reduxBilling && !reduxShipping) {
+    if (!address.length) {
       getAddresses();
     }
-  }, [autoFetch, address.length, reduxBilling, reduxShipping, getAddresses]);
+  }, [address.length, getAddresses]);
 
-  if (!Array.isArray(address) && !reduxBilling && !reduxShipping)
+  if (!Array.isArray(address))
     return { billingAddress: null, shippingAddress: null, getAddresses };
 
   const billingNode = address.find((a: CheckoutAddressNode) => a?.addressType === "cart_billing");
@@ -50,8 +48,8 @@ export const useAddressesFromApi = (autoFetch: boolean = false): {
       : null;
 
   return {
-    billingAddress: reduxBilling ? mapNode(reduxBilling) : mapNode(billingNode),
-    shippingAddress: reduxShipping ? mapNode(reduxShipping) : mapNode(shippingNode),
+    billingAddress:  mapNode(billingNode),
+    shippingAddress: mapNode(shippingNode),
     getAddresses,
   };
 };

+ 2 - 1
src/utils/hooks/useCache.ts

@@ -1,5 +1,6 @@
 import { type DocumentNode, type OperationVariables } from "@apollo/client";
-import { graphqlRequest, type CacheLifeOption, GraphqlRequestResult } from "@/lib/graphql-fetch";
+import { graphqlRequest, type CacheLifeOption } from "@/lib/graphql-fetch";
+import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
 import { GET_FILTER_ATTRIBUTES } from "@/graphql";
 
 export interface PageCacheConfig {

+ 11 - 39
src/utils/hooks/useCartDetail.ts

@@ -1,64 +1,36 @@
 "use client";
 
-import { useMutation } from "@apollo/client/react";
 import { useAppDispatch, useAppSelector } from "@/store/hooks";
-import { addItem } from "@/store/slices/cart-slice";
-import { useCallback, useEffect, useState, useRef } from "react";
-import { GET_CART_ITEM } from "@/graphql";
+import { fetchCartDetail } from "@/store/slices/cart-slice";
+import { useCallback, useEffect } from "react";
 import { getCartToken } from "@/utils/getCartToken";
-import { formatCartDetail } from "@/utils/cartDetailTools";
 
 
 export function useCartDetail() {
   const dispatch = useAppDispatch();
-  const cart = useAppSelector((state) => state.cartDetail.cart);
-  const [isInFlight, setIsInFlight] = useState(false);
-  const isInFlightRef = useRef(false);
-
-  const [getCartDetailMutation, { data, loading: isLoading, error }] =  useMutation(GET_CART_ITEM, {
-      onCompleted: (response) => {
-        const cartData = response?.createReadCart?.readCart;
-        if (cartData) {
-          const cartDetail = formatCartDetail(cartData);
-          dispatch(addItem(cartDetail));
-        }
-      },
-      onError: (error) => {
-        console.error("Cart detail error:", error);
-      },
-  });
+  const cartDetail = useAppSelector((state) => state.cartDetail);
+  const {cart, loading, error} = cartDetail;
 
   const getCartDetail = useCallback(async () => {
     const token = getCartToken();
     if (!token) {
       return;
     }
-
-    if (isInFlightRef.current) return;
-
-    isInFlightRef.current = true;
-    setIsInFlight(true);
-    try {
-      await getCartDetailMutation();
-    } catch (e) {
-      throw e;
-    } finally {
-      isInFlightRef.current = false;
-      setIsInFlight(false);
-    }
-  }, [getCartDetailMutation]);
+    dispatch(fetchCartDetail()); 
+  }, [dispatch]);
 
   // useCartDetail在组件中被调用时就会触发useEffect
+  // 初始化执行一次,获取购物车数据
   useEffect(() => {
-    if (!cart && !isInFlightRef.current) {
+    if (cart === null && !loading) {
       getCartDetail();
     }
-  }, [cart, getCartDetail]);
+  }, [getCartDetail]);  
 
   return {
-    cartData: cart || data?.createReadCart?.readCart,
+    cartData: cart,
     getCartDetail,
-    isLoading: isLoading || (isInFlight && !cart),
+    isLoading: loading,
     error,
   };
 }

+ 28 - 12
src/utils/hooks/useCheckoutPaymentMethod.ts

@@ -1,6 +1,6 @@
 "use client";
 
-import { useEffect, useState } from "react";
+import { useEffect, useState, useRef } from "react";
 import { GET_CHECKOUT_PAYMENT_METHODS, CREATE_CHECKOUT_PAYMENT_METHODS } from "@/graphql";
 import {useApolloClient} from "@apollo/client/react";
 import {CheckoutPaymentMethod, CreateCheckoutPaymentMethodVariables} from "@/types/checkout/type";
@@ -15,11 +15,15 @@ export function useCheckoutPaymentMethod() {
     const [data, setData] = useState<CheckoutPaymentMethod[]>([]);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState<string|null>(null);
+    const isIntRef = useRef(true);
 
     const getPaymentMethod = (
         resolveCallback: (res:PaymentMethodsResult) => void = () => {}, 
         rejectCallback: (res: PaymentMethodsResult) => void = ()=> {}
     ) => {
+        if(!isIntRef.current) {
+            setLoading(true);
+        }
         return apolloClient.query({
             query: GET_CHECKOUT_PAYMENT_METHODS,
             fetchPolicy: "no-cache",
@@ -30,24 +34,35 @@ export function useCheckoutPaymentMethod() {
         
             }
         }).then((res) => {
-            const shippingMethods =  res.data?.collectionPaymentMethods || [];
+            if(isIntRef.current) {
+                isIntRef.current = false;
+            }
+            const paymentMethods =  res.data?.collectionPaymentMethods || [];
             const resData: PaymentMethodsResult = {
-                data: shippingMethods,
+                data: paymentMethods,
                 msg: '',
                 error: false
             };
+
+            setData(resData.data);
+            setLoading(false);
             
-            console.log('GET_CHECKOUT_paymentmethod res ---- ',shippingMethods);
+            console.log('GET_CHECKOUT_paymentmethod res ---- ',paymentMethods);
             resolveCallback(resData);
             return resData;
         
         }).catch((err) => {
+            if(isIntRef.current) {
+                isIntRef.current = false;
+            }
             console.error('GET_CHECKOUT_paymentmethod error ---- ',err);
             const resData: PaymentMethodsResult = {
                 data: [],
                 msg: err.message,
                 error: true
             };
+            setError(resData.msg);
+            setLoading(false);
             rejectCallback(resData);
             return resData;
         });
@@ -78,14 +93,15 @@ export function useCheckoutPaymentMethod() {
     };
 
     useEffect(() => { 
-        getPaymentMethod((resolveData) => {
-            const methodsData = resolveData.data;
-            setData(methodsData);
-            setLoading(false);
-        }, (rejectData) => {
-            setError(rejectData.msg);
-            setLoading(false);
-        });
+        getPaymentMethod();
+        // getPaymentMethod((resolveData) => {
+        //     const methodsData = resolveData.data;
+        //     setData(methodsData);
+        //     setLoading(false);
+        // }, (rejectData) => {
+        //     setError(rejectData.msg);
+        //     setLoading(false);
+        // });
     }, []);
 
     return {

+ 50 - 1
src/utils/hooks/useCheckoutShippingMethod.ts

@@ -1,5 +1,6 @@
 "use client";
 
+import { useState, useEffect, useRef } from "react";
 import { GET_CHECKOUT_SHIPPING_RATES, CREATE_CHECKOUT_SHIPPING_METHODS } from "@/graphql";
 import {useApolloClient} from "@apollo/client/react";
 import {CheckoutShippingRate, CreateCheckoutShippingMethodVariables} from "@/types/checkout/type";
@@ -11,11 +12,18 @@ interface ShippingMethodsResult {
 }
 export function useCheckoutShippingMethod() {
     const apolloClient = useApolloClient();
+    const [data, setData] = useState<CheckoutShippingRate[]>([]);
+    const [loading, setLoading] = useState(true);
+    const [error, setError] = useState<string|null>(null);
+    const isIntRef = useRef(true);
 
     const getShippingMethod = (
         resolveCallback: (res:ShippingMethodsResult) => void = () => {}, 
         rejectCallback: (res: ShippingMethodsResult) => void = ()=> {}
     ) => {
+        if(!isIntRef.current) {
+            setLoading(true);
+        }
         return apolloClient.query({
             query: GET_CHECKOUT_SHIPPING_RATES,
             fetchPolicy: "no-cache",
@@ -26,24 +34,33 @@ export function useCheckoutShippingMethod() {
         
             }
         }).then((res) => {
+            if(isIntRef.current) {
+                isIntRef.current = false;
+            }
             const shippingMethods =  res.data?.collectionShippingRates || [];
             const resData: ShippingMethodsResult = {
                 data: shippingMethods,
                 msg: '',
                 error: false
             };
-            
+            setData(resData.data);
+            setLoading(false);
             console.log('GET_CHECKOUT_shippingmethod res ---- ',shippingMethods);
             resolveCallback(resData);
             return resData;
         
         }).catch((err) => {
+            if(isIntRef.current) {
+                isIntRef.current = false;
+            }
             console.error('GET_CHECKOUT_shippingmethod error ---- ',err);
             const resData: ShippingMethodsResult = {
                 data: [],
                 msg: err.message,
                 error: true
             };
+            setError(resData.msg);
+            setLoading(false);
             rejectCallback(resData);
             return resData;
         });
@@ -53,11 +70,43 @@ export function useCheckoutShippingMethod() {
         return apolloClient.mutate({
             mutation: CREATE_CHECKOUT_SHIPPING_METHODS,
             variables:params,
+        }).then((res) => {
+            const resData = {
+                data: res.data?.createCheckoutShippingMethod?.checkoutShippingMethod,
+                msg: '',
+                error: false
+            };
+            
+            return resData;
+        
+        }).catch((err) => {
+            const resData = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            
+            return resData;
         });
     };
 
+    useEffect(() => { 
+        getShippingMethod();
+        // getShippingMethod((resolveData) => {
+        //     const methodsData = resolveData.data;
+        //     setData(methodsData);
+        //     setLoading(false);
+        // }, (rejectData) => {
+        //     setError(rejectData.msg);
+        //     setLoading(false);
+        // });
+    }, []);
+
 
     return {
+        data,
+        loading,
+        error,
         getShippingMethod,
         saveShippingMethod
     };

+ 31 - 0
src/utils/hooks/useCurrencyList.ts

@@ -0,0 +1,31 @@
+"use client";
+
+
+import { useAppDispatch, useAppSelector } from "@/store/hooks";
+import { fetchCurrencyList } from "@/store/slices/currency-slice";
+import { useCallback, useEffect } from "react";
+
+export function useCurrencyList() {
+  const dispatch = useAppDispatch();
+  const currencyList = useAppSelector((state) => state.currencyList);
+  const {currencies, loading, error} = currencyList;
+
+  const getCurrencyList = useCallback(async () => {
+    dispatch(fetchCurrencyList()); 
+  }, [dispatch]);
+
+  
+  // 初始化执行一次,获取购物车数据
+  useEffect(() => {
+    if (currencies.length === 0 && !loading) {
+      getCurrencyList();
+    }
+  }, [getCurrencyList]);  
+
+  return {
+    currencies: currencies,
+    getCurrencyList,
+    isLoading: loading,
+    error,
+  };
+}

+ 109 - 0
src/utils/hooks/usePlaceOrder.ts

@@ -0,0 +1,109 @@
+import { useApolloClient } from "@apollo/client/react";
+import { 
+    CREATE_PAYMENT_INITIATE,
+    CREATE_PAYMENT_CALLBACK,
+    CREATE_CANCEL_ORDER,
+} from "@/graphql";
+import {
+    CreatePaymentInitiateVariables, 
+    CreatePaymentCallbackVariables,
+    CreateCancelOrderVariables,
+    CreatePaymentInitiate,
+    CreatePaymentCallback,
+    CreateCancelOrder,
+} from "@/types/checkout/type";
+
+interface CreateOrderResult {
+    data: CreatePaymentInitiate | null;
+    msg: string;
+    error: boolean;
+}
+interface CreatePaymnetCallbackResult {
+    data: CreatePaymentCallback | null;
+    msg: string;
+    error: boolean;
+}
+interface CreateCancelOrderResult {
+    data: CreateCancelOrder | null;
+    msg: string;
+    error: boolean;
+}
+export const usePlaceOrder = () => {
+    const client = useApolloClient();
+
+    const createOrder = (params: CreatePaymentInitiateVariables = {}) => {
+        return client.mutate({
+            mutation: CREATE_PAYMENT_INITIATE,
+            variables: params
+        }).then((res) => {
+            const resData = res?.data?.createPaymentInitiate?.paymentInitiate ?? null; 
+            const result: CreateOrderResult = { 
+                data:resData,
+                msg: resData?.message ?? '',
+                error: false
+            };
+            return result;
+        }).catch((err) => {
+            const result: CreateOrderResult = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            return result;
+        });
+
+    };
+
+    // 支付回调
+    const createPaymentCallback = (params: CreatePaymentCallbackVariables) => { 
+        return client.mutate({
+            mutation: CREATE_PAYMENT_CALLBACK,
+            variables: params
+        }).then((res) => {
+            const resData = res?.data?.createPaymentCallback?.paymentCallback ?? null; 
+            const result: CreatePaymnetCallbackResult = { 
+                data:resData,
+                msg: resData?.message ?? '',
+                error: false
+            };
+            return result;
+        }).catch((err) => {
+            const result: CreatePaymnetCallbackResult = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            return result;
+        });
+    };
+
+    // 取消订单
+    const cancelOrder = (params: CreateCancelOrderVariables) => { 
+        return client.mutate({
+            mutation: CREATE_CANCEL_ORDER,
+            variables: params
+        }).then((res) => {
+            const resData = res?.data?.createCancelOrder?.cancelOrder ?? null; 
+            const result: CreateCancelOrderResult = { 
+                data:resData,
+                msg: resData?.message ?? '',
+                error: false
+            };
+            return result;
+        }).catch((err) => {
+            const result: CreateCancelOrderResult = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            return result;
+        });
+    }
+
+
+  return {
+    createOrder,
+    createPaymentCallback,
+    cancelOrder
+  };
+};

+ 44 - 0
src/utils/hooks/useSaveCheckoutCart.ts

@@ -0,0 +1,44 @@
+import { useApolloClient } from "@apollo/client/react";
+import { 
+    CREATE_SAVE_CHECKOUT_CART
+} from "@/graphql";
+
+import { SaveCheckoutCartVariables, SaveCheckoutCart } from "@/types/checkout/type";
+
+interface SaveCheckoutCartResult {
+    data: SaveCheckoutCart | null;
+    msg: string;
+    error: boolean;
+}
+
+export const useSaveCheckoutCart = () => {
+    const client = useApolloClient();
+    const saveCheckoutCart = (params: SaveCheckoutCartVariables) => {
+        return client.mutate({
+            mutation: CREATE_SAVE_CHECKOUT_CART,
+            variables: params
+        }).then((res) => {
+            const resData = res.data?.createSaveCheckoutCart.saveCheckoutCart ?? null; 
+            const result: SaveCheckoutCartResult = { 
+                data:resData,
+                msg: '',
+                error: false
+            };
+            return result;
+        }).catch((err) => {
+            const result: SaveCheckoutCartResult = {
+                data: null,
+                msg: err.message,
+                error: true
+            };
+            return result;
+        });
+
+    };
+
+
+
+  return {
+    saveCheckoutCart
+  };
+};