瀏覽代碼

添加店铺配置管理,当前货币,当前渠道,当前语言 管理模块;applepay 对接完成 -- v1

fogwind 1 天之前
父節點
當前提交
17b8ab66d4
共有 42 個文件被更改,包括 1196 次插入436 次删除
  1. 14 1
      .env.example
  2. 5 0
      .gitignore
  3. 29 1
      README.md
  4. 1 1
      package.json
  5. 17 10
      pnpm-lock.yaml
  6. 3 7
      src/app/(checkout)/checkout/_components/BillingAddressCheckout.tsx
  7. 23 8
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  8. 38 14
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  9. 22 19
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  10. 45 6
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  11. 301 0
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx
  12. 3 6
      src/app/(checkout)/checkout/_components/ShippingAddressCheckout.tsx
  13. 17 21
      src/app/(checkout)/checkout/continuetopay/page.tsx
  14. 9 10
      src/app/(checkout)/checkout/page.tsx
  15. 3 3
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  16. 15 14
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  17. 7 3
      src/app/(public)/paymentresult/result/page.tsx
  18. 11 1
      src/app/globals.css
  19. 80 26
      src/app/layout.tsx
  20. 1 1
      src/components/theme/ui/PhoneNumberInput/PhoneNumberInput.tsx
  21. 39 0
      src/lib/Applepay/applepay.ts
  22. 17 0
      src/lib/Applepay/type.ts
  23. 3 140
      src/lib/graphql-fetch.ts
  24. 58 0
      src/providers/ConfigProvider.tsx
  25. 30 0
      src/providers/PaypalWrapper.tsx
  26. 2 0
      src/providers/index.ts
  27. 31 16
      src/proxy.ts
  28. 51 0
      src/proxy/auth.ts
  29. 46 0
      src/proxy/currency.ts
  30. 20 0
      src/store/selectors/currentConfig.ts
  31. 0 76
      src/store/slices/currency-slice.ts
  32. 39 0
      src/store/slices/currentconfig-slice.ts
  33. 4 3
      src/store/store.ts
  34. 1 1
      src/types/checkout/type.ts
  35. 4 4
      src/types/customer/order.ts
  36. 9 1
      src/types/customer/type.ts
  37. 15 0
      src/types/graphqlFetch/type.ts
  38. 138 11
      src/utils/bagisto/index.ts
  39. 3 1
      src/utils/constants.ts
  40. 15 0
      src/utils/hooks/useConfig.ts
  41. 0 31
      src/utils/hooks/useCurrencyList.ts
  42. 27 0
      src/utils/orderDetailTools.ts

+ 14 - 1
.env.example

@@ -7,5 +7,18 @@ NEXTAUTH_URL=http://localhost:3001
 # NextAuth 使用 secret(下面的值)对 session中的token 对象进行 签名 + 加密,生成一个 JWT 字符串,解密这个JWT字符串就可以还原token对象。
 NEXTAUTH_SECRET=9jstKgQ1Q22PlDDhPtXTZ6G4MEzHGDJIkebcd/5UyZ4=
 
+# paypal client id
+PAYPAL_SANDBOX_CLIENT_ID=
+PAYPAL_PRODUCTION_CLIENT_ID=
+
 # Application Settings
-COMPANY_NAME=Your Company Name
+COMPANY_NAME=Your Company Name
+
+# store name
+STORE_NAME=Asteria
+# 默认货币
+DEFAULT_CURRENCY=USD
+# 默认语言
+DEFAULT_LOCAL=en
+# 默认渠道
+DEFAULT_CHANNEL=default

+ 5 - 0
.gitignore

@@ -23,6 +23,7 @@
 # misc
 .DS_Store
 *.pem
+.vscode
 
 # debug
 npm-debug.log*
@@ -34,6 +35,8 @@ yarn-error.log*
 .env
 .env.production
 .env.local
+.env.development.local
+.env.test
 
 # vercel
 .vercel
@@ -42,5 +45,7 @@ yarn-error.log*
 *.tsbuildinfo
 next-env.d.ts
 
+# .well-known
+/public/.well-known
 
 ~

File diff suppressed because it is too large
+ 29 - 1
README.md


+ 1 - 1
package.json

@@ -32,7 +32,7 @@
     "@heroui/tooltip": "^2.2.29",
     "@heroui/use-disclosure": "^2.2.19",
     "@internationalized/date": "3.12.0",
-    "@paypal/react-paypal-js": "^9.2.0",
+    "@paypal/react-paypal-js": "^10.1.2",
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-types/shared": "^3.33.1",
     "@reduxjs/toolkit": "^2.10.1",

+ 17 - 10
pnpm-lock.yaml

@@ -76,8 +76,8 @@ importers:
         specifier: 3.12.0
         version: 3.12.0
       '@paypal/react-paypal-js':
-        specifier: ^9.2.0
-        version: 9.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+        specifier: ^10.1.2
+        version: 10.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       '@react-aria/visually-hidden':
         specifier: ^3.8.31
         version: 3.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -971,11 +971,11 @@ packages:
   '@panva/hkdf@1.2.1':
     resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
 
-  '@paypal/paypal-js@9.7.0':
-    resolution: {integrity: sha512-eUQVZWTEhXhaPsYDmfUaOeV5zfeLUn7kCE7/BXm6uAtMhUoK0S7uPtUzosqfH1vZgjfUSyTzghDJtYxXyRRZig==}
+  '@paypal/paypal-js@10.0.3':
+    resolution: {integrity: sha512-GtY3PID9Ey76fIJBAV8PobIwdH6Nsbm7MZlqFwB+MAPQEtVEwGe1EHrsZpb4AK0iuJNs9E3gaB75kgpFSpAEJQ==}
 
-  '@paypal/react-paypal-js@9.2.0':
-    resolution: {integrity: sha512-wckuxilT+fYYwg0TYbEbxkclYeJnhIlThpNtV9RLXQA6uBbt5xPIzGv9wt2ykQTLiNIqhXhw1Hlz5yxUeCteKQ==}
+  '@paypal/react-paypal-js@10.1.2':
+    resolution: {integrity: sha512-nPzTw58U+6Emv5qQDxVP5AdUL1gvdU5b1rHGOOTvB/WHmC2Ebn0soCOMNgP8xsvdz+Kshy2TJru0ERqC2zFkTA==}
     peerDependencies:
       react: ^16.8.0 || ^17 || ^18 || ^19
       react-dom: ^16.8.0 || ^17 || ^18 || ^19
@@ -1561,6 +1561,9 @@ packages:
   '@swc/helpers@0.5.21':
     resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
 
+  '@swc/helpers@0.5.23':
+    resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
+
   '@tailwindcss/node@4.3.0':
     resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
 
@@ -4510,13 +4513,13 @@ snapshots:
 
   '@panva/hkdf@1.2.1': {}
 
-  '@paypal/paypal-js@9.7.0':
+  '@paypal/paypal-js@10.0.3':
     dependencies:
       promise-polyfill: 8.3.0
 
-  '@paypal/react-paypal-js@9.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+  '@paypal/react-paypal-js@10.1.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
     dependencies:
-      '@paypal/paypal-js': 9.7.0
+      '@paypal/paypal-js': 10.0.3
       '@paypal/sdk-constants': 1.0.158
       react: 19.2.5
       react-dom: 19.2.5(react@19.2.5)
@@ -4999,7 +5002,7 @@ snapshots:
   '@react-spectrum/provider@3.11.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
     dependencies:
       '@adobe/react-spectrum': 3.47.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
-      '@swc/helpers': 0.5.21
+      '@swc/helpers': 0.5.23
       react: 19.2.5
       react-dom: 19.2.5(react@19.2.5)
 
@@ -5378,6 +5381,10 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  '@swc/helpers@0.5.23':
+    dependencies:
+      tslib: 2.8.1
+
   '@tailwindcss/node@4.3.0':
     dependencies:
       '@jridgewell/remapping': 2.3.5

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

@@ -19,17 +19,13 @@ import {
     IS_VALID_PHONECODE,
     IS_VALID_FULL_PHONE 
 } from "@utils/constants";
-import { Country } from "@/types/checkout/type";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
-
-export default function BillingAddressCheckout ({
-    countries,
-}:{
-    countries: Country[];
-}) {
+import { useConfig } from "@utils/hooks/useConfig";
+export default function BillingAddressCheckout () {
+    const {countries} = useConfig();
     const { 
         getValues,
         setValue,

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

@@ -9,18 +9,23 @@ import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 export default function CheckoutPlaceOrder({
     paymentMethod,
-    clickPlaceOrder, // 校验运输方式,地址 
+    clickPlaceOrder, 
+    validateCheckout,// 校验运输方式,地址 
 }: {
     paymentMethod: string;
     clickPlaceOrder: PlaceOrderFunction;
+    validateCheckout: () => Promise<boolean>;
 }) {
     const router = useRouter();
     const { showToast } = useCustomToast();
     const { createPaymentCallback } = usePlaceOrder();
     const paypalCreateOrder = async () => {
-
+        overlayLoading.start();
+        const valid = await validateCheckout();
+        if(!valid) {overlayLoading.stop();}
+        // 校验通过
         const placeOrderRes = await clickPlaceOrder();
-        if(placeOrderRes.valid && placeOrderRes.data !== null) {
+        if(placeOrderRes.data !== null) {
             return {
                 error: false,
                 msg: '',
@@ -39,19 +44,26 @@ export default function CheckoutPlaceOrder({
     };
 
     const airwallexPlaceOrder = async () => {
+        overlayLoading.start();
+        const valid = await validateCheckout();
+        if(!valid) {overlayLoading.stop();}
 
         const placeOrderRes = await clickPlaceOrder({
             paymentSuccessUrl: window.location.origin + '/paymentresult/result'
         });
         overlayLoading.stop();
-        if(placeOrderRes.valid && placeOrderRes.data !== null) {
+        if(placeOrderRes.data !== null) {
             window.location.href = placeOrderRes.data.gatewayOrderId;
         }
     };
 
     const cardAirwallexPlaceOrder = async () => {
+        overlayLoading.start();
+        const valid = await validateCheckout();
+        if(!valid) {overlayLoading.stop();}
+
         const placeOrderRes = await clickPlaceOrder();
-        if(placeOrderRes.valid && placeOrderRes.data !== null && placeOrderRes.otherData !== null) {
+        if(placeOrderRes.data !== null && placeOrderRes.otherData !== null) {
             const webOrderId = placeOrderRes.data.orderId;
             const cardNumberElement = placeOrderRes.otherData;
             const paymentIntentIdClientSecret = placeOrderRes.data.gatewayOrderId.split(':');
@@ -76,13 +88,17 @@ export default function CheckoutPlaceOrder({
     };
 
     const afterpayPlaceOrder = async () => { 
-        if(window.AfterPay) { 
+        if(window.AfterPay) {
+            overlayLoading.start();
+            const valid = await validateCheckout();
+            if(!valid) {overlayLoading.stop();} 
+
             const placeOrderRes = await clickPlaceOrder({
                 paymentSuccessUrl: window.location.origin + '/paymentresult/result',
                 paymentCancelUrl: window.location.origin + '/paymentresult/result'
             });
             
-            if(placeOrderRes.valid && placeOrderRes.data !== null) {
+            if(placeOrderRes.data !== null) {
                 const afterpayToken = placeOrderRes.data.gatewayOrderId;
                 const webOrderId = placeOrderRes.data.orderId;
                 window.AfterPay.initialize({countryCode: "US"});
@@ -130,7 +146,6 @@ export default function CheckoutPlaceOrder({
                 window.AfterPay.transfer({token: afterpayToken});
             }
         } else {
-            overlayLoading.stop();
             showToast("Please wait for the Afterpay to load completely.","warning");
         }
     };

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

@@ -6,7 +6,6 @@ import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 import { useForm, FormProvider } from "react-hook-form";
 import { 
-    Country,
     ShipAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
@@ -38,6 +37,7 @@ import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { clearCart } from "@/store/slices/cart-slice";
 import {loadAfterpayScript} from "@/lib/Afterpay/afterpay";
+import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -216,11 +216,9 @@ function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateC
 
 export default function CheckoutWrapper({
     loginEmail,
-    countries,
     cartDetailData,
 }: {
     loginEmail: string;
-    countries: Country[];
     cartDetailData: CartDetail;
 }) {
     const { createOrder } = usePlaceOrder(); 
@@ -447,12 +445,27 @@ export default function CheckoutWrapper({
         return true;
 
     };
+    // 苹果支付下单
+    const applepayPlaceOrder = async () => { 
+        const res = await createOrder();
+        if(!res.error) {
+            //创建订单成功后清空redux 购物车
+            dispatch(clearCart());  
+            return {
+                orderId: res.data?.gatewayOrderId ?? '',
+                webOrderId: res.data?.orderId ?? ''
+            };
+        } else{
+            // 抛出错误让第三方支付能够捕获
+            throw new Error("CREATE_ORDER_ERROR: " + res.msg);
+        } 
+    };
     // 下单
     const handlePlaceOrder: PlaceOrderFunction = async (params) => { 
         // 校验地址
-        overlayLoading.start();
-        const validRes = await validateCheckout();
-        if(validRes) {
+        // overlayLoading.start();
+        // const validRes = await validateCheckout();
+        // if(validRes) {
             const res = await createOrder(params);
             if(res.error) {
                 overlayLoading.stop();
@@ -467,7 +480,7 @@ export default function CheckoutWrapper({
             }
             const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
             const resultData:PlaceOrderResult<keyof ExtraMapPlaceOrderResult> = {
-                valid: true, 
+                // valid: true, 
                 data: res.data,
                 otherData: null
             }
@@ -476,10 +489,10 @@ export default function CheckoutWrapper({
                 resultData.otherData = airwallexCardElements.cardNumber as Payment.CardNumberElementType;
             }
             return resultData;
-        } else {
-            overlayLoading.stop();
-            return {valid: false, data: null, otherData: null};
-        }
+        // } else {
+        //     overlayLoading.stop();
+        //     return {valid: false, data: null, otherData: null};
+        // }
     };                    
     
     useEffect(() => {
@@ -543,8 +556,8 @@ export default function CheckoutWrapper({
                                 <div>
                                     <FormProvider {...addressForm}>
                                         <form>
-                                            <ShippingAddressCheckout countries={countries} />
-                                            <BillingAddressCheckout countries={countries} />
+                                            <ShippingAddressCheckout />
+                                            <BillingAddressCheckout />
                                         </form>
                                     </FormProvider>
                                 </div>
@@ -598,10 +611,21 @@ export default function CheckoutWrapper({
                     { paymentMethodLoading ?
                         <LoadingCheckoutPlaceOrder />
                     :
+                        (selectedPaymentMethod === 'applepay' ?
+                        <PaypalApplepayButton 
+                            grandTotal={cartData.grandTotal}
+                            billingContact={cartData.billingAddress}
+                            shippingContact={cartData.shippingAddress}
+                            validateCheckout={validateCheckout}
+                            createOrder={applepayPlaceOrder}
+                        />
+                        :
                         <CheckoutPlaceOrder 
                             paymentMethod={selectedPaymentMethod}
+                            validateCheckout={validateCheckout}
                             clickPlaceOrder={handlePlaceOrder}
-                        />
+                        />)
+                        
                     }
                 </div>
             </div>

+ 22 - 19
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -3,30 +3,33 @@
 import { useState } from "react";
 import Image from "next/image";
 import { useRouter } from 'next/navigation';
+import { useAppSelector } from "@/store/hooks";
+import {selectCurrentCurrency} from "@/store/selectors/currentConfig";
 import { OrderDetails } from "@/types/customer/type";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import ProductsModalPendingOrder from "./ProductsModalPendingOrder";
 import dynamic from "next/dynamic";
+import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 
 const CountDown = dynamic(() => import("@/components/common/CountDown"), {
   ssr: false,
 });
 export default function ContinueToPayOrderInfo({
-    orderDetail,
-    currencySymbol
+    orderDetail
 }: {
     orderDetail: OrderDetails;
-    currencySymbol: string;
 }) { 
     
     const router = useRouter();
+    const currentCurrency = useAppSelector(selectCurrentCurrency);
+    const currencySymbol = currentCurrency.symbol;
     
     const [productsModalOpen, setProductsModalOpen] = useState(false);
 
     const countDownEnd = (new Date(orderDetail.createdAt)).getTime() + 24*3600*1000;
 
-    const shippingAddress = orderDetail.addresses.edges.find(item => item.node.addressType === "order_shipping")?.node;
-    const billingAddress = orderDetail.addresses.edges.find(item => item.node.addressType === "order_billing")?.node;
+    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
+    const billingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
 
     const totalQty = orderDetail.totalQtyOrdered > 1 ? String(orderDetail.totalQtyOrdered) + 'Items' : '1 Item';
     const productItems = orderDetail.items.edges.map((item) => {
@@ -66,9 +69,9 @@ export default function ContinueToPayOrderInfo({
                 <div className="w-full mt-3 box-border rounded-md border-1 px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]">
                     <div className="border-b-1 flex h-11.25 items-center justify-between">
                         <p className="text-ly-13 font-medium">
-                            {shippingAddress?.firstName} {shippingAddress?.lastName}
+                            {shippingAddress.firstName} {shippingAddress.lastName}
                             <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
-                            {shippingAddress?.phone}
+                            {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>
@@ -79,11 +82,11 @@ export default function ContinueToPayOrderInfo({
                             <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?.address}, 
-                            {shippingAddress?.city}, 
-                            {shippingAddress?.state}, 
-                            {shippingAddress?.postcode}, 
-                            {shippingAddress?.country}
+                            {shippingAddress.address}, 
+                            {shippingAddress.city}, 
+                            {shippingAddress.state}, 
+                            {shippingAddress.postcode}, 
+                            {shippingAddress.country}
                         </span>
                     </div>
                 </div>  
@@ -95,9 +98,9 @@ export default function ContinueToPayOrderInfo({
                 <div className="w-full mt-3 box-border rounded-md border-1 px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]">
                     <div className="border-b-1 flex h-11.25 items-center justify-between">
                         <p className="text-ly-13 font-medium">
-                            {billingAddress?.firstName} {billingAddress?.lastName}
+                            {billingAddress.firstName} {billingAddress.lastName}
                             <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
-                            {billingAddress?.phone}
+                            {billingAddress.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>
@@ -108,11 +111,11 @@ export default function ContinueToPayOrderInfo({
                             <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">
-                            {billingAddress?.address}, 
-                            {billingAddress?.city}, 
-                            {billingAddress?.state}, 
-                            {billingAddress?.postcode}, 
-                            {billingAddress?.country}
+                            {billingAddress.address}, 
+                            {billingAddress.city}, 
+                            {billingAddress.state}, 
+                            {billingAddress.postcode}, 
+                            {billingAddress.country}
                         </span>
                     </div>
                 </div>  

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

@@ -1,34 +1,39 @@
 "use client";
 
 import Image from "next/image";
-import { use,useState,useEffect } from "react";
+import { use,useState,useEffect,useCallback } from "react";
 import { useForm, get, useWatch } from "react-hook-form";
 import { useRouter } from 'next/navigation'
 import { useCustomToast } from "@/utils/hooks/useToast";
 import AirwallexCardInput from "../PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCard} from "@/lib/Airwallex/useAirwallexCard";
 import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
-import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
+import { OrderDetails } from "@/types/customer/type";
+import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import PaypalButton from "../PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import {loadAfterpayScript} from "@/lib/Afterpay/afterpay";
+import PaypalApplepayButton from "../PaymentButton/PaypalApplepayButton";
+import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 
 /**@todo 优化目标:不使用 react-hook-form */
 export function PaymentMethodContinueTpPay({
     paymentMethodsDataPromise,
     defaultPaymentMethod,
     orderId,
+    orderDetail
 }: {
-    paymentMethodsDataPromise: Promise<GraphqlRequestResult<RepayOrderPaymentMethodsData>>;
+    paymentMethodsDataPromise: Promise<FetchGraphqlResult<RepayOrderPaymentMethodsData>>;
     defaultPaymentMethod: {method: string; methodTitle: string;}
     orderId: number;
+    orderDetail: OrderDetails;
 }) {
     const router = useRouter();
     const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
-    const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
+    const paymentMethods =  paymentMethodsData.data.collectionRepayOrderPaymentMethods || [];
 
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
@@ -38,7 +43,9 @@ export function PaymentMethodContinueTpPay({
         expiry: 'airwallex_expiry'
     });
     const [isPay, setIsPay] = useState(false);
-  
+    const shippingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
+    const billingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
+
     const {
         register,
         // watch,
@@ -261,6 +268,28 @@ export function PaymentMethodContinueTpPay({
         }
     };
 
+    const applepayPlaceOrder = async () => { 
+
+        const res = await paymentRepay({
+            orderId: Number(orderId),
+            paymentMethod: selectedPaymentMethod
+        });
+        
+        if(!res.error && res.data !== null) {
+            return {
+                orderId: res.data.gatewayOrderId,
+                webOrderId: res.data.orderId
+            };
+        } else {
+            // 抛出错误让第三方支付能够捕获
+            throw new Error("REPAY_ORDER_ERROR: " + res.msg);
+        }
+    };
+
+    const validateCheckout = useCallback(async() => { 
+        return true;
+    }, []);
+
     const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
     useEffect(() => {
@@ -274,7 +303,7 @@ export function PaymentMethodContinueTpPay({
         <>
             <form name="shipping method" className="w-full box-border px-4 mt-4">
                 <div className="mt-3 w-full">
-                    {paymentMethodsData.error && <p className="text-red-500 text-ly-12">{paymentMethodsData.error}</p>}
+                    {paymentMethodsData.error && <p className="text-red-500 text-ly-12">{paymentMethodsData.error.message}</p>}
                     <ul className="w-full">
                     {
                         paymentMethods.map((item) => {
@@ -359,6 +388,16 @@ export function PaymentMethodContinueTpPay({
                             Place Order
                         </button>
                     }
+                    {selectedPaymentMethod === 'applepay' &&
+                        <PaypalApplepayButton 
+                            grandTotal={orderDetail.grandTotal}
+                            billingContact={billingContact}
+                            shippingContact={shippingContact}
+                            validateCheckout={validateCheckout}
+                            createOrder={applepayPlaceOrder}
+                            isRePay={true}
+                        />
+                    }
 
                 </div>
             </div>

+ 301 - 0
src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx

@@ -0,0 +1,301 @@
+"use client";
+
+
+import {useRef,useEffect,useCallback} from "react";
+import { useRouter } from 'next/navigation';
+// import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import {
+  usePayPal,
+  useEligibleMethods,
+  INSTANCE_LOADING_STATE,
+  useApplePayOneTimePaymentSession,
+  type ConfirmOrderResponse,
+  type UseApplePayOneTimePaymentSessionProps,
+} from "@paypal/react-paypal-js/sdk-v6";
+import { useAppSelector } from "@/store/hooks";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { CartAddress } from "@/types/cart/type";
+import { useConfig } from "@/utils/hooks/useConfig";
+import {selectCurrentCurrency} from "@/store/selectors/currentConfig";
+import {ApplePayAddressContact} from "@/lib/Applepay/type";
+// import {getPaypalApplePayConfig, type PaypalApplePaySession} from "@/lib/Applepay/applepay";
+
+export default function PaypalApplepayButton({
+    createOrder,
+    validateCheckout,
+    billingContact,
+    shippingContact,
+    grandTotal,
+    isRePay
+}:{
+    validateCheckout: () => Promise<boolean>;
+    createOrder: () => Promise<{orderId: string;webOrderId: string;}>;
+    billingContact: CartAddress | null;
+    shippingContact: CartAddress | null;
+    grandTotal: number;
+    isRePay?: boolean;
+}) { 
+    const router = useRouter();
+    // const {createPaymentCallback} = usePlaceOrder();
+    const {countries,store} = useConfig();
+
+    const currentCurrency = useAppSelector(selectCurrentCurrency);
+    const currencyCode = currentCurrency.code;
+    
+    const storeName = store.storeName;
+    const gatewayOrderIdRef = useRef("");
+    const webOrderIdRef = useRef("");
+    // const [configCountryCode, setConfigCountryCode] = useState('');
+
+    // paypal sdk 是否加载完成
+    const { loadingStatus, isHydrated } = usePayPal();
+    // Fetch eligibility(资格) for one-time payment flow
+
+
+    const {
+        error: eligibilityError,
+        eligiblePaymentMethods,
+        isLoading: isEligibilityLoading,
+    } = useEligibleMethods({
+        payload: {
+            currencyCode: currencyCode, // 需要根据用户选择的货币走
+            paymentFlow: "ONE_TIME_PAYMENT", // 一次性支付
+        },
+    });
+    const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
+    const isApplepayEligible =  eligiblePaymentMethods?.isEligible("applepay");
+    const applePayConfig = eligiblePaymentMethods?.getDetails("applepay");
+
+    
+
+    // 支付回调配置
+
+    /**
+     * when the user has authorized the Apple Pay payment with Touch ID, Face ID, or a passcode.
+     * ApplePaySession onpaymentauthorized 时触发 */
+    const configCreateOrder =  useCallback(async () => {
+        const res = await createOrder();
+        gatewayOrderIdRef.current = res.orderId;
+        webOrderIdRef.current = res.webOrderId;
+        return res;
+    },[createOrder]);
+
+    /**await paypal.Applepay().confirmOrder() 后调onApprove */
+    const onApprove = useCallback((data: ConfirmOrderResponse) => {
+        console.log("applepay approved:", data);
+        /**
+         * approveApplePayPayment: {
+         *     id: string; //gatewayOrderId
+         *     status: "APPROVED"
+         * }
+         */
+        overlayLoading.stop();
+        router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success');
+
+    },[router]);
+    /**ApplePaySession oncancel */
+    const onCancel = useCallback(() => {
+        console.log("applepay cancelled:");
+        confirmDialog({
+            title: "Payment cancelled",
+            content: "You canceled the payment.",
+            noCancel: true,
+        }).then(() => {  });
+        
+    },[]);
+
+    /**
+     * 1. ApplePaySession onvalidatemerchant 出错
+     * 2. ApplePaySession completePayment 出错
+     * 3. createOrder->paypal.Applepay().confirmOrder()->onApprove() 链条出错
+     * 4. 用户点击后的整个链条出错
+     */
+    const onError = useCallback((error: Error) => {
+        
+        console.error("applepay error:", error);
+        if(!isRePay) {
+            overlayLoading.stop();
+            if(webOrderIdRef.current || gatewayOrderIdRef.current) {
+                // 订单创建成功了,跳转二次支付页面
+                confirmDialog({
+                    title: "Payment failed",
+                    content: error.message,
+                    noCancel: true,
+                }).then(() => {  
+                    router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure');
+                });
+                
+            } else {
+                // 订单创建失败,留在当前页面
+                confirmDialog({
+                    title: "Payment failed",
+                    content: error.message + " Please try again.",
+                    noCancel: true,
+                }).then(() => { });
+            }
+
+        } else {
+            
+            // 登录用户 二次支付页面 
+            confirmDialog({
+                title: "Payment Failed",
+                content: error.message,
+                noCancel: true,
+            }).then(() => {  });
+        
+        }
+    },[router,isRePay]);
+    
+
+    const addressContact: ApplePayAddressContact = {};
+    if(billingContact) {
+        const billCountry = countries.find((item) => item.code === billingContact.country);
+        addressContact.billingContact = {
+            addressLines: [billingContact.address],
+            administrativeArea: billingContact.state,
+            country: billCountry?.name,
+            countryCode: billingContact.country,
+            emailAddress: billingContact.email,
+            familyName: billingContact.lastName,
+            givenName: billingContact.firstName,
+            locality: billingContact.city,
+            postalCode:billingContact.postcode,
+        }
+    }
+    if(shippingContact) {
+        const shipCountry = countries.find((item) => item.code === shippingContact.country);
+        addressContact.shippingContact = {
+            addressLines: [shippingContact.address],
+            administrativeArea: shippingContact.state,
+            country: shipCountry?.name,
+            countryCode: shippingContact.country,
+            emailAddress: shippingContact.email,
+            familyName: shippingContact.lastName,
+            givenName: shippingContact.firstName,
+            locality: shippingContact.city,
+            postalCode:shippingContact.postcode,
+        }
+    }
+    console.log('applePayConfig ---- ', applePayConfig?.config);
+    /*
+    useEffect(() => { 
+        if(!sdkInstance || !isHydrated) return;
+        const getConfig = async () => {
+            const paypalSdkApplePayPaymentSession = await sdkInstance.createApplePayOneTimePaymentSession() as PaypalApplePaySession;
+            const res = await getPaypalApplePayConfig(paypalSdkApplePayPaymentSession);
+            console.log('res ---- ', res);
+            setConfigCountryCode(res.countryCode);
+
+        };
+        getConfig();
+    }, [isHydrated, sdkInstance]);
+    */
+   const merchantCountry = applePayConfig?.config.merchantCountry;
+
+    if(!isApplepayEligible) { 
+        return <p className="text-ly-12 text-ly-errorcolor text-center">Applepay is not available on your device.</p>;
+    }
+    
+    return (
+        <div className="box-border w-full">
+            {isLoading || !isHydrated || isEligibilityLoading || applePayConfig === undefined ?
+                <p className="text-ly-12 text-center">Applepay is loading...</p>
+            :
+                <ApplePayButtonBox 
+                    onClickHandler={validateCheckout}
+                    eligibilityError={eligibilityError}
+                    applePayConfig={applePayConfig.config}
+                    applePaySessionVersion={4}
+                    paymentRequest={{
+                        countryCode: merchantCountry === 'C2' ? 'CN' : merchantCountry as string,
+                        currencyCode: currencyCode,
+                        requiredBillingContactFields: ["postalAddress"],
+                        requiredShippingContactFields: ["name", "email", "postalAddress"],
+                        total: {
+                            label: storeName,
+                            amount: String(grandTotal),
+                            type: "final",
+                        },
+                        ...addressContact
+                    }}
+                    createOrder={configCreateOrder}
+                    onApprove={onApprove}
+                    onCancel={onCancel}
+                    onError={onError}
+                />
+            } 
+        </div>
+    );
+}
+
+// ApplePayButtonBox是 @paypal/react-paypal-js/sdk-v6 中的<ApplePayOneTimePaymentButton>组件的代码复制过来的
+function ApplePayButtonBox({
+    eligibilityError,
+    onClickHandler, // 点击苹果支付按钮
+    ...hookProps
+}: {
+    eligibilityError: Error | null;
+    onClickHandler: () => Promise<boolean>;
+} & UseApplePayOneTimePaymentSessionProps
+) {
+    const { error, handleClick, handleDestroy } = useApplePayOneTimePaymentSession(hookProps);
+
+    const buttonRef = useRef<HTMLElement>(null);
+    const handleClickRef = useRef(handleClick);
+    // handleClickRef.current = handleClick;
+
+    useEffect(() => { 
+        handleClickRef.current = handleClick;
+    }, [handleClick]);
+
+    // Apple's <apple-pay-button> manages its own enabled/disabled state internally
+    // via canMakePayments(); we deliberately don't add an SDK-level disabled layer
+    // (merchants control presentation themselves). React's onClick also doesn't
+    // work on the element due to its shadow DOM, so we attach the handler directly.
+    useEffect(() => {
+        const el = buttonRef.current;
+        if (!el) { return; }
+        const applepayButtonClick = () => {
+            onClickHandler().then((res) => {
+                if(res) {
+                    handleClickRef.current().catch(() => {
+                        // Errors are captured by the hook's setError
+                    });
+                }
+            }).catch((err) => {
+                console.error(err);
+            });
+            
+        };
+
+        el.addEventListener("click", applepayButtonClick);
+        return () => el.removeEventListener("click", applepayButtonClick);
+    }, [onClickHandler]);
+
+    useEffect(() => {
+        if (error) {
+            console.error(error);
+        }
+    }, [error]);
+
+    // Cleanup on unmount
+    useEffect(() => {
+        return () => {
+            handleDestroy();
+        };
+    }, [handleDestroy]);
+
+
+    return (
+        <>
+            <apple-pay-button
+                ref={buttonRef}
+                buttonstyle="black"
+                type="pay"
+                locale="en"
+            />
+            { eligibilityError && <p className="text-ly-errorcolor text-ly-12 text-center">{eligibilityError.message}</p> }
+        </>
+    );
+};

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

@@ -13,17 +13,14 @@ import {
 } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
 import { GET_COUNTRY_STATES } from "@/graphql";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
-import { Country } from "@/types/checkout/type";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
+import { useConfig } from "@utils/hooks/useConfig";
 
-export default function ShippingAddressCheckout ({
-    countries,
-}:{
-    countries: Country[];
-}) {
+export default function ShippingAddressCheckout () {
+    const {countries} = useConfig();
     const { 
         getValues,
         setValue,

+ 17 - 21
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -2,12 +2,11 @@ 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 {serverGraphqlFetch} from "@utils/bagisto/index";
 import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
-import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS, GET_CURRENCIES } from "@/graphql";
+import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS } from "@/graphql";
 // import { Country } from "@/types/checkout/type";
 import { OrderDetailsData } from "@/types/customer/type";
-import { CurrenciesData } from "@/types/currency/type";
 import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
 import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
@@ -44,34 +43,30 @@ export default async function ContinueToPay({searchParams}: {
 
     const query = await searchParams;
     const orderid = query?.orderid;
-    const orderDetailResponse = await authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
-        id: orderid,// `/api/shop/customer-orders/${orderid}`
+    const {data: orderDetailResponse} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
+        query: GET_ORDER_DETAILS,
+        variables: {
+            id: orderid as string
+        }
     }); 
-    const orderDetailData = orderDetailResponse.data?.customerOrder;
-    if(orderDetailData === undefined || orderDetailData?.status !== "pending")  {
+    const orderDetailData = orderDetailResponse.customerOrder;
+    if(!orderDetailData  || orderDetailData?.status !== "pending")  {
         redirect('/', RedirectType.replace);
     }
 
-    const currencyRes = await fullCacheGraphqlRequest<CurrenciesData>(GET_CURRENCIES);
-    const currencyList = currencyRes.data?.currencies?.edges.map((item) => item.node) ?? [];
     // 获取支付方式列表
-    const paymentMethodsResponse = authorizationGraphqlQuery<RepayOrderPaymentMethodsData>(GET_REPAYORDER_PAYMENT_METHODS,{
-        orderId: Number(orderid)
+    const paymentMethodsResponse = serverGraphqlFetch<RepayOrderPaymentMethodsData,{orderId:number}>({
+        query: GET_REPAYORDER_PAYMENT_METHODS,
+        variables: {
+            orderId:  Number(orderid)
+        }
     });
-    
-    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}/>
+            <ContinueToPayOrderInfo orderDetail={orderDetailData} />
            
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium">Payment Method</h3>
@@ -80,6 +75,7 @@ export default async function ContinueToPay({searchParams}: {
             <Suspense fallback={<LoadingPaymentMethod/>}> 
                 <PaymentMethodContinueTpPay 
                     orderId={orderDetailData._id}
+                    orderDetail={orderDetailData}
                     paymentMethodsDataPromise={paymentMethodsResponse}
                     defaultPaymentMethod={orderDetailData.payment}
                 />

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

@@ -1,10 +1,10 @@
 import { getServerSession } from "next-auth";
 import { authOptions } from "@utils/auth";
 import { redirect, RedirectType } from 'next/navigation';
-import { fullCacheGraphqlRequest, authorizationGraphqlMutate} from "@/lib/graphql-fetch";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
 import CheckoutWrapper from "./_components/CheckoutWrapper";
-import { GET_COUNTRIES, GET_CART_ITEM } from "@/graphql";
-import { Country } from "@/types/checkout/type";
+import { GET_CART_ITEM } from "@/graphql";
+
 import { GetCartItemData } from "@/types/cart/type";
 
 /**
@@ -23,21 +23,20 @@ export default async function CheckoutPage() {
 
     const session = await getServerSession(authOptions); // 游客是null
 
-    const {data: countriesData} = await fullCacheGraphqlRequest<{
-        countries: Country[] 
-    }>(GET_COUNTRIES);
 
-    const cartDetailsRes = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
-    if(cartDetailsRes.data === null || cartDetailsRes.data.createReadCart === null) {
+    const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
+        query: GET_CART_ITEM,
+        operationName:'GetCartItem'
+    });
+    if(cartDetailsRes.createReadCart === null) {
         redirect('/', RedirectType.replace);
     }
-    const cartDetails = cartDetailsRes.data.createReadCart.readCart;
+    const cartDetails = cartDetailsRes.createReadCart.readCart;
     return (
         <>
             <CheckoutWrapper key={cartDetails.id}
                 cartDetailData={cartDetails}
                 loginEmail={session?.user?.email || ""}
-                countries={countriesData?.countries || []}
             />
         </>
     );

+ 3 - 3
src/app/(public)/paymentresult/_components/CancelOrFailure.tsx

@@ -2,7 +2,7 @@
 
 import {use, useEffect, useRef} from "react";
 import Link from "next/link";
-import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
+import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { OrderDetailsData } from "@/types/customer/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import Faqs from "./Faqs";
@@ -13,13 +13,13 @@ export default function CancelOrFailure({
     paymentStatus,
     isGuest
 }: {
-    orderDetailPromise: Promise<GraphqlRequestResult<OrderDetailsData>>,
+    orderDetailPromise: Promise<FetchGraphqlResult<OrderDetailsData>>,
     paymentStatus: string,
     isGuest: boolean
 }) { 
     const dispatch = useAppDispatch();
     const promiseRes = use(orderDetailPromise);
-    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
+    const orderDetailRes = promiseRes.data.customerOrder ?? null;
     const {cancelOrder} = usePlaceOrder();
     const orderId = orderDetailRes?._id;
     const orderStatus = orderDetailRes?.status;

+ 15 - 14
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -3,9 +3,10 @@
 import {use, useEffect} from "react";
 import Link from "next/link";
 import Image from "next/image";
-import {GraphqlRequestResult} from "@/types/graphqlFetch/type";
+import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { OrderDetailsData,ProductItemAdditional } from "@/types/customer/type";
-import { useCurrencyList } from "@utils/hooks/useCurrencyList";
+import { useConfig } from "@utils/hooks/useConfig";
+import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import Faqs from "./Faqs";
 
 
@@ -33,14 +34,14 @@ export default function OrderDetailWrapper({
     orderDetailPromise,
     isGuest
 }: {
-    orderDetailPromise: Promise<GraphqlRequestResult<OrderDetailsData>>;
+    orderDetailPromise: Promise<FetchGraphqlResult<OrderDetailsData>>;
     isGuest: boolean;
 }) { 
 
-    const {currencies} = useCurrencyList();
+    const {currencies} = useConfig();
     const promiseRes = use(orderDetailPromise);
-    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
-    const shippingAddress = orderDetailRes?.addresses?.edges.find(item => item.node.addressType === "order_shipping");
+    const orderDetailRes = promiseRes.data.customerOrder;
+    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetailRes.addresses,"order_shipping");
     let orderCreatedAt = 'Can\'t get order date';
     if(orderDetailRes?.createdAt) {
         const localDate = new Date(orderDetailRes.createdAt);
@@ -91,14 +92,14 @@ export default function OrderDetailWrapper({
                     <div className="text-ly-12 flex-none">Ship to:</div>
                     <div className="w-40.5 text-ly-12 leading-ly-20 text-[#666666] flex-none">
                         {
-                            shippingAddress?.node.firstName 
-                            + ' ' + shippingAddress?.node.lastName 
-                            + ' ' + shippingAddress?.node.phone
-                            + ' ' + shippingAddress?.node.address
-                            + ' ' + shippingAddress?.node.city
-                            + ' ' + shippingAddress?.node.state 
-                            + ' ' + shippingAddress?.node.postcode
-                            + ' ' + shippingAddress?.node.country
+                            shippingAddress.firstName 
+                            + ' ' + shippingAddress.lastName 
+                            + ' ' + shippingAddress.phone
+                            + ' ' + shippingAddress.address
+                            + ' ' + shippingAddress.city
+                            + ' ' + shippingAddress.state 
+                            + ' ' + shippingAddress.postcode
+                            + ' ' + shippingAddress.country
                         }
                     </div>
                 </div>

+ 7 - 3
src/app/(public)/paymentresult/result/page.tsx

@@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
 import { cookies } from 'next/headers';
 import { redirect, RedirectType } from 'next/navigation'
 import { authOptions } from "@utils/auth";
-import {authorizationGraphqlQuery} from "@/lib/graphql-fetch";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {GET_ORDER_DETAILS} from "@/graphql";
 import { OrderDetailsData } from "@/types/customer/type";
 import SuccessPageSkeleton from "../_components/SuccessPageSkeleton";
@@ -34,8 +34,12 @@ export default async function SuccessPage({searchParams}: {
     }
 
 
-    const orderDetailPromise = authorizationGraphqlQuery<OrderDetailsData>(GET_ORDER_DETAILS,{
-        id: orderid,// `/api/shop/customer-orders/${orderid}`
+
+    const orderDetailPromise = serverGraphqlFetch<OrderDetailsData,{id:string}>({
+        query: GET_ORDER_DETAILS,
+        variables: {
+            id:  orderid as string
+        }
     });
 
     return (

+ 11 - 1
src/app/globals.css

@@ -214,7 +214,7 @@ body {
   }
   /*输入框样式*/
   .ly-input{
-    @apply block w-full box-border text-ly-12 p-4 border-1 border-ly-inputborder h-11.5 placeholder:text-ly-placeholder
+    @apply block w-full box-border text-ly-12 px-4 leading-4 border-1 border-ly-inputborder h-11.5 placeholder:text-ly-placeholder focus:outline-none
   }
 
   .fade-in{
@@ -261,4 +261,14 @@ body {
       transform: translateY(100%);
     }
   }
+}
+
+apple-pay-button {
+    --apple-pay-button-box-sizing: border-box;
+    --apple-pay-button-width: 300px;
+    --apple-pay-button-height: 48px;
+    --apple-pay-button-border-radius: 24px;
+    --apple-pay-button-padding: 6px 0px;
+    display: block;
+    width: 100%;
 }

+ 80 - 26
src/app/layout.tsx

@@ -1,17 +1,19 @@
+import Script from 'next/script'
 import { Outfit } from "next/font/google";
 import "./globals.css";
-import { GlobalProviders,ReduxProvider } from "@/providers";
+import { GlobalProviders,ReduxProvider,PayPalWrapper,ConfigProvider } 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 { GET_CART_ITEM,GET_COUNTRIES,GET_CURRENCIES } from "@/graphql";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
 import { GetCartItemData } from "@/types/cart/type";
+import { Country } from "@/types/checkout/type";
+import { CurrenciesData } from "@/types/currency/type";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -46,46 +48,98 @@ export default async function RootLayout({
   children: React.ReactNode;
 }>) {
 
-  const cartDetailsRes = await authorizationGraphqlMutate<GetCartItemData>(GET_CART_ITEM,{},'GetCartItem');
+  let paypalClientId = process.env.PAYPAL_PRODUCTION_CLIENT_ID;
+  if (process.env.NODE_ENV !== "production") {
+    paypalClientId = process.env.PAYPAL_SANDBOX_CLIENT_ID;
+  }
+  if(paypalClientId === undefined) {
+    throw new Error('Paypal Client Id is not defined');
+  }
+  const defaultChannel = process.env.DEFAULT_CHANNEL;
+  const defaultCurrency = process.env.DEFAULT_CURRENCY;
+  const defaultLocale = process.env.DEFAULT_LOCAL;
+  const storeName = process.env.STORE_NAME;
+  if(!defaultChannel || !defaultCurrency || !defaultLocale ||!storeName) {
+    throw new Error('Default Channel or Default Currency or Default Locale or store name is not defined');
+  }
+
+  // 获取country list 和currency list
+  const {data: countryResData} = await serverGraphqlFetch<{
+      countries: Country[] 
+  }>({
+    query: GET_COUNTRIES
+  });
+  const {data: currencyResData} = await serverGraphqlFetch<CurrenciesData>({
+    query: GET_CURRENCIES
+  });
+ 
+  const countries = countryResData.countries;
+  const currencyList = currencyResData.currencies?.edges.map((item) => item.node) ?? [];
+  const storeConfig = {
+    defaultChannel: defaultChannel,
+    defaultCurrency: defaultCurrency,
+    defaultLocale: defaultLocale,
+    storeName: storeName
+  };
+  const currentCurrency = currencyList.find((item) => item.code === storeConfig.defaultCurrency);
+
+  if(currentCurrency === undefined) {
+    throw new Error('Currency is not match with default currency');
+  }
+
+  const currentConfig = {
+    currentCurrency: currentCurrency,
+    currentLocale: storeConfig.defaultLocale,
+    currentChannel: storeConfig.defaultChannel
+  };
+
+  const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
+    query: GET_CART_ITEM,
+    operationName:'GetCartItem'
+  });
   
-  const cartDetails = cartDetailsRes.data?.createReadCart?.readCart ?? null;
+  const cartDetails = cartDetailsRes.createReadCart?.readCart ?? null;
   return (
     <html lang="en" suppressHydrationWarning>
       <head>
         <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"></meta>
+        {/* <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
+        <script>eruda.init();</script> */}
       </head>
       <body className={clsx(
         "min-h-screen font-outfit text-foreground bg-background antialiased",
         outfit.variable
       )}>
+        {/* <Script src="https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js" /> */}
+        <Script src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js" />
         
         <main>
           <ErrorBoundary>
-            <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
-                }
-              }}>
+            <PayPalWrapper clientId={paypalClientId}>
+              <ConfigProvider
+                  config={{
+                    currencies: currencyList,
+                    countries: countries,
+                    store: storeConfig
+                  }}
+              >
+              <ReduxProvider 
+                preloadedState={{
+                  cartDetail:{
+                    cart: cartDetails,
+                    loading: false,
+                    error: null
+                  },
+                  currentConfig: currentConfig
+                }}
+              >
                 <GlobalProviders>
                   {children}
                   <AddToCartModalWrapper />
                 </GlobalProviders>
               </ReduxProvider>
-            </PayPalProvider>
+              </ConfigProvider>
+            </PayPalWrapper>
             <SpeculationRules />
           </ErrorBoundary>
         </main>

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

@@ -180,7 +180,7 @@ function PhoneNumberInput({
                     </div>
                 </div>
                 
-                <input className="flex-auto h-11.5 pr-3 placeholder:text-ly-placeholder text-ly-12" 
+                <input className="flex-auto h-11.5 pr-3 placeholder:text-ly-placeholder text-ly-12 focus:outline-none" 
                     value={phoneNumber}
                     type="text"
                     placeholder={placeholder}

+ 39 - 0
src/lib/Applepay/applepay.ts

@@ -0,0 +1,39 @@
+import type {
+    ApplePayOneTimePaymentSession
+}
+from "@paypal/react-paypal-js/sdk-v6";
+
+
+export type PaypalApplePaySession =
+    ApplePayOneTimePaymentSession & {
+
+        config: () => Promise<{
+            merchantCapabilities:string[];
+            supportedNetworks:string[];
+            merchantCountry:string;
+            tokenNotificationURL:string;
+            isEligible: boolean;
+        }>
+
+    };
+
+
+export async function getPaypalApplePayConfig(
+    session: PaypalApplePaySession
+){
+
+    const config = await session.config();
+
+
+    return {
+        countryCode: config.merchantCountry,
+
+        merchantCapabilities: config.merchantCapabilities,
+
+        supportedNetworks: config.supportedNetworks,
+
+        tokenNotificationURL: config.tokenNotificationURL,
+
+        isEligible: config.isEligible
+    };
+}

+ 17 - 0
src/lib/Applepay/type.ts

@@ -0,0 +1,17 @@
+export interface ApplePayContact {
+    addressLines?: string[];
+    administrativeArea?: string;
+    country?: string;
+    countryCode?: string;
+    emailAddress?: string;
+    familyName?: string;
+    givenName?: string;
+    locality?: string;
+    postalCode?: string;
+}
+
+
+export interface ApplePayAddressContact {
+    billingContact?: ApplePayContact;
+    shippingContact?: ApplePayContact;
+}

+ 3 - 140
src/lib/graphql-fetch.ts

@@ -1,4 +1,4 @@
-import { cookies } from 'next/headers'
+
 import { type DocumentNode } from "graphql";
 import {
   type OperationVariables,
@@ -6,8 +6,8 @@ import {
 } 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";
+
+
 
 /* 定义自己的context类型
 import "@apollo/client";
@@ -175,140 +175,3 @@ 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 authorizationGraphqlQuery<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables
->(
-    query: DocumentNode,
-    variables?: TVariables,
-): 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 queryOption: ApolloClient.QueryOptions<TData> = {
-            query,
-            variables,
-            fetchPolicy: "no-cache",
-            context: {
-                headers: {
-                    "Authorization": "Bearer " + token,
-                },
-                fetchOptions: {
-                    cache: 'no-store',
-                },
-            }
-        };
-        console.log('authorizationGraphqlQuery token ---- ', token);
-        const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
-        console.log('authorizationGraphqlQuery result ---- ', result);
-        const resData = result.data || null;
-        return {data: resData, error: result.error? result.error.message : '' };
-    } catch (error) {
-        throw error;
-    }
-    
-}
-
-
-export async function fullCacheGraphqlRequest<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables
->(
-    query: DocumentNode,
-    variables?: TVariables,
-): Promise<GraphqlRequestResult<TData>> {
-    
-    const client = getClient();
-    try {
-        const queryOption: ApolloClient.QueryOptions<TData> = {
-            query,
-            variables,
-            fetchPolicy: "cache-first",
-            context: {
-                fetchOptions: {
-                    cache: 'force-cache',
-                },
-            }
-        };
-        const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
-        console.log('fullCacheGraphqlRequest result ---- ', result);
-        const resData = result.data || null;
-        return {data: resData, error: result.error? result.error.message : '' };
-    } catch (error) {
-        throw error;
-    }
-    
-}

+ 58 - 0
src/providers/ConfigProvider.tsx

@@ -0,0 +1,58 @@
+"use client";
+
+
+import {
+    createContext,
+} from "react";
+
+import {CurrencyItem} from "@/types/currency/type";
+import { Country } from "@/types/checkout/type";
+
+
+
+export interface StoreConfig {
+
+    storeName: string;
+
+    defaultCurrency: string;
+
+    defaultLocale: string;
+    
+    defaultChannel: string;
+}
+
+
+
+export interface AppConfig {
+
+
+    currencies: CurrencyItem[];
+
+
+    countries: Country[];
+
+
+    store: StoreConfig;
+
+}
+
+export const ConfigContext = createContext<AppConfig|null>(null);
+
+export function ConfigProvider({
+    config,
+    children
+}:{
+    config:AppConfig;
+    children:React.ReactNode;
+}){
+    return (
+        <ConfigContext.Provider
+            value={config}
+        >
+            {children}
+        </ConfigContext.Provider>
+    );
+}
+
+
+

+ 30 - 0
src/providers/PaypalWrapper.tsx

@@ -0,0 +1,30 @@
+"use client";
+import { PayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
+
+export function PayPalWrapper({ 
+    children,
+    clientId
+}: { 
+    clientId: string;
+    children: React.ReactNode;
+}) {
+
+  return (
+    <PayPalProvider
+        clientId={clientId}
+        components={[
+            "paypal-payments",
+            "venmo-payments",
+            "paypal-guest-payments",
+            "paypal-subscriptions",
+            "card-fields",
+            "paypal-messages",
+            "applepay-payments"
+        ]}
+        environment={process.env.NODE_ENV === "production" ? "production" : "sandbox"} // 上线改为 "production"
+        pageType="checkout"
+    >
+        {children}
+    </PayPalProvider>
+  );
+}

+ 2 - 0
src/providers/index.ts

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

+ 31 - 16
src/proxy.ts

@@ -1,26 +1,41 @@
 import { NextResponse } from 'next/server'
 import type { NextRequest } from 'next/server'
-import { getToken } from 'next-auth/jwt'
+import {handleCurrency} from "./proxy/currency";
+import {handleAuth} from "./proxy/auth";
 
-export async function proxy(request: NextRequest) {
-    const { pathname } = request.nextUrl
+export async function proxy(
+    request:NextRequest
+){
+    let response = NextResponse.next();
 
-    const restrictedPaths = ['/customer/login', '/customer/register']
+    /**
+     * Currency
+     */
+    response = handleCurrency(
+        request,
+        response
+    );
 
-    if (restrictedPaths.some((path) => pathname.startsWith(path))) {
-        const token = await getToken({
-            req: request,
-            secret: process.env.NEXTAUTH_SECRET
-        })
 
-        if (token) {
-            return NextResponse.redirect(new URL('/', request.url))
-        }
-    }
 
-    return NextResponse.next()
+    /**
+     * Auth
+     */
+    response = await handleAuth(
+        request,
+        response
+    );
+
+
+
+    return response;
+
 }
 
+
 export const config = {
-    matcher: ['/customer/login', '/customer/register'],
-}
+    matcher:[
+        "/((?!_next|api|favicon.ico).*)"
+    ]
+
+};

+ 51 - 0
src/proxy/auth.ts

@@ -0,0 +1,51 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+import { getToken } from "next-auth/jwt";
+export async function handleAuth(
+    request: NextRequest,
+    response: NextResponse
+) {
+
+    const { pathname } = request.nextUrl;
+
+    const restrictedPaths = [
+        "/customer/login",
+        "/customer/register"
+    ];
+
+
+    const isRestricted =  restrictedPaths.some(path => pathname.startsWith(path));
+
+    if (!isRestricted) {
+        return response;
+    }
+
+    const token = await getToken({
+        req: request,
+        secret: process.env.NEXTAUTH_SECRET
+    });
+
+    if (!token) {
+        return response;
+    }
+
+
+    /**
+     * 创建 redirect response
+     */
+    const redirectResponse = NextResponse.redirect(new URL("/",request.url));
+
+    /**
+     * 复制之前 response 的 cookies
+     *
+     * 保留 currency cookie
+     */
+    response.cookies.getAll().forEach(cookie => {
+        redirectResponse.cookies.set(cookie);
+    });
+
+
+
+    return redirectResponse;
+
+}

+ 46 - 0
src/proxy/currency.ts

@@ -0,0 +1,46 @@
+import type { NextRequest } from "next/server";
+import type { NextResponse } from "next/server";
+import {CURRENT_CURRENCY,CURRENT_LOCAL,CURRENT_CHANNEL} from "@/utils/constants";
+
+export function handleCurrency(
+    request: NextRequest,
+    response: NextResponse
+) {
+
+    const currency = request.cookies.get(CURRENT_CURRENCY);
+    const local = request.cookies.get(CURRENT_LOCAL);
+    const channel = request.cookies.get(CURRENT_CHANNEL);
+    const defaultCurrency = process.env.DEFAULT_CURRENCY;
+    const defaultLocal = process.env.DEFAULT_LOCAL;
+    const defaultChannel = process.env.DEFAULT_CHANNEL;
+    if (!currency && defaultCurrency) {
+        
+        response.cookies.set(CURRENT_CURRENCY,defaultCurrency,{
+            path: '/', // 该cookie对那些路径有效,/ 表示所有路径
+            maxAge: 60 * 60 * 24 * 365, // 过期时间 1年,到期后自动删除
+            sameSite: 'lax', // 允许跨域
+            secure: process.env.NODE_ENV === 'production' // true 表示这个 Cookie 是否只能通过 HTTPS 发送
+        })
+    }
+    if (!local && defaultLocal) {
+        
+        response.cookies.set(CURRENT_LOCAL,defaultLocal,{
+            path: '/', // 该cookie对那些路径有效,/ 表示所有路径
+            maxAge: 60 * 60 * 24 * 365, // 过期时间 1年,到期后自动删除
+            sameSite: 'lax', // 允许跨域
+            secure: process.env.NODE_ENV === 'production' // true 表示这个 Cookie 是否只能通过 HTTPS 发送
+        })
+    }
+    if (!channel && defaultChannel) {
+        
+        response.cookies.set(CURRENT_CHANNEL,defaultChannel,{
+            path: '/', // 该cookie对那些路径有效,/ 表示所有路径
+            maxAge: 60 * 60 * 24 * 365, // 过期时间 1年,到期后自动删除
+            sameSite: 'lax', // 允许跨域
+            secure: process.env.NODE_ENV === 'production' // true 表示这个 Cookie 是否只能通过 HTTPS 发送
+        })
+    }
+
+    return response;
+
+}

+ 20 - 0
src/store/selectors/currentConfig.ts

@@ -0,0 +1,20 @@
+import type { RootState } from "@/store/store";
+
+// 为了解决在组件中获取值时typescript提示可能为null的情况
+export function selectCurrentCurrency(
+    state: RootState
+) {
+
+    const currency = state.currentConfig.currentCurrency;
+
+
+    if (!currency) {
+
+        throw new Error(
+            "currentCurrency is not initialized"
+        );
+    }
+
+
+    return currency;
+}

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

@@ -1,76 +0,0 @@
-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;

+ 39 - 0
src/store/slices/currentconfig-slice.ts

@@ -0,0 +1,39 @@
+
+import { createSlice } from "@reduxjs/toolkit";
+import {CurrencyItem} from "@/types/currency/type";
+// interface CurrencyState {
+//   currencies: CurrencyItem[]
+//   loading: boolean;
+//   error: string | null;
+// }
+export interface CurrentConfigState {
+  currentCurrency: CurrencyItem | null;
+  currentLocale: string | null;
+  currentChannel: string | null;
+}
+
+
+
+const initialState: CurrentConfigState = {
+  currentCurrency: null,
+  currentLocale: null,
+  currentChannel: null,
+};
+
+const currentConfigSlice = createSlice({
+  name: "currentConfig",
+  initialState,
+  reducers: {
+      setCurrentCurrency(state, action) {
+        state.currentCurrency = action.payload;
+      },
+      setCurrentLocale(state, action) {
+        state.currentLocale = action.payload;
+      },
+      setCurrentChannel(state, action) {
+        state.currentChannel = action.payload;
+      }
+  }
+});
+
+export default currentConfigSlice.reducer;

+ 4 - 3
src/store/store.ts

@@ -3,7 +3,7 @@ import { configureStore } from "@reduxjs/toolkit";
 import cartSlice, {CartState} from "./slices/cart-slice";
 import userSlice from "./slices/user-slice";
 import addToCartDialogReducer from "./slices/addToCartDialogSlice";
-import currencySlice from "./slices/currency-slice";
+import currentConfigSlice, {CurrentConfigState} from "./slices/currentconfig-slice";
 
 // export const store = configureStore({
 //   reducer: {
@@ -17,7 +17,8 @@ import currencySlice from "./slices/currency-slice";
 // export type RootState = ReturnType<typeof store.getState>;
 // export type AppDispatch = typeof store.dispatch;
 export interface PreloadedStateType { 
-  cartDetail: CartState;  
+  cartDetail: CartState; 
+  currentConfig: CurrentConfigState;
 }
 export const makeStore = (preloadedState: PreloadedStateType) => {
     return configureStore({
@@ -25,7 +26,7 @@ export const makeStore = (preloadedState: PreloadedStateType) => {
         cartDetail: cartSlice,
         user: userSlice,
         addToCartDialog: addToCartDialogReducer,
-        currencyList: currencySlice
+        currentConfig: currentConfigSlice
       },
       preloadedState
     });

+ 1 - 1
src/types/checkout/type.ts

@@ -479,7 +479,7 @@ export type ExtraMapPlaceOrderResult = {
 
 export type ExtraDataPlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> = ExtraMapPlaceOrderResult[Code];
 export interface PlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> {
-    valid: boolean; 
+    // valid: boolean; 
     data: CreatePaymentInitiate | null;
     otherData: ExtraDataPlaceOrderResult<Code> | null;
 }

+ 4 - 4
src/types/customer/order.ts

@@ -23,11 +23,11 @@ interface ItemsInOrderDetails {
         node: ProductsInOrderDetails;
     }[];
 }
-
-interface AddressInOrderDetails { 
+export type OrderAddressType = "order_shipping" | "order_billing";
+export interface AddressInOrderDetails { 
     id: string;
     _id: number;
-    addressType: string;
+    addressType: OrderAddressType;
     orderId: number;
     name: string;
     firstName: string;
@@ -46,7 +46,7 @@ interface AddressInOrderDetails {
     updatedAt: string;
 }
 
-interface AddressListInOrderDetails { 
+export interface AddressListInOrderDetails { 
     edges: {
         node: AddressInOrderDetails;
     }[];

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

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

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

@@ -1,4 +1,19 @@
 export interface GraphqlRequestResult<TData = unknown> {
   data: TData | null;
   error: string | undefined;
+}
+
+export interface FetchGraphqlError {
+    message: string;
+    locations: Array<{
+      line: number;
+      column: number;
+    }>,
+    path: Array<string>,
+    extensions: Record<string, unknown>
+}
+export interface FetchGraphqlResult<TData = unknown> {
+  status: number;
+  data: TData;
+  error: FetchGraphqlError | null;
 }

+ 138 - 11
src/utils/bagisto/index.ts

@@ -20,7 +20,15 @@ import {
   FORGET_PASSWORD,
 } from "@/graphql/customer/mutations";
 import { DocumentNode } from "graphql";
-import { GRAPHQL_URL, REST_API_URL } from "@/utils/constants";
+import { 
+  GRAPHQL_URL, 
+  REST_API_URL,
+  GUEST_CART_TOKEN,
+  IS_GUEST,
+  CURRENT_CURRENCY,
+  CURRENT_LOCAL,
+  CURRENT_CHANNEL,
+} from "@/utils/constants";
 import {
   GET_FOOTER,
   GET_THEME_CUSTOMIZATION,
@@ -36,6 +44,8 @@ import {
   ThemeCustomizationResponse,
   PageData,
 } from "@/types/theme/theme-customization";
+import { decodeJWT } from "@/utils/jwt-cookie";
+import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 
 
 type ExtractVariables<T> = T extends { variables: object }
@@ -46,6 +56,35 @@ interface PageByUrlKeyResponse {
   pageByUrlKeypages?: PageData[];
 }
 
+async function getBaseHeader() {
+    /** 
+     * Headers:
+     *   X-LOCALE   — locale code, e.g. "en", "fr", "ar"
+     *   X-CHANNEL  — channel code, e.g. "default"
+     *   X-CURRENCY — currency code, e.g. "USD", "EUR", "INR"
+     * */
+    const cookieStore = await cookies();
+    const xCurrency = cookieStore.get(CURRENT_CURRENCY)?.value; // 在proxy.ts中已经设置过值了
+    const xLocal = cookieStore.get(CURRENT_LOCAL)?.value; // 在proxy.ts中已经设置过值了
+    const xChannel = cookieStore.get(CURRENT_CHANNEL)?.value; // 在proxy.ts中已经设置过值了
+    const baseHeaders: Record<string, string> = {
+      "Content-Type": "application/json",
+      "X-STOREFRONT-KEY": STOREFRONT_KEY,
+      
+    };
+    if(xCurrency) {
+      baseHeaders["X-Currency"] = xCurrency;
+    }
+    if(xLocal) {
+      baseHeaders["X-Local"] = xLocal;
+    }
+    if(xChannel) {
+      baseHeaders["X-Channel"] = xChannel;
+    }
+    return baseHeaders;
+}
+
+
 // rest api fetch
 export async function restApiFetch<T>({
   api,
@@ -82,10 +121,8 @@ export async function restApiFetch<T>({
       accessToken = sessions?.user?.accessToken;
     }
 
-    const baseHeaders: Record<string, string> = {
-      "Content-Type": "application/json",
-      "X-STOREFRONT-KEY": STOREFRONT_KEY,
-    };
+    const headerRes = await getBaseHeader();
+    const baseHeaders: Record<string, string> = {...headerRes};
 
     if (accessToken) {
       baseHeaders.Authorization = `Bearer ${accessToken}`;
@@ -130,8 +167,100 @@ console.log('restApiFetch --- baseHeaders:', baseHeaders)
   }
 }
 
+// graphql fetch use for server side
+export async function serverGraphqlFetch<
+  TData,
+  TVariables = Record<string, never>
+>({
+  cache = "force-cache",
+  headers,
+  query,
+  tags,
+  variables,
+  revalidate = 60,
+  operationName = ''
+}: {
+  cache?: RequestCache;
+  headers?: HeadersInit | Record<string, string>;
+  query: string | DocumentNode;
+  tags?: string[];
+  variables?: TVariables;
+  revalidate?: number;
+  operationName?: string;
+}): Promise<FetchGraphqlResult<TData>> {
+  try {
+    const queryString = typeof query === "string" ? query : (query.loc?.source?.body ?? "");
+
+    let accessToken: string | undefined = undefined;
+    let guestToken: string | undefined = undefined;
+
+    const cookieStore = await cookies();
+    
+    const isGuest = cookieStore.get(IS_GUEST)?.value;
+    const gt =  cookieStore.get(GUEST_CART_TOKEN)?.value;
+    if(isGuest === 'false') {
+      // 登录用户
+      const sessions = (await getServerSession(
+        authOptions,
+      )) as BagistoSession | null;
+      accessToken = sessions?.user?.accessToken;
+    } else if(gt) {
+        // 游客
+        const jwtRes = decodeJWT<{
+            sessionToken: string;
+            cartId: number;
+            isGuest: boolean;
+        }>(gt, true);
+        guestToken = jwtRes?.sessionToken || '';
+    }
+
+    const headerRes = await getBaseHeader();
+    const baseHeaders: Record<string, string> = {...headerRes};
 
+    if (accessToken) {
+      baseHeaders['Authorization'] = `Bearer ${accessToken}`;
+    } else if (guestToken) {
+      baseHeaders['Authorization'] = `Bearer ${guestToken}`;
+    }
+
+    if (headers) {
+      if (headers instanceof Headers) {
+        headers.forEach((value, key) => (baseHeaders[key] = value));
+      } else {
+        Object.assign(baseHeaders, headers);
+      }
+    }
+    console.log('serverGraphqlFetch --- baseHeaders:', baseHeaders);
+    const result = await fetch(GRAPHQL_URL, {
+      method: "POST",
+      headers: baseHeaders,
+      body: JSON.stringify({
+        query: queryString,
+        ...(variables && { variables }),
+      }),
+      cache,
+      next: {
+        revalidate: cache === "no-store" ? 0 : revalidate || 60,
+        ...(tags && { tags }),
+      },
+    });
+
+    const body = await result.json();
+    console.log('serverGraphqlFetch --- body:',body);
+    if (body.errors) {
+      if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
+        return { status: result.status, data: body.data, error: null };
+      } else {
+        return { status: result.status, data: body.data, error: body.errors[0] };
+      }
+    }
+    return { status: result.status, data: body.data,error: null };
+  } catch (e) {
+    throw e;
+  }
+}
 
+// graphql fetch use for route handler
 export async function bagistoFetch<T>({
   cache = "force-cache",
   headers,
@@ -169,10 +298,8 @@ export async function bagistoFetch<T>({
       accessToken = sessions?.user?.accessToken;
     }
 
-    const baseHeaders: Record<string, string> = {
-      "Content-Type": "application/json",
-      "X-STOREFRONT-KEY": STOREFRONT_KEY,
-    };
+    const headerRes = await getBaseHeader();
+    const baseHeaders: Record<string, string> = {...headerRes};
 
     if (accessToken) {
       baseHeaders.Authorization = `Bearer ${accessToken}`;
@@ -224,7 +351,7 @@ export async function bagistoFetch<T>({
     throw e;
   }
 }
-
+/*
 export async function bagistoFetchNoSession<T>({
   query,
   tags,
@@ -276,7 +403,7 @@ export async function bagistoFetchNoSession<T>({
     throw { error: e, query };
   }
 }
-
+*/
 export const removeEdgesAndNodes = <T>(array: Array<T>) => {
   return array?.map((edge) => edge);
 };

+ 3 - 1
src/utils/constants.ts

@@ -247,7 +247,9 @@ export const SortByFields: SortOrderTypes[] = [
 export const GUEST_CART_TOKEN = "guest_cart_token";
 export const GUEST_CART_ID = "guest_cart_id";
 export const IS_GUEST = "is_guest";
-
+export const CURRENT_CURRENCY = "current_currency";
+export const CURRENT_LOCAL = "current_local";
+export const CURRENT_CHANNEL = "current_channel";
 
 export const NEXTAUTH_TOKEN = "next-auth.session-token";
 export const NEXTAUTH_SECURE_TOKEN = "__Secure-next-auth.session-token";

+ 15 - 0
src/utils/hooks/useConfig.ts

@@ -0,0 +1,15 @@
+import { useContext } from "react";
+import { ConfigContext } from "@/providers/ConfigProvider";
+export function useConfig(){
+    const context = useContext(ConfigContext);
+
+    if(!context){
+
+        throw new Error(
+            "useConfig must be used inside ConfigProvider"
+        );
+
+    }
+
+    return context;
+}

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

@@ -1,31 +0,0 @@
-"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,
-  };
-}

+ 27 - 0
src/utils/orderDetailTools.ts

@@ -0,0 +1,27 @@
+import { AddressListInOrderDetails,OrderAddressType } from "@/types/customer/type";
+import { CartAddress } from "@/types/cart/type";
+
+export function getAddressFromOrderDetailAddressList(addressList: AddressListInOrderDetails, addressType: OrderAddressType) {
+    const address = addressList.edges.find((x) => x.node.addressType === addressType);
+
+    if (!address) {
+        throw new Error(
+            "Order shipping address missing"
+        );
+    }
+    const res:  CartAddress = {
+        id: String(address.node._id),
+        firstName: address.node.firstName,
+        lastName: address.node.lastName,
+        email: address.node.email,
+        address: address.node.address,
+        city: address.node.city,
+        state: address.node.state,
+        country: address.node.country,
+        postcode: address.node.postcode,
+        phone: address.node.phone,
+     }
+
+    return res;
+
+}