瀏覽代碼

购物车页/结账页/待支付页样式;后退至购物车和结账页触发重新获取购物车数据;

fogwind 1 天之前
父節點
當前提交
472aec18e7
共有 55 個文件被更改,包括 1802 次插入 和 728 次删除
  1. 1 0
      README.md
  2. 二進制
      public/image/billingaddress-bg.webp
  3. 二進制
      public/image/giftcard-bg.webp
  4. 二進制
      public/image/payment/afterpay.webp
  5. 二進制
      public/image/payment/applepay.webp
  6. 二進制
      public/image/payment/credit.webp
  7. 二進制
      public/image/payment/klarna.webp
  8. 二進制
      public/image/payment/paypal.webp
  9. 20 8
      src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx
  10. 4 3
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  11. 47 15
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  12. 79 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueAddressInfo.tsx
  13. 435 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueCheckout.tsx
  14. 143 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinuePaymentMethod.tsx
  15. 0 290
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  16. 280 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayWrapper.tsx
  17. 19 13
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  18. 8 45
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  19. 40 33
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  20. 1 1
      src/app/(checkout)/checkout/_components/LostInsurance/LostInsurance.tsx
  21. 7 4
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx
  22. 5 5
      src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx
  23. 16 6
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  24. 30 26
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  25. 36 27
      src/app/(checkout)/checkout/_components/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx
  26. 2 2
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  27. 5 56
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  28. 25 21
      src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx
  29. 39 22
      src/app/(checkout)/checkout/cart/page.tsx
  30. 2 2
      src/app/(checkout)/checkout/continuetopay/loading.tsx
  31. 5 23
      src/app/(checkout)/checkout/continuetopay/page.tsx
  32. 2 0
      src/app/(checkout)/checkout/page.tsx
  33. 4 26
      src/app/(public)/customer/account/_components/AccountHeader.tsx
  34. 42 0
      src/app/(public)/customer/account/_components/VipPlusCard.tsx
  35. 63 3
      src/app/(public)/customer/account/page.tsx
  36. 2 2
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  37. 2 2
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  38. 15 0
      src/app/globals.css
  39. 5 1
      src/app/layout.tsx
  40. 59 0
      src/components/cart/CartBackRefresh.tsx
  41. 81 31
      src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx
  42. 36 0
      src/components/cart/UnpaidOrder/UnpaidOrderSectionLoading.tsx
  43. 2 2
      src/components/common/CountDown.tsx
  44. 43 0
      src/components/common/VipPlusRulesModal/VipPlusRulesModal.tsx
  45. 1 1
      src/app/(public)/paymentresult/_components/Faqs.tsx
  46. 4 2
      src/components/theme/ui/kernel/confirm/api.ts
  47. 61 36
      src/components/theme/ui/kernel/confirm/host.tsx
  48. 4 2
      src/components/theme/ui/kernel/confirm/store.ts
  49. 31 3
      src/lib/Airwallex/airwallexInit.ts
  50. 2 0
      src/providers/ConfigProvider.tsx
  51. 52 0
      src/types/api/customer/profile/type.ts
  52. 2 0
      src/types/api/gift/my-gift-cards.ts
  53. 7 7
      src/types/customer/order.ts
  54. 15 8
      src/utils/constants.ts
  55. 18 0
      src/utils/helper.ts

+ 1 - 0
README.md

@@ -422,6 +422,7 @@ ssr + redux:
 第一次渲染时在服务端获取数据存入redux,后续在客户端操作时更新redux。
 购物车页和结账页不从redux中取数据,在服务端重新获取。
 但是在购物车页和结账页对购物车数据做的修改需要同步到redux。
+>上面的方案有严重缺陷,会阻塞页面的渲染,已修改
 
 ## 关于结账页地址
 如果用户在结账页新建一个地址,这个地址的type是 cart_billing/cart_shipping。

二進制
public/image/billingaddress-bg.webp


二進制
public/image/giftcard-bg.webp


二進制
public/image/payment/afterpay.webp


二進制
public/image/payment/applepay.webp


二進制
public/image/payment/credit.webp


二進制
public/image/payment/klarna.webp


二進制
public/image/payment/paypal.webp


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

@@ -22,14 +22,19 @@ export default function CheckoutCoupon({
     const { showToast } = useCustomToast();
 
     const [codeValue, setCodeValue] = useState(couponCode);
+    const [errMsg, setErrMsg] = useState('');
 
-    const amount = Number(discountAmount.replace(/^\D+/ig,''));
+    const amount = Number(discountAmount.replace(/\,/g,'').replace(/^\D+/ig,''));
 
     const codeInputChange = (e: ChangeEvent<HTMLInputElement>) => {
         setCodeValue(e.target.value);
     };
 
     const submitCouponCode = async () => {
+        if(!codeValue) {
+            setErrMsg('Please input your code.');
+            return;
+        }
         const code = couponCode && couponCode === codeValue ? '' : codeValue; 
 
         overlayLoading.start();
@@ -42,20 +47,26 @@ export default function CheckoutCoupon({
         });
         overlayLoading.stop();
         if(!saveRes.error) {
-            if(!code) setCodeValue('');
+            if(!code) {
+                setCodeValue('');
+                setErrMsg('Coupon code was canceled.')
+            } else {
+                showToast(`Coupon code "${code}" was applied.`, 'success');
+                setErrMsg('');
+            }
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
                 onCouponChange(newCartDetail);
             }
         } else {
-            showToast(saveRes.msg, 'danger');
+            setErrMsg(saveRes.msg);
         }
 
     };
     return (
         <>
-            <div className="relative box-border w-full flex justify-between items-center">
-                <span className="text-ly-24 font-medium">Discount</span>
+            <div className="relative box-border pt-6 border-t-1 w-full flex justify-between items-center">
+                <span className="text-ly-24 font-medium leading-6">Discount</span>
                
                 
                 <div className="flex items-center">
@@ -90,19 +101,20 @@ export default function CheckoutCoupon({
                     </p>
                     <p className="text-ly-12 leading-4 mt-3">* Not valid on promotional wigs.</p>
                 </div>
-                <div className="flex justify-between items-center mt-3 box-border w-full border-1 h-12 rounded-sm pl-3 pr-2">
-                    <input className="focus:outline-none"
+                <div className="flex justify-between items-center mt-3 box-border w-full border-1 h-11 rounded-sm pl-3 pr-1">
+                    <input className="focus:outline-none text-ly-12"
                         type="text"
                         value={codeValue}
                         placeholder="Coupon Code"
                         onChange={codeInputChange}
                     />
-                    <button className="flex items-center text-ly-14 rounded-sm bg-ly-green text-white px-2 h-8"
+                    <button className="flex items-center justify-center text-ly-14 rounded-sm bg-ly-middlegreen text-white w-19 h-9"
                         onClick={submitCouponCode}
                     >
                         {couponCode && codeValue === couponCode ? 'Remove' : 'Apply'}
                     </button>
                 </div>
+                {errMsg && <p className="mt-3 text-ly-14 text-ly-errorcolor">{errMsg}</p>}
             </div>
 
         </>

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

@@ -39,6 +39,7 @@ export default function CheckoutPlaceOrder({
         }
         // 校验通过
         const placeOrderRes = await clickPlaceOrder();
+        overlayLoading.stop();
         if(placeOrderRes.data !== null) {
             return {
                 error: false,
@@ -307,21 +308,21 @@ export default function CheckoutPlaceOrder({
                     />
                 </div>
                 {(paymentMethod === 'awxklarna' || paymentMethod === 'awxafterpay') &&
-                    <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                    <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                         onClick={airwallexPlaceOrder}
                     >
                         Place Order
                     </button>
                 }
                 {paymentMethod === 'airwallex' && 
-                    <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                    <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                         onClick={cardAirwallexPlaceOrder}
                     >
                         Place Order
                     </button>
                 }
                 {(paymentMethod === 'afterpay' || paymentMethod === 'clearpay') &&
-                    <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                    <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                         onClick={afterpayPlaceOrder}
                     >
                         Place Order

文件差異過大導致無法顯示
+ 47 - 15
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx


+ 79 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueAddressInfo.tsx

@@ -0,0 +1,79 @@
+"use client";
+
+import type { OrderDetails } from "@/types/customer/type";
+import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
+
+
+export function ContinueAddressInfo({
+    orderDetail
+}: {
+    orderDetail: OrderDetails;
+}) { 
+
+    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
+    const billingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
+
+
+    return(
+        <>
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-16">Shipping Address</h3>
+                <div className="w-full mt-4 box-border 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}
+                            <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
+                            {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>
+                    <div className="flex items-center h-10">
+                        <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"  d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
+                            <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                            <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}
+                        </span>
+                    </div>
+                </div>  
+
+            </div>
+
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-16">Billing Address</h3>
+                <div className="w-full mt-4 box-border px-3.75 bg-[url(/image/billingaddress-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}
+                            <span className="border-r border-ly-gray h-3.5 mx-2"></span> 
+                            {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>
+                    <div className="flex items-center h-10">
+                        <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"  d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
+                            <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round"  ></path>
+                            <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}
+                        </span>
+                    </div>
+                </div>  
+
+            </div>
+
+        </>
+    );
+}

+ 435 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueCheckout.tsx

@@ -0,0 +1,435 @@
+"use client";
+
+import clsx from "clsx";
+import { useState,useCallback } from "react";
+import { useRouter } from 'next/navigation'
+import { useCustomToast } from "@/utils/hooks/useToast";
+import {
+    airwallexManager
+} from "@/lib/Airwallex/useAirwallexCard";
+import type { OrderDetails } from "@/types/customer/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 PaypalApplepayButton from "../PaymentButton/PaypalApplepayButton";
+import KlarnaButton from "../PaymentButton/KlarnaButton";
+import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
+
+
+export function ContinueCheckout({
+    selectedPaymentMethod,
+    orderDetail
+}: {
+    selectedPaymentMethod: string;
+    orderDetail: OrderDetails;
+}) {
+    const orderId = orderDetail._id;
+
+    const router = useRouter();
+    const { showToast } = useCustomToast();
+    const { paymentRepay, createPaymentCallback } = usePlaceOrder();
+
+    const orderCurrencyCode = orderDetail.orderCurrencyCode;
+    const [isPay, setIsPay] = useState(false);
+    const shippingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
+    const billingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
+
+    const paypalRepayOrder = async () => {
+        overlayLoading.start();
+        const res = await paymentRepay({
+            orderId: Number(orderId),
+            paymentMethod: selectedPaymentMethod
+        });
+        overlayLoading.stop();
+        if(!res.error && res.data !== null) {
+            return {
+                error: false,
+                msg: '',
+                orderId: res.data.gatewayOrderId,
+                webOrderId: res.data.orderId
+            };
+        } else {
+            confirmDialog({
+                title: "Warning",
+                content: res.msg + " Create order failed. Please try again.",
+                noCancel: true,
+            }).then(() => {});
+            return {
+                error: true,
+                msg: '',
+                orderId: '',
+                webOrderId: ''
+            };
+        }
+
+    };
+
+    const airwallexRepay = async () => {
+        setIsPay(true);
+        overlayLoading.start();
+        const res = await paymentRepay({
+            orderId: Number(orderId),
+            paymentMethod: selectedPaymentMethod,
+            paymentSuccessUrl: window.location.origin + '/paymentresult/result'
+        });
+        
+        if(!res.error && res.data !== null) {
+            // 创建订单成功了,跳转支付页面
+            window.location.href = res.data.gatewayOrderId;
+        } else {
+            //创建订单出错了
+            overlayLoading.stop();
+            confirmDialog({
+                title: "Warning",
+                content: res.msg + " Create order failed. Please try again.",
+                noCancel: true,
+            }).then(() => {});
+        }
+        setIsPay(false);
+    };
+
+
+    const validateAirwallexCard = () => {
+
+        const res = airwallexManager.validateCard();
+        if(res.msg) {
+            showToast(res.msg, 'danger');
+        }
+        return res.valid;
+       
+    };
+
+    const cardAirwallexRepay = async () => { 
+        setIsPay(true);
+        overlayLoading.start();
+        const validRes = validateAirwallexCard();
+        if(validRes) {
+           
+            const res = await paymentRepay({
+                orderId: Number(orderId),
+                paymentMethod: selectedPaymentMethod,
+            });
+            
+            if(!res.error && res.data !== null) {
+                // 创建订单成功
+                const cardNumberElement = airwallexManager.getCardNumberElement();
+                const paymentIntentIdClientSecret = res.data.gatewayOrderId.split(':');
+                const paymentIntentId = paymentIntentIdClientSecret[0];
+                const paymentIntentClientSecret = paymentIntentIdClientSecret[1];
+
+                cardNumberElement?.confirm({
+                    intent_id: paymentIntentId,
+                    client_secret: paymentIntentClientSecret,
+                }).then((result) => { 
+                    overlayLoading.stop();
+                    console.log('cardNumberElement.confirm result ---- ', result);
+                    // 支付成功 跳转页面
+                    router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=success');
+                }).catch((error) => {
+                    overlayLoading.stop();
+                    console.log('cardNumberElement.confirm error ---- ', error);
+                    // 支付失败
+                    router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=failure');
+                });
+            } else {
+                //创建订单出错了
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: res.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            }
+        }else {
+            overlayLoading.stop();
+        }
+        setIsPay(false);
+    };
+
+    const afterpayPlaceOrder = async () => { 
+        setIsPay(true);
+        overlayLoading.start();
+        if(window.AfterPay) { 
+            
+            const res = await paymentRepay({
+                orderId: Number(orderId),
+                paymentMethod: selectedPaymentMethod,
+                paymentSuccessUrl: window.location.origin + '/paymentresult/result',
+                paymentCancelUrl: window.location.origin + '/paymentresult/result'
+            });
+            
+            if(!res.error && res.data !== null) {
+                const afterpayToken = res.data.gatewayOrderId;
+                const webOrderId = res.data.orderId;
+                let countryCode = "US";
+                if(orderCurrencyCode === "CAD") {
+                    countryCode = "CA";
+                } else if(orderCurrencyCode === "AUD") {
+                    countryCode = "AU";
+                } else if(orderCurrencyCode === "NZD") {
+                    countryCode = "NZ";
+                } else if(orderCurrencyCode === "GBP") {
+                    countryCode = "GB";
+                }
+                window.AfterPay.initialize({countryCode: countryCode});
+                // To avoid triggering browser anti-popup rules, the AfterPay.open()
+                // function must be directly called inside the click event listener
+                window.AfterPay.open();
+                // If you don't already have a checkout token at this point, you can
+                // AJAX to your backend to retrieve one here. The spinning animation
+                // will continue until `AfterPay.transfer` is called.
+                // If you fail to get a token you can call AfterPay.close()
+                window.AfterPay.onComplete = async (event) => {
+                    console.log('window.AfterPay.onComplete event ---- ', event);
+                    const status: "success" | "cancel" | "failure" = event.data.status === "SUCCESS" ? 'success' : 'cancel';
+                    const resCallback = await createPaymentCallback({
+                        orderId: Number(webOrderId),
+                        gatewayOrderId: event.data.orderToken,
+                        status: status,
+                    });
+                    overlayLoading.stop();
+                    if (event.data.status == "SUCCESS") {
+                        // The customer confirmed the payment schedule.
+                        // The token is now ready to be captured from your server backend.
+                        // 跳转结果页 成功
+                        if(!resCallback.error) {
+                            // 后端capture成功,跳转到成功落地页 
+                            router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
+                        } else {
+                            // capture 失败,提示错误
+                            confirmDialog({
+                                title: "Somthing Wrong",
+                                content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderId,
+                                noCancel: true,
+                            }).then(() => { 
+                                router.replace('/');
+                            });
+                            
+                        }
+                        
+                    } else {
+                        // The customer cancelled the payment or closed the popup window.
+                        confirmDialog({
+                            title: "Payment Cancelled",
+                            content: "You canceled the payment.",
+                            noCancel: true,
+                        }).then(() => { });
+                    }
+                }
+                window.AfterPay.transfer({token: afterpayToken});
+            } else {
+                //创建订单出错了
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: res.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            }
+        } else {
+            overlayLoading.stop();
+            showToast("Please wait for the Afterpay to load completely.","warning");
+        }
+    };
+
+    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 klarnaPlaceOrder = async () => { 
+        overlayLoading.start();
+ 
+        const resRepay = await paymentRepay({
+            orderId:Number(orderId),
+            paymentMethod: selectedPaymentMethod,
+            paymentSuccessUrl: window.location.origin + '/paymentresult/result',
+            paymentCancelUrl: window.location.origin + '/paymentresult/result'
+        });
+        
+        if(!resRepay.error && resRepay.data !== null) {
+            const klarnaToken = resRepay.data.gatewayOrderId;
+            const webOrderId = resRepay.data.orderId;
+            const billingAddress = billingContact;
+            const shippingAddress = shippingContact;
+
+            const klarnaClientToken = klarnaToken.split("|")[0];
+            // const klarnaVerifyToken = klarnaToken.split("|")[1];
+            window.Klarna.Payments.init({
+                client_token: klarnaClientToken,
+            });
+            //klarna-widget-container
+            window.Klarna.Payments.load({
+                container: '#klarna-widget-container',
+            }, (response) => {
+                if(!response.show_form) { // klarna不可用
+                    // 跳转结果页
+                    router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=failure');
+                } else {
+
+                    
+                    window.Klarna.Payments.authorize(
+                        {},
+                        {
+                            billing_address: {
+                                given_name: billingAddress.firstName,
+                                family_name: billingAddress.lastName,
+                                email: billingAddress.email,
+                                phone: billingAddress.phone,
+                                street_address: billingAddress.address,
+                                postal_code: billingAddress.postcode,
+                                city: billingAddress.city,
+                                country: billingAddress.country
+                            },
+                            shipping_address: {
+                                given_name: shippingAddress.firstName,
+                                family_name: shippingAddress.lastName,
+                                email: shippingAddress.email,
+                                street_address: shippingAddress.address,
+                                postal_code: shippingAddress.postcode,
+                                city: shippingAddress.city,
+                                phone: shippingAddress.phone,
+                                region: shippingAddress.state,
+                                country: shippingAddress.country
+                            },
+                            // customer: {
+                            //     date_of_birth: "1941-03-21",
+                            // },
+                        },
+                        async function(res) {
+                            console.log('klarna authorize res ---- ',res);
+                            if(res.approved && res.authorization_token) { // 可以支付
+                                overlayLoading.stop();
+                                router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
+                                /*
+                                const resCallback = await createPaymentCallback({
+                                    orderId: Number(webOrderId),
+                                    gatewayOrderId: res.authorization_token,
+                                    verifyToken: klarnaVerifyToken,
+                                    status: "success",
+                                });
+                                // 跳转结果页
+                                overlayLoading.stop();
+                                if(!resCallback.error) {
+                                    router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
+                                } else {
+                                    // callback 失败,提示错误
+                                    confirmDialog({
+                                        title: "Somthing Wrong",
+                                        content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderId,
+                                        noCancel: true,
+                                    }).then(() => { 
+                                        router.replace('/');
+                                    });
+                                    
+                                }
+                                */
+
+                            } else {
+                                // if(res.show_form) { // klarna可用,但是有报错
+                                // } else {
+                                //     // klarna 不可用 跳转结果页 
+                                // }
+                                overlayLoading.stop();
+                                router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=failure');
+                            }
+                        }
+                    );
+                    
+
+                }
+            });
+            
+        } else {
+                //创建订单出错了
+                overlayLoading.stop();
+                confirmDialog({
+                    title: "Warning",
+                    content: resRepay.msg + " Create order failed. Please try again.",
+                    noCancel: true,
+                }).then(() => {});
+            }
+        
+    };
+
+    return (
+        <div className="w-full box-border mt-6">
+
+            <div className={clsx("w-full",{
+                "hidden": selectedPaymentMethod !== 'paypal_smart_button'
+            })}>
+                <PaypalButton
+                    createOrder={paypalRepayOrder}
+                    isRePay={true}
+                />
+            </div>
+            
+            {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
+                <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
+                    onClick={airwallexRepay}
+                    disabled={isPay}
+                >
+                    Pay Now
+                </button>
+            }
+            {selectedPaymentMethod === 'airwallex' && 
+                <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
+                    onClick={cardAirwallexRepay}
+                    disabled={isPay}
+                >
+                    Pay Now
+                </button>
+            }
+            {(selectedPaymentMethod === 'afterpay' || selectedPaymentMethod === 'clearpay') && 
+                <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
+                    onClick={afterpayPlaceOrder}
+                >
+                    Pay Now
+                </button>
+            }
+
+            <div className={clsx("w-full",{
+                "hidden": selectedPaymentMethod !== 'applepay'
+            })}>
+                <PaypalApplepayButton 
+                    grandTotal={orderDetail.grandTotal}
+                    billingContact={billingContact}
+                    shippingContact={shippingContact}
+                    validateCheckout={validateCheckout}
+                    createOrder={applepayPlaceOrder}
+                    isRePay={true}
+                />
+            </div>
+            
+            <div className={clsx("w-full",{
+                "hidden": selectedPaymentMethod !== 'klarna'
+            })}>
+                <KlarnaButton 
+                    clickHandler={klarnaPlaceOrder}
+                />
+            </div>
+
+        </div>
+    );
+  
+}
+
+

文件差異過大導致無法顯示
+ 143 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinuePaymentMethod.tsx


文件差異過大導致無法顯示
+ 0 - 290
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx


文件差異過大導致無法顯示
+ 280 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayWrapper.tsx


+ 19 - 13
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -441,15 +441,21 @@ export function PaymentMethodContinueTpPay({
                     <ul className="w-full">
                     {
                         paymentMethods.map((item) => {
-                            return (<li className="w-full mt-3" key={item.method}>
+                            return (<li key={item.method} 
+                                className={clsx("w-full mt-3",selectedPaymentMethod === 'airwallex' && item.method === 'airwallex' && "bg-[#ECF7F4] border-1 border-ly-green rounded-xs")}
+                            >
                                 <label htmlFor={"paymentmethod-" + item.method} 
-                                    className="group box-border flex items-center relative bg-ly-lightgray  p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
+                                    className={clsx(
+                                        "group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm", 
+                                        selectedPaymentMethod !== 'airwallex' && "has-checked:bg-[#ECF7F4] has-checked:border-1 has-checked:border-ly-green has-checked:rounded-xs",
+                                        selectedPaymentMethod === 'airwallex' && "has-checked:bg-[#ECF7F4]"
+                                    )}
                                 >
                                     <p className="pr-2 flex-none relative">
-                                        <Image className="block h-8 w-auto"
+                                        <Image className="block h-6 w-auto"
                                             src={getPaymentMethodLogo(item.method)} alt={"Payment Method " + item.title}
-                                            width={32}
-                                            height={32}
+                                            width={57}
+                                            height={24}
                                         />
                                     </p>
                                     <p className="text-ly-12 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
@@ -471,7 +477,7 @@ export function PaymentMethodContinueTpPay({
                                     />
                                 </label> 
                                 { item.method === "airwallex" &&
-                                    <div className="w-full mt-3"
+                                    <div className="w-full box-border px-3 pb-3"
                                         style={{display: airwallexCardElementShow ? 'block' : 'none'}}
                                     >
                                         <AirwallexCardInput />
@@ -488,7 +494,7 @@ export function PaymentMethodContinueTpPay({
             
             <div className="w-full box-border px-4 mt-3">
                 <p className="text-ly-12 text-[#666666] leading-4.75">
-                    By providing your information, you agree to Asteria'sPrivacy Policyand Terms of Use.
+                    By providing your information, you agree to Asteria's Privacy Policy and Terms of Use.
                 </p>
 
                 <div className="mt-6 w-full">
@@ -502,26 +508,26 @@ export function PaymentMethodContinueTpPay({
                     </div>
                     
                     {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
-                        <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                        <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                             onClick={airwallexRepay}
                             disabled={isPay}
                         >
-                            Pay
+                            Pay Now
                         </button>
                     }
                     {selectedPaymentMethod === 'airwallex' && 
-                        <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                        <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                             onClick={cardAirwallexRepay}
                             disabled={isPay}
                         >
-                            Pay
+                            Pay Now
                         </button>
                     }
                     {(selectedPaymentMethod === 'afterpay' || selectedPaymentMethod === 'clearpay') && 
-                        <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                        <button className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-white rounded-3xl text-ly-16 font-bold"
                             onClick={afterpayPlaceOrder}
                         >
-                            Place Order
+                            Pay Now
                         </button>
                     }
 

+ 8 - 45
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -1,67 +1,30 @@
 "use client";
 
 import Image from "next/image";
+import {getProductAdditionalInfo} from "@/utils/orderDetailTools";
 import CommonModal from "@/components/theme/ui/CommonModal";
-import type {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";
+import type {ProductsInOrderDetails} from "@/types/customer/type";
 import { Price } from "@components/theme/ui/Price";
 
 
-// 根据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,
     currencyCode,
+    totalQtyOrdered,
+    orderSubtotal,
     onClose,
 }: {
     isOpen: boolean;
     products: ProductsInOrderDetails[],
     currencyCode: string;
+    totalQtyOrdered: number;
+    orderSubtotal: number;
     onClose: () => void;
 }) {
 
-    const { subTotal, totalItems } = caclculateTotal(products);
     
+    const totalItems = totalQtyOrdered > 1 ? String(totalQtyOrdered) + " Items" : String(totalQtyOrdered) + " Item"
 
     const closeModal = () => {
         onClose();
@@ -82,7 +45,7 @@ export default function ProductsModalPendingOrder({
             <div className="mt-6 flex justify-between items-center">
                 <span className="text-ly-12">{totalItems}</span>
                 <Price className="text-ly-14 font-bold"
-                    amount={subTotal}
+                    amount={orderSubtotal}
                     currencyCode={currencyCode}
                 />
             </div>

文件差異過大導致無法顯示
+ 40 - 33
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx


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

@@ -53,7 +53,7 @@ export default function LostInsurance({
         title = `Fixed $${cartData.shippingInsuranceValue} Package Insurance`;
     }
     return (<>
-        <div className="relative box-border w-full bg-[#F9F9F9] p-3">
+        <div className="relative box-border w-full bg-[#ECF7F4] p-3">
             <div className="flex justify-between items-center">
                 <span className="text-ly-13">
                     {title}

+ 7 - 4
src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx

@@ -194,7 +194,10 @@ export default function PaypalButton({
         // isPending: guestButtonPending, 
         error: guestButtonError, 
         handleClick: guestButtonHandleClick,
-    } = usePayPalGuestPaymentSession(paypalOnHandlerConfig);
+    } = usePayPalGuestPaymentSession({
+        fullPageOverlay: { enabled: true },
+        ...paypalOnHandlerConfig
+    });
 
     const payLaterDetails = eligiblePaymentMethods?.getDetails("paylater");
     const { 
@@ -224,18 +227,18 @@ export default function PaypalButton({
                         :
                             <paypal-button onClick={() => {
                                 return handlerPaypalClick(paypalButtonHandleClick);
-                            }} type="pay" className="w-full" disabled={isPaying} />
+                            }} type="pay" className="checkout-paypal-button w-full" disabled={isPaying} />
                         }
                         {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 disabled={isPaying} ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
+                                <paypal-basic-card-button className="checkout-paypal-button" disabled={isPaying} ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
                             </paypal-basic-card-container>
                         }
                      
                         {isPayLaterEligible && 
-                            <paypal-pay-later-button className="w-full mt-3" disabled={isPaying}
+                            <paypal-pay-later-button className="w-full mt-3 checkout-paypal-button" disabled={isPaying}
                                 onClick={() => handlerPaypalClick(paylaterButtonHandleClick)}
                                 countryCode={payLaterDetails?.countryCode}
                                 productCode={payLaterDetails?.productCode}

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

@@ -26,28 +26,28 @@ export default function AirwallexCardInput() {
 
         <div id="airwallex-dropIn-card-container" className="w-full">
             <div className="w-full">
-                <div id={cardNumberId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></div>
+                <div id={cardNumberId} className="w-full box-border bg-white border-1 border-ly-inputborder h-12.5 pl-4 flex items-center text-ly-12"></div>
                 {cardNumberErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{cardNumberErrorMsg}</p>}
             </div>
             <div className="mt-3 flex justify-between gap-4">
                 <div className="w-full">
-                    <div id={expiryId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></div>
+                    <div id={expiryId} className="w-full box-border bg-white border-1 border-ly-inputborder h-12.5 pl-4 flex items-center text-ly-12"></div>
                     {expiryErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{expiryErrorMsg}</p>}
                 </div>
                 <div className="w-full">
-                    <div id={cvcId} className="w-full box-border border-1 border-ly-inputborder h-11.5 pl-3 flex items-center"></div>
+                    <div id={cvcId} className="w-full box-border bg-white border-1 border-ly-inputborder h-12.5 pl-4 flex items-center text-ly-12"></div>
                     {cvcErrorMsg && <p className="text-ly-12 text-ly-errorcolor">{cvcErrorMsg}</p>}
                 </div>
             </div>
            
-            <div className="mt-2 w-full flex items-center">
+            {/* <div className="mt-2 w-full flex items-center">
                 <svg className="flex-none w-4.5 h-4.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="1.4em" height="1.4em" viewBox="0 0 24 24" id="pci-svg">
                     <path d="m18,8l-1,0l0,-2c0,-2.76 -2.24,-5 -5,-5s-5,2.24 -5,5l0,2l-1,0c-1.1,0 -2,0.9 -2,2l0,10c0,1.1 0.9,2 2,2l12,0c1.1,0 2,-0.9 2,-2l0,-10c0,-1.1 -0.9,-2 -2,-2zm-6,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2s-0.9,2 -2,2zm3.1,-9l-6.2,0l0,-2c0,-1.71 1.39,-3.1 3.1,-3.1c1.71,0 3.1,0.92125 3.1,2.84372l0,-0.78122l0,3.0375z"></path>
                 </svg>
                 <p className="text-ly-12 leading-ly-18">
                     Your card details are protected using PCI DSS v3.2 security standards.      
                 </p>
-            </div>
+            </div> */}
         </div>
     )
 }

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

@@ -1,6 +1,7 @@
 "use client";
 
 import Image from "next/image";
+import clsx from "clsx";
 import { type Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import {useStore} from "@/utils/hooks/useConfig";
@@ -116,15 +117,24 @@ export function PaymentMethodCheckout({
                 {
                     paymentMethods.map((item) => {
                         return (
-                            <li className="w-full mt-3" key={item.method}>
+                            <li key={item.method}
+                                className={clsx(
+                                    "w-full mt-3",
+                                    selectedPaymentMethod === 'airwallex' && item.method === 'airwallex' && "bg-[#ECF7F4] border-1 border-ly-green rounded-xs"
+                                )}
+                            >
                                 <label htmlFor={"paymentmethod-" + item.method}
-                                    className="group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
+                                    className={clsx(
+                                        "group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm", 
+                                        selectedPaymentMethod !== 'airwallex' && "has-checked:bg-[#ECF7F4] has-checked:border-1 has-checked:border-ly-green has-checked:rounded-xs",
+                                        selectedPaymentMethod === 'airwallex' && "has-checked:bg-[#ECF7F4]"
+                                    )}
                                 >
                                     <p className="pr-2 flex-none relative">
                                         {/**这里的width=32 height=32仅仅是让next.js有一个值计算图片的比例,并不能决定图片的渲染尺寸 */}
-                                        <Image className="block h-8 w-auto" 
-                                            width={32} 
-                                            height={32} 
+                                        <Image className="block h-6 w-auto" 
+                                            width={57} 
+                                            height={24} 
                                             src={getPaymentMethodLogo(item.method)} 
                                             alt={"Payment Method " + item.title} 
                                         />
@@ -150,7 +160,7 @@ export function PaymentMethodCheckout({
                                     />
                                 </label>
                                 { item.method === "airwallex" &&
-                                    <div className="w-full mt-3"
+                                    <div className="w-full box-border px-3 pb-3"
                                         style={{display: airwallexOtherInfoShow ? 'block' : 'none'}}
                                     >
                                         <AirwallexCardInput />

+ 30 - 26
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -19,57 +19,61 @@ export default function PromotionsDetails({cartData}:{
                     <span>Subtotal</span>
                     <span>{cartData.formattedSubtotal}</span>
                 </li>
-                {cartData.shippingAmount !== 0 &&
+                
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Shipping Fee</span>
+                    <span>{cartData.formattedShippingAmount}</span>
+                </li>
+                {cartData.discountAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>Shipping Method</span>
-                        <span>{cartData.formattedShippingAmount}</span>
+                        <span>Discount ({cartData.couponCode})</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedDiscountAmount}</span>
                     </li>
                 }
-                {cartData.earnableRewardPoints > 0 &&
+                {cartData.giftcardAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>You Earn</span>
-                        <span>{cartData.earnableRewardPoints} Reward Points</span>
+                        <span>Gift Card</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
                     </li>
                 }
-                {cartData.shippingInsuranceAmount !== 0 &&
+                {cartData.vipDiscountAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>{title}</span>
-                        <span>{cartData.formattedShippingInsuranceAmount}</span>
+                        <span>Plus Save</span>
+                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedVipDiscountAmount}</span>
                     </li>
                 }
                 {cartData.vipPlusAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>Vip Plus Fee</span>
+                        <span>Asteriahair Plus Fee</span>
                         <span>{cartData.formattedVipPlusAmount}</span>
                     </li>
                 }
                 {cartData.rewardPointsAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>You Redeem ({cartData.rewardPointsUsed} Reward Points)</span>
+                        <p>You Redeem <br/><span className="text-ly-placeholder">({cartData.rewardPointsUsed} Reward Points)</span></p>
                         <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedRewardPointsAmount}</span>
                     </li>
                 }
-                {cartData.discountAmount !== 0 &&
-                    <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>Coupon</span>
-                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedDiscountAmount}</span>
-                    </li>
-                }
-                {cartData.vipDiscountAmount !== 0 &&
+                {cartData.earnableRewardPoints > 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>Plus</span>
-                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedVipDiscountAmount}</span>
+                        <span>You Earn</span>
+                        <span>{cartData.earnableRewardPoints} Reward Points</span>
                     </li>
                 }
-                {cartData.giftcardAmount !== 0 &&
+                {cartData.shippingInsuranceAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                        <span>Gift Card</span>
-                        <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                        <span>{title}</span>
+                        <span>{cartData.formattedShippingInsuranceAmount}</span>
                     </li>
                 }
-                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
-                    <span>Grand Total</span>
-                    <span>{cartData.formattedGrandTotal}</span>
+                
+                
+                
+                
+                
+                <li className="flex justify-between items-center mt-5 pt-4 border-t-1 border-[#ACBDB8]">
+                    <span className="text-ly-12">Grand Total</span>
+                    <span className="text-ly-14 font-bold">{cartData.formattedGrandTotal}</span>
                 </li>
             </ul>
         </div>

+ 36 - 27
src/app/(checkout)/checkout/_components/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx

@@ -2,9 +2,11 @@
 
 import { useRouter } from 'next/navigation';
 import {useState, type ChangeEvent} from "react";
+import Big from 'big.js';
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useLoginModal} from "@/providers/LoginModalProvider";
+import { useStore } from "@/utils/hooks/useConfig";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
@@ -12,6 +14,10 @@ import type {CartDetail} from "@/types/cart/type";
 
 // earnableRewardPoints 判断是否开启下单送积分
 
+function calculateRewardPointsMoney(points: number, rate: number) {
+    return new Big(points).times(rate).toFixed(2);
+}
+
 export default function RewardPointsRedeemMoney({
     isGuest,
     cartUsedPoints,
@@ -27,6 +33,7 @@ export default function RewardPointsRedeemMoney({
 }) {
     const cartUsedPointsStr = String(cartUsedPoints);
     const router = useRouter();
+    const {store} = useStore();
     const {openLoginModal} = useLoginModal();
     const {saveCheckoutCart} = useSaveCheckoutCart();
     const { showToast } = useCustomToast();
@@ -101,44 +108,46 @@ export default function RewardPointsRedeemMoney({
                     Please <button className="underline" onClick={guestUserClick}>LOGIN</button> to use reward points
                 </div>
             :
-            <div className="w-full">
-                <div className="relative box-border w-full flex justify-between items-center">
-                    <span className="text-ly-24 font-medium">Reward points redeem</span>
-                
-                    
-                    <div className="flex items-center">
-                        {amount > 0 &&
-                        <span className="flex items-center text-ly-14 px-2 flex-none h-6 bg-ly-gold mr-2">
-                            -{formatRewardPointsAmount}
-                        </span>
-                        }
-                        
-                        <button className="w-6 h-6 flex-none">
-                            <svg className="block w-full h-full" 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>
-                        </button>
-                    </div>
+            <div className="w-full pt-6 border-t-1">
+                <div className="relative box-border w-full text-ly-24 font-medium leading-6">
+                    Redeem Points
                 </div>
                 
                 <div className="w-full mt-3">
-                    <div className="box-border w-full bg-ly-middlegray p-3 rounded-md">
-                        
-                        <p className="text-ly-12">
-                            You have <span className="font-bold">{hasRewardPoints} Reward Points</span>  available.  100 point = $1. Input points value below to redeem.
+                    <div className="box-border w-full h-30 px-4 py-3 bg-linear-to-b from-[#D9FFF2] to-[#ffffff]">
+                        <p className="text-ly-12">Available Points:</p>
+                        <p className="mt-2">
+                            <span className="text-ly-48 font-bold leading-12">{hasRewardPoints}</span>
+                            <span className="text-ly-14 ml-2">≈${calculateRewardPointsMoney(hasRewardPoints,store.rewardPointsRate)}</span>
                         </p>
-                        <div className="flex justify-between items-center mt-3 box-border w-full border-1 h-12 rounded-sm pl-3 pr-2 bg-white">
-                            <input className="focus:outline-none"
+                    </div>
+                    <div className="box-border w-full mt-3">
+                        
+                        
+                        <div className="flex justify-between items-center box-border w-full border-1 h-11 rounded-sm pl-3 pr-1 bg-white">
+                            <input className="focus:outline-none w-40 text-ly-12"
                                 type="text"
                                 value={usedPoints === '0' ? '' : usedPoints}
                                 placeholder="Reward Points"
                                 onChange={pointsInputChange}
                             />
-                            <button className="flex items-center text-ly-14 rounded-sm bg-ly-green text-white px-2 h-8"
-                                onClick={submitPointsUse}
-                            >
-                                {cartUsedPoints > 0 ? 'Cancel' : 'Apply'}
-                            </button>
+                            <div className="flex-none flex items-center">
+                                {amount > 0 && 
+                                    <span className="flex items-center h-6 px-0.5 text-ly-14 bg-ly-gold mr-2">
+                                        -{formatRewardPointsAmount}
+                                    </span>
+                                }
+                                <button className="flex items-center text-ly-12 rounded-sm bg-ly-green text-white px-5 h-9"
+                                    onClick={submitPointsUse}
+                                >
+                                    {cartUsedPoints > 0 ? 'Cancel' : 'Apply'}
+                                </button>
+                            </div>
                         </div>
                     </div>
+                    <p className="text-ly-12 mt-1 text-ly-placeholder">
+                        Exchange rate: 100 Points = ${100 * store.rewardPointsRate}
+                    </p>
                     
                 </div>
             </div>

文件差異過大導致無法顯示
+ 2 - 2
src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx


+ 5 - 56
src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx

@@ -6,18 +6,13 @@ import type {CartDetail} from "@/types/cart/type";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail } from "@/utils/cartDetailTools";
+import { getLeftDays } from "@/utils/helper";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import CommonModal from "@/components/theme/ui/CommonModal";
+import { staticVipData } from "@/utils/constants";
 import {useLoginModal} from "@/providers/LoginModalProvider";
+import { VipPlusRulesModal } from "@/components/common/VipPlusRulesModal/VipPlusRulesModal";
 
-// 问题: plus金额,折扣,时间是前端写死的。写死金额只显示美金数字,不随货币改变;
-const staticVipData = {
-    price: '$19.90',
-    linePrice: '$29.90',
-    days: '180',
-    discount: '5%'
-};
 
 export default function CheckoutVipPlus({
     cartData,
@@ -116,26 +111,6 @@ export default function CheckoutVipPlus({
         });
     };
 
-    
-
-    // vipExpireDate: "2027-01-30T22:40:33-08:00"
-    const getVipLeftDays = (vipExpireDate: string) => {
-        const target = new Date(vipExpireDate);
-        const now = new Date(); // 当前时间
-
-        // 计算相差的毫秒数
-        let diffMs = target.getTime() - now.getTime(); 
-        if(diffMs <= 0) {
-            diffMs = 0;
-        }
-
-        // 转换为天数 1.1天 显示2天
-        const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
-
-        return diffDays;
-
-    }
-
     return (<>
         {cartData.isVip && 
             <div className="relative box-border w-full ly-vipplus-bg p-3.75">
@@ -147,7 +122,7 @@ export default function CheckoutVipPlus({
                         </button>
                     </div>
                     <div className="text-ly-12">
-                        {getVipLeftDays(cartData.vipExpireDate)} days left
+                        {getLeftDays(cartData.vipExpireDate)} days left
                     </div>
                 </div>
                 <div className="flex justify-between items-center mt-3.75">
@@ -191,33 +166,7 @@ export default function CheckoutVipPlus({
                 </div>
             </div>
         }
-
-        <CommonModal 
-            destroyOnClose={true}
-            isOpen={isShow}
-            onClose={toggleModal}
-            contentClassName={"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 rounded-xl bg-white px-3"}
-            body={
-                <div className="relative py-4">
-                    <div className="text-center text-ly-16 font-bold">Asteriahair Plus</div>  
-                    <p className="text-center text-ly-12 leading-5 mt-3">
-                        In order to provide customers with a better shopping experience, Asteriahair has specially launched Asteriahair PLUS membership (no refund after purchase). PLUS members can enjoy unlimited free shipping, 5% off on each order within 180 days, exclusive instant discounts, exclusive customer service, free returns and other membership privileges.
-                    </p>
-                    <p className="text-center text-ly-12 leading-5 mt-2">
-                        We hope to establish a long-term win-win relationship with you through PLUS. At the same time, we will continue to enrich the rights and interests of PLUS members and provide you with more complete services.
-                    </p>
-                    <p className="text-center text-ly-12 leading-5 mt-2">
-                        Thank you for choosing Asteriahair. We are dedicated to creating the ultimate shopping experience for you!(The final right of interpretation belongs to Asteriahair)
-                    </p>
-                    <p className="text-center text-ly-12 leading-5 mt-2">
-                        Note: PLUS membership can't be used for wholesale orders, clearance and flash sale products.
-                    </p>
-                    <button className="absolute top-4 right-4 w-6 h-6" onClick={() => toggleModal(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>
-            }
-        />
+        <VipPlusRulesModal isShow={isShow} onClose={() => toggleModal(false)} />
 
     </>);
 }

+ 25 - 21
src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx

@@ -37,7 +37,7 @@ function computeSaveAmount(cartData: CartDetail) {
     // 优惠金额 礼品卡,plus
     const x = Big(cartData.discountAmount).plus(cartData.giftcardAmount);
 
-    return Big(cartData.vipPlusAmount).plus(x).toFixed(2);
+    return Big(cartData.vipDiscountAmount).plus(x).toFixed(2);
 }
 
 /**
@@ -354,18 +354,20 @@ export function CartWrapper({
                                 alt="Plus Tag"
                             />
                         }
-                        Total:
+                        Total({cartData.itemsQty > 1 ? `${cartData.itemsQty} items` : `${cartData.itemsQty} item`}):
                     </div>
                     <button className="flex items-center justify-end" onClick={showAmountDetail} type="button">
                         <span className="w-6 h-6 flex-none">
                             <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M18 9L12 15L6 9"></path></svg>
                         </span>
-                        <span className="flex-none flex items-center text-ly-14 bg-ly-gold h-6 px-2 ml-2">
-                            -<Price className="inline"
-                                amount={saveAmount}
-                                currencyCode={currentCurrencyCode}
-                            />
-                        </span>
+                        {Number(saveAmount) > 0 &&
+                            <span className="flex-none flex items-center text-ly-14 bg-ly-gold h-6 px-2 ml-2">
+                                -<Price className="inline"
+                                    amount={saveAmount}
+                                    currencyCode={currentCurrencyCode}
+                                />
+                            </span>
+                        }
                         <Price className="ml-2 text-ly-24 font-bold"
                             amount={grandTotal}
                             currencyCode={currentCurrencyCode}
@@ -414,36 +416,38 @@ export function CartWrapper({
                                 {cartData.formattedSubtotal}
                             </span>
                         </li>
-                        {/* {cartData.earnableRewardPoints > 0 &&
-                            <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
-                                <span>You Earn</span>
-                                <span>{cartData.earnableRewardPoints} Reward Points</span>
-                            </li>
-                        } */}
+                        
                         {cartData.vipPlusAmount !== 0 &&
                             <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
-                                <span>Vip Plus Fee</span>
+                                <span>Asteriahair Plus Fee</span>
                                 <span>{cartData.formattedVipPlusAmount}</span>
                             </li>
                         }
                         {cartData.discountAmount !== 0 &&
                             <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
-                                <span>Coupon</span>
+                                <span>Discount ({cartData.couponCode})</span>
                                 <span className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">-{cartData.formattedDiscountAmount}</span>
                             </li>
                         }
+                        {cartData.giftcardAmount !== 0 &&
+                            <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
+                                <span>Gift Card</span>
+                                <span className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                            </li>
+                        }
                         {cartData.vipDiscountAmount !== 0 &&
                             <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
-                                <span>Plus</span>
+                                <span>Plus Save</span>
                                 <span className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">-{cartData.formattedVipDiscountAmount}</span>
                             </li>
                         }
-                        {cartData.giftcardAmount !== 0 &&
+                        
+                        {/* {cartData.earnableRewardPoints > 0 &&
                             <li className="flex items-center justify-between text-ly-12 mt-4 first:mt-0">
-                                <span>Gift Card</span>
-                                <span className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                                <span>You Earn</span>
+                                <span>{cartData.earnableRewardPoints} Reward Points</span>
                             </li>
-                        }
+                        } */}
                     </ul>
                     <p className="mt-2 text-ly-12 leading-5 text-ly-placeholder">
                         Total is an estimate. Final amount shown at checkout

+ 39 - 22
src/app/(checkout)/checkout/cart/page.tsx

@@ -1,3 +1,4 @@
+import { Suspense } from "react";
 import { createHash } from "crypto";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import { GET_CART_ITEM, GET_CUSTOMER_ORDERS } from "@/graphql";
@@ -8,7 +9,8 @@ import TitleHeader from "@components/layout/navbar/TitleHeader";
 import { EmptyCart } from "./_components/EmptyCart";
 import { PromiseInCart } from "./_components/PromiseInCart";
 import { CartWrapper } from "./_components/CartWrapper";
-import { UnpaidOrderSection } from "./_components/UnpaidOrderSection";
+import { UnpaidOrderSection } from "@/components/cart/UnpaidOrder/UnpaidOrderSection";
+import { UnpaidOrderSectionLoading } from "@/components/cart/UnpaidOrder/UnpaidOrderSectionLoading";
 /**
  * 购物车页 
  * 登录用户的话,获取用户地址列表 从auth session 中获取email
@@ -25,26 +27,38 @@ export default async function CheckoutPage() {
 
     const session = await auth(); // 游客是null
     const isGuest = session === null ? true : false;
-    let unpaidOrder: Omit<OrderDetails, 'addresses'> | null = null;
-    let unpaidOrderExpiried = false;
+
+    
+    let unpaidOrderPromise: Promise<{success: boolean;orderDetail: Omit<OrderDetails, "addresses"> | null;error?:any;expiried: boolean | null;}> | null = null;
     if(!isGuest) {
-        try {
-            const {data: ordersResponse } = await serverGraphqlFetch<OrderListData,OrderListDataVariables>({
-                query: GET_CUSTOMER_ORDERS,
-                variables: {
-                    first: 1,
-                    status: 'pending'
-                }
-            }); 
-            unpaidOrder = ordersResponse?.customerOrders?.edges[0]?.node ?? null;
-            if(unpaidOrder) {
-                const countDownEnd = (new Date(unpaidOrder.createdAt)).getTime() + 24*3600*1000;
+        unpaidOrderPromise = serverGraphqlFetch<OrderListData,OrderListDataVariables>({
+            query: GET_CUSTOMER_ORDERS,
+            variables: {
+                first: 1,
+                status: 'pending'
+            }
+        }).then((result) => {
+            const orderInfo = result.data?.customerOrders?.edges[0]?.node ?? null;
+            let unpaidOrderExpiried = false;
+            if(orderInfo) {
+                const countDownEnd = (new Date(orderInfo.createdAt)).getTime() + 24*3600*1000;
                 unpaidOrderExpiried = countDownEnd <= (new Date()).getTime() ? true : false;
             }
+            return {
+                success: true,
+                orderDetail: orderInfo,
+                expiried: unpaidOrderExpiried
+            };
+        }).catch((err) => {
+            console.error('Get unpaid order error:',err);
+            return {
+                success: false,
+                orderDetail: null,
+                expiried: null,
+                error: err
+            };
+        });
             
-        } catch(err) {
-            console.error('get unpaid order:', err)
-        }
     }
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
         query: GET_CART_ITEM
@@ -60,11 +74,14 @@ export default async function CheckoutPage() {
         <>
             <TitleHeader title={title} />
             <PromiseInCart />
-            {(unpaidOrder && !unpaidOrderExpiried) &&
-
-                <UnpaidOrderSection 
-                    orderDetail={unpaidOrder}
-                />
+            {unpaidOrderPromise &&
+                <Suspense fallback={<UnpaidOrderSectionLoading />}>
+                    <div className="box-border w-full px-4 mt-4">
+                        <UnpaidOrderSection 
+                            orderInforPromise={unpaidOrderPromise}
+                        />
+                    </div>
+                </Suspense>
 
             }
             

+ 2 - 2
src/app/(checkout)/checkout/continuetopay/loading.tsx

@@ -41,8 +41,8 @@ export default function Loading() {
             
                 <div className="w-full box-border px-4 mt-6">
                     <div className="flex items-center">
-                        <span className="text-ly-12 flex-none mr-1 bg-skeletonbg">
-                            Order Summary(2)
+                        <span className="text-ly-12 flex-none mr-1 w-40 h-3 bg-skeletonbg">
+                            
                         </span>
                         <button className="flex-none w-4 h-4 bg-skeletonbg" type="button"></button>             
                     </div>

+ 5 - 23
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,16 +1,12 @@
-import {Suspense} from "react";
 import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
 import { auth } from "@/utils/auth/auth-helper";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
-import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
 import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS } from "@/graphql";
-// import { Country } from "@/types/checkout/type";
 import type { OrderDetailsData } from "@/types/customer/type";
 import type { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
-import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
-import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
 import TitleHeader from "@components/layout/navbar/TitleHeader";
+import { ContinueToPayWrapper } from "../_components/ContinueToPay/ContinueToPayWrapper";
 
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
@@ -82,24 +78,10 @@ export default async function ContinueToPay({searchParams}: {
                 </Link>
             </div>
         :
-            <div className="w-full pb-8">
-            
-                <ContinueToPayOrderInfo orderDetail={orderDetailData} />
-            
-                <div className="mt-6 box-border px-4">
-                    <h3 className="text-ly-24 font-medium">Payment Method</h3>
-
-                </div>
-                <Suspense fallback={<LoadingPaymentMethod/>}> 
-                    <PaymentMethodContinueTpPay 
-                        orderId={orderDetailData._id}
-                        orderDetail={orderDetailData}
-                        paymentMethodsDataPromise={paymentMethodsResponse}
-                        defaultPaymentMethod={orderDetailData.payment}
-                    />
-                </Suspense>
-                
-            </div>
+            <ContinueToPayWrapper 
+                orderDetailData={orderDetailData}
+                paymentMethodsPromise={paymentMethodsResponse}
+            />
         }
     </>);
   

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

@@ -6,6 +6,7 @@ import { GET_CART_ITEM } from "@/graphql";
 import { auth } from "@/utils/auth/auth-helper";
 import type { GetCartItemData } from "@/types/cart/type";
 import TitleHeader from "@components/layout/navbar/TitleHeader";
+import { PromiseInCart } from "./cart/_components/PromiseInCart";
 
 /**
  * 进入结账页 
@@ -35,6 +36,7 @@ export default async function CheckoutPage() {
     return (
         <>
             <TitleHeader title="Checkout" />
+            <PromiseInCart />
             <CheckoutWrapper key={cartDatahash}
                 cartDetailData={cartDetails}
                 loginEmail={session?.user?.email || ""}

+ 4 - 26
src/app/(public)/customer/account/_components/AccountHeader.tsx

@@ -1,40 +1,18 @@
 "use client";
 import Link from "next/link";
 import { useState } from "react";
+import type {CustomerVipGrowthData,CustomerProfileUser} from "@/types/api/customer/profile/type";
 import SettingModal from "@/components/customer/SettingModal";
 import AccountVipPopup from "./AccountVipPopup";
 import Image from "next/image";
-type NextLevelInfo = {
-  group_id: number;
-  group_name: string;
-  growth_level_name: string;
-  min_growth_value: number;
-  icon: string | null;
-  require_first_order: boolean;
-};
 
-export type VipGrowthData = {
-  growth_value: number;
-  level_name: string;
-  discount_rate: number;
-  level_icon: string | null;
-  benefits: string[];
-  next_level: NextLevelInfo;
-  growth_to_next_level: string; // 提示文案,如"First order required"
-  expires_at: string | null;
-  first_order_completed: boolean;
-};
 interface AccountHeaderProps {
-  user: {
-    name: string;
-    email: string;
-    image_url?: string | null;
-  };
+  user: CustomerProfileUser;
 
   points: number;
   coupon_count: number;
   wishlist_count?: number;
-  vipData: VipGrowthData;
+  vipData: CustomerVipGrowthData;
 }
 
 export default function AccountHeader({
@@ -53,7 +31,7 @@ export default function AccountHeader({
     progressText = `growth value ${vipData.growth_value}/${vipData.next_level.min_growth_value}`;
   } else {
   
-    progressText = vipData.growth_to_next_level;
+    progressText = String(vipData.growth_to_next_level);
     progressPercent = 0; 
   }
   return (

文件差異過大導致無法顯示
+ 42 - 0
src/app/(public)/customer/account/_components/VipPlusCard.tsx


+ 63 - 3
src/app/(public)/customer/account/page.tsx

@@ -1,7 +1,15 @@
+import { Suspense } from "react";
+import { GET_CUSTOMER_ORDERS } from "@/graphql";
+import { auth } from "@/utils/auth/auth-helper";
+import type { OrderListData, OrderListDataVariables, OrderDetails } from "@/types/customer/type";
+import type {CustomerProfileResponse} from "@/types/api/customer/profile/type";
 import AccountSwiper from "@components/customer/accountSwiper";
 import AccountHeader from "./_components/AccountHeader"
 import AccountBottomHrefSwiper from "./_components/AccountBottomHrefSwiper"
-import { restApiFetch } from "@utils/bagisto/index";
+import { restApiFetch, serverGraphqlFetch } from "@utils/bagisto/index";
+import { UnpaidOrderSection } from "@/components/cart/UnpaidOrder/UnpaidOrderSection";
+import { UnpaidOrderSectionLoading } from "@/components/cart/UnpaidOrder/UnpaidOrderSectionLoading";
+import { VipPlusCard } from "./_components/VipPlusCard";
 // import Link from "next/link";
 const orderNavs = [
   {
@@ -33,12 +41,48 @@ const orderNavs = [
 ];
 
 const MyAccountPage =async () => {
-  const res = await restApiFetch({
+
+  const session = await auth(); // 游客是null
+  const isGuest = session === null ? true : false;
+  
+ 
+  let unpaidOrderPromise: Promise<{success: boolean;orderDetail: Omit<OrderDetails, "addresses"> | null;error?:any;expiried: boolean | null;}> | null = null;
+  if(!isGuest) {
+        unpaidOrderPromise = serverGraphqlFetch<OrderListData,OrderListDataVariables>({
+            query: GET_CUSTOMER_ORDERS,
+            variables: {
+                first: 1,
+                status: 'pending'
+            }
+        }).then((result) => {
+            const orderInfo = result.data?.customerOrders?.edges[0]?.node ?? null;
+            let unpaidOrderExpiried = false;
+            if(orderInfo) {
+                const countDownEnd = (new Date(orderInfo.createdAt)).getTime() + 24*3600*1000;
+                unpaidOrderExpiried = countDownEnd <= (new Date()).getTime() ? true : false;
+            }
+            return {
+                success: true,
+                orderDetail: orderInfo,
+                expiried: unpaidOrderExpiried
+            };
+        }).catch((err) => {
+            console.error('Get unpaid order error:',err);
+            return {
+                success: false,
+                orderDetail: null,
+                expiried: null,
+                error: err
+            };
+        });
+  }
+
+
+  const res = await restApiFetch<{data:CustomerProfileResponse}>({
     api: `/customer/profile`,
     method: "GET",
     isRoute: false,
   });
-console.log('accountres-----------------------------:',res);
   return (
     <div className="min-h-screen bg-white pt-2 px-2">
       <AccountHeader
@@ -47,6 +91,22 @@ console.log('accountres-----------------------------:',res);
         coupon_count={res.body.data.coupon_count}
         vipData={res.body.data.growth}
       />
+      {unpaidOrderPromise &&
+          <Suspense fallback={<UnpaidOrderSectionLoading />}>
+            
+            <UnpaidOrderSection 
+                orderInforPromise={unpaidOrderPromise}
+            />
+            
+          </Suspense>
+
+      }
+      {res.body.data.plus.is_plus_member && 
+        <div className="box-border w-full pt-6">
+            <VipPlusCard customerPlusInfo={res.body.data.plus} />
+        </div>
+      }
+
       {/* 我的订单 */}
       <div className="">
         <h3 className="mt-8 ml-4 text-base">My Order</h3>

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

@@ -5,7 +5,7 @@ import Link from "next/link";
 import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import type { OrderDetailsData } from "@/types/customer/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
-import Faqs from "./Faqs";
+import OrderFaqs from "@/components/order/OrderFaqs";
 import { useAppDispatch } from "@/store/hooks";
 import { fetchCartDetail } from "@/store/slices/cart-slice";
 export default function CancelOrFailure({
@@ -94,7 +94,7 @@ export default function CancelOrFailure({
                 </Link>
             }
             <div className="box-border w-full mt-6">
-                <Faqs />
+                <OrderFaqs />
             </div>
         </div>
     );

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

@@ -8,7 +8,7 @@ import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import type { OrderDetailsData } from "@/types/customer/type";
 import {deleteGuestCookieAction} from "@/actions";
 import {getAddressFromOrderDetailAddressList,getProductAdditionalInfo} from "@/utils/orderDetailTools";
-import Faqs from "./Faqs";
+import OrderFaqs from "@/components/order/OrderFaqs";
 import { Price } from "@components/theme/ui/Price";
 
 /**
@@ -74,7 +74,7 @@ export default function OrderDetailWrapper({
                 </Link>
             </div>
             <div className="box-border px-4 w-full mt-6">
-                <Faqs />
+                <OrderFaqs />
             </div>
             <div className="box-border px-4 w-full mt-6">
                 <h4 className="text-ly-24 font-medium">Order Information</h4>

+ 15 - 0
src/app/globals.css

@@ -350,5 +350,20 @@
     display: block;
     width: 100%;
   }
+
+  paypal-button.checkout-paypal-button,
+  paypal-basic-card-button.checkout-paypal-button,
+  paypal-pay-later-button.checkout-paypal-button {
+    --paypal-button-border-radius: 1.5rem;
+    height: 3rem;
+  }
+  .checkout-paypal-message {
+    --paypal-message-font-size: 0.75rem;
+    --paypal-message-text-align: left;
+  }
+  /* paypal-message {
+    --paypal-message-font-size: 3rem;
+    --paypal-message-text-align: left;
+  } */
 }
 

+ 5 - 1
src/app/layout.tsx

@@ -18,6 +18,7 @@ import type { Country } from "@/types/checkout/type";
 import type { CurrenciesData } from "@/types/currency/type";
 import {CURRENT_CURRENCY,CURRENT_LOCAL,CURRENT_CHANNEL} from "@/utils/constants";
 import AuthSessionGuard from "@/components/customer/AuthSessionGuard";
+import { CartBackRefresh } from "@/components/cart/CartBackRefresh";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -118,7 +119,9 @@ export default async function RootLayout({
     // 用户当前使用值
     currentCurrency: currentCurrency,
     currentLocale: currentLocale,
-    currentChannel: currentChannel
+    currentChannel: currentChannel,
+    // 其他配置
+    rewardPointsRate: 0.01, // 每个积分可兑换0.01美元
   };
 
 
@@ -147,6 +150,7 @@ export default async function RootLayout({
               <ReduxProvider>
                 <GlobalProviders>
                   {children}
+                  <CartBackRefresh />
                   <AddToCartModalWrapper />
                   <AuthSessionGuard />
                 </GlobalProviders>

+ 59 - 0
src/components/cart/CartBackRefresh.tsx

@@ -0,0 +1,59 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { usePathname, useRouter } from "next/navigation";
+
+const REFRESH_PATHS = [
+    "/checkout/cart",
+    "/checkout"
+];
+
+
+export function CartBackRefresh() {
+  const pathname = usePathname();
+  const router = useRouter();
+
+  // 记录浏览器历史导航的目标路径
+  const targetPathRef = useRef<string | null>(null);
+
+  // 确保 popstate 发生时,即使 pathname 没有变化,
+  // effect 也有机会重新检查目标路径
+  const [popstateVersion, setPopstateVersion] = useState(0);
+
+  useEffect(() => {
+    function handlePopState() {
+      targetPathRef.current = window.location.pathname;
+      setPopstateVersion((version) => version + 1);
+    }
+
+    window.addEventListener("popstate", handlePopState);
+
+    return () => {
+      window.removeEventListener("popstate", handlePopState);
+    };
+  }, []);
+
+  useEffect(() => {
+    const targetPath = targetPathRef.current;
+
+    // 目标路径还没与 Next.js 当前 pathname 同步时,
+    // 保留标记,等待 pathname 更新后再次检查
+    if (!targetPath || targetPath !== pathname) {
+      return;
+    }
+
+    // 只刷新指定页面
+    if (!REFRESH_PATHS.includes(pathname)) {
+      targetPathRef.current = null;
+      return;
+    }
+
+    // 消费本次历史导航标记,避免重复刷新
+    targetPathRef.current = null;
+
+    console.log("[CartBackRefresh] refresh:", pathname);
+    router.refresh();
+  }, [pathname, popstateVersion, router]);
+
+  return null;
+}

文件差異過大導致無法顯示
+ 81 - 31
src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx


+ 36 - 0
src/components/cart/UnpaidOrder/UnpaidOrderSectionLoading.tsx

@@ -0,0 +1,36 @@
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+export function UnpaidOrderSectionLoading() {
+    const arr = [1,2,3];
+    return (<div className="w-full box-border p-4 animate-pulse">
+            <div className="relative w-full box-border p-3">
+                <div className="flex items-center w-full">
+                    <span className="text-ly-12 bg-skeletonbg w-60 h-3"></span>
+                </div>
+    
+                <div className="relative">
+                    <div className="w-full flex gap-3 mt-2.5 overflow-x-scroll">
+                        {arr.map((i) => {
+                            return(
+                                <div className="relative w-21.5 h-21.5 flex-none last:mr-8 bg-skeletonbg" key={i}>
+                                    <Image className="object-cover" fill src={NOT_IMAGE} alt="placeholder image"/>
+                                </div>
+                            );
+                        })}
+                    </div>
+                </div>
+    
+                <div className="flex items-center justify-between w-full mt-3">
+                    <div className="flex items-center">
+                        <span className="text-ly-16 h-4 w-40 bg-skeletonbg"></span>
+                    
+                    </div>
+                    
+                    
+                    <div className="flex-none px-4 h-8 text-ly-14 font-bold bg-skeletonbg rounded-2xl"></div>
+                </div>
+    
+                <button className="absolute top-3 right-3 w-6 h-6 bg-skeletonbg"></button>
+            </div>
+    </div>);
+}

+ 2 - 2
src/components/common/CountDown.tsx

@@ -14,8 +14,8 @@ export default function CountDown({
     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 defaultBoxClassName = boxClassName || "flex items-center gap-2 flex-none text-ly-16 text-white";
+    const defaultTimerClassName = timerClassName || "flex items-center justify-center w-8 h-8 bg-white rounded-sm text-ly-16 text-ly-middlegreen";
     
     const timerRef = useRef<number | null>(null);
     const [seconds, setSeconds] = useState(() => {

+ 43 - 0
src/components/common/VipPlusRulesModal/VipPlusRulesModal.tsx

@@ -0,0 +1,43 @@
+"use client";
+
+import CommonModal from "@/components/theme/ui/CommonModal";
+
+export function VipPlusRulesModal({
+    isShow,
+    onClose
+}: {
+    isShow: boolean;
+    onClose: () => void;
+}) {
+
+
+    return (<>
+        <CommonModal 
+            destroyOnClose={true}
+            isOpen={isShow}
+            onClose={onClose}
+            contentClassName={"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 rounded-xl bg-white px-3"}
+            body={
+                <div className="relative py-4">
+                    <div className="text-center text-ly-16 font-bold">Asteriahair Plus</div>  
+                    <p className="text-center text-ly-12 leading-5 mt-3">
+                        In order to provide customers with a better shopping experience, Asteriahair has specially launched Asteriahair PLUS membership (no refund after purchase). PLUS members can enjoy unlimited free shipping, 5% off on each order within 180 days, exclusive instant discounts, exclusive customer service, free returns and other membership privileges.
+                    </p>
+                    <p className="text-center text-ly-12 leading-5 mt-2">
+                        We hope to establish a long-term win-win relationship with you through PLUS. At the same time, we will continue to enrich the rights and interests of PLUS members and provide you with more complete services.
+                    </p>
+                    <p className="text-center text-ly-12 leading-5 mt-2">
+                        Thank you for choosing Asteriahair. We are dedicated to creating the ultimate shopping experience for you!(The final right of interpretation belongs to Asteriahair)
+                    </p>
+                    <p className="text-center text-ly-12 leading-5 mt-2">
+                        Note: PLUS membership can't be used for wholesale orders, clearance and flash sale products.
+                    </p>
+                    <button className="absolute top-4 right-4 w-6 h-6" onClick={onClose}>
+                        <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>
+            }
+        />
+
+    </>);
+}

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

@@ -1,7 +1,7 @@
 "use client";
 import {useLiveChat} from "@/providers/LiveChatProvider";
 
-export default function Faqs() { 
+export default function OrderFaqs() { 
 
     const {show} = useLiveChat();
     const showLiveChat = () => {

+ 4 - 2
src/components/theme/ui/kernel/confirm/api.ts

@@ -9,10 +9,12 @@ const createId = () =>{
 // let id = createId();
 
 export function confirmDialog(options: {
-  title: string;
-  content?: string;
+  title?: string;
+  content?: string | string[];
   noOk?: boolean;
   noCancel?: boolean;
+  okBtnTxt?: string;
+  cancelBtnTxt?: string;
 }) {
   return new Promise<boolean>((resolve) => {
     const state = confirmStore.getSnapshot();

+ 61 - 36
src/components/theme/ui/kernel/confirm/host.tsx

@@ -1,46 +1,71 @@
 "use client";
-
+import clsx from "clsx";
 import { AnimatePresence, motion } from "framer-motion";
 import { confirmStore } from "./store";
 import { useConfirmQueue } from "./useConfirm";
 
 export function ConfirmHost() {
-  const {queue} = useConfirmQueue();
-
-  const current = queue[0];
-
-//   if (!current) return null;
-
-  const close = (result: boolean) => {
-    current.resolve(result);
-
-    confirmStore.setState({
-      queue: queue.slice(1),
-    });
-  };
-
-  return (
-    <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>}
+    const {queue} = useConfirmQueue();
+
+    const current = queue[0];
+
+    //   if (!current) return null;
+
+    const close = (result: boolean) => {
+        current.resolve(result);
+
+        confirmStore.setState({
+            queue: queue.slice(1),
+        });
+    };
+
+    return (
+        <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="relative w-80 px-3 pb-6 pt-4 bg-white rounded-xl">
+                    <div className={clsx("text-center text-ly-16 font-bold mb-3", !current.title && 'text-white')}>
+                        {current.title ? current.title : 'notice'}
+                    </div>
+                    {Array.isArray(current.content) ? 
+                        current.content.map((txt,i) => {
+                            return <p key={i} className="text-ly-16 leading-5 text-center">
+                                {txt}
+                            </p>
+                        })
+                    : 
+                        <p className="text-ly-16 leading-5 text-center">{current.content}</p>
+
+                    }
+                    
+                    <div className="w-full flex justify-center flex-wrap gap-4 mt-4">
+                        {!current.noOk && <button type="button" 
+                            className="grow shrink basis-auto min-w-[calc(50%_-_0.5rem)] box-border h-12 border-1 flex justify-center items-center rounded-3xl text-ly-16" 
+                            onClick={() => close(true)}
+                        >
+                            {current.okBtnTxt ? current.okBtnTxt : 'Yes'}
+                        </button>}
+                        {!current.noCancel && <button type="button" 
+                            className="grow shrink basis-auto min-w-[calc(50%_-_0.5rem)] h-12 flex justify-center items-center rounded-3xl bg-ly-middlegreen text-ly-16 text-white" 
+                            onClick={() => close(false)}
+                        >
+                            {current.cancelBtnTxt ? current.cancelBtnTxt : 'Cancel'}
+                        </button>}
                     </div>
+                    {!current.noCancel &&
+                        <button type="button" className="absolute top-4 right-4 h-6 w-6" onClick={() => close(false)}>
+                            <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.2" strokeLinejoin="round" strokeLinecap="round"  d="M7 7L17 17"></path><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.2" strokeLinejoin="round" strokeLinecap="round"  d="M7 17L17 7"></path></svg>
+                        </button>
+                    }
                 </div>
-            {/* </div> */}
-        </motion.div>}
-    </AnimatePresence>
-  );
+            </motion.div>}
+        </AnimatePresence>
+    );
 }
 

+ 4 - 2
src/components/theme/ui/kernel/confirm/store.ts

@@ -2,10 +2,12 @@ import { createStore } from "../core/store";
 
 export type ConfirmItem = {
   id: string;
-  title: string;
-  content?: string;
+  title?: string;
+  content?: string | string[];
   noOk?: boolean;
   noCancel?: boolean;
+  okBtnTxt?: string;
+  cancelBtnTxt?: string;
   resolve: (v: boolean) => void;
 };
 

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

@@ -1,6 +1,7 @@
 import { init, type InitResult,createElement, type Payment } from '@airwallex/components-sdk';
 import { env } from "@/env";
 
+// 文档地址 https://www.airwallex.com/docs/js/payments/card-number
 // ============================================================
 // Types
 // ============================================================
@@ -189,9 +190,36 @@ class AirwallexManager {
     }
     
     this.createPromise = Promise.all([
-      createElement("cardNumber"),
-      createElement("cvc"),
-      createElement("expiry")
+      createElement("cardNumber",{
+        placeholder: '1234 1234 1234 1234',
+        style:{
+          base: {
+            'fontSize': '1rem',
+            '::placeholder': {'color': '#a6a6a6'} as any // 这里不得不断言为any, 因为airwallex sdk的类型定义有问题,属于官方bug
+            
+          } 
+        }
+      }),
+      createElement("cvc",{
+        placeholder: 'CVC',
+        style:{
+          base: {
+            'fontSize': '1rem',
+            '::placeholder': {'color': '#a6a6a6'} as any // 这里不得不断言为any, 因为airwallex sdk的类型定义有问题,属于官方bug
+            
+          } 
+        }
+      }),
+      createElement("expiry",{
+        placeholder: 'Month/Year',
+        style:{
+          base: {
+            'fontSize': '1rem',
+            '::placeholder': {'color': '#a6a6a6'} as any // 这里不得不断言为any, 因为airwallex sdk的类型定义有问题,属于官方bug
+            
+          } 
+        }
+      })
     ]).then(([cardNumber,cvc,expiry]) => {
 
       this.elements = {

+ 2 - 0
src/providers/ConfigProvider.tsx

@@ -22,6 +22,8 @@ export interface StoreConfig {
     currentLocale: string;
 
     currentChannel: string;
+
+    rewardPointsRate: number; //// 每个积分可兑换几美元
 }
 
 

+ 52 - 0
src/types/api/customer/profile/type.ts

@@ -0,0 +1,52 @@
+export interface CustomerProfileUser{
+    id: number;
+    name: string;
+    first_name: string;
+    last_name: string;
+    email: string;
+    phone: string | null;
+    image_url: string | null;
+    gender: string | null;
+    date_of_birth: string | null;
+}
+
+type CustomerNextLevelInfo = {
+  group_id: number;
+  group_name: string;
+  growth_level_name: string;
+  min_growth_value: number;
+  icon: string | null;
+  require_first_order: boolean;
+};
+
+export type CustomerVipGrowthData = {
+  growth_value: number;
+  level_name: string;
+  discount_rate: number;
+  level_icon: string | null;
+  benefits: string[];
+  next_level: CustomerNextLevelInfo;
+  growth_to_next_level: string | number; // 提示文案,如"First order required"
+  expires_at: string | null;
+  first_order_completed: boolean;
+};
+
+export interface CustomerPlusData {
+    is_plus_member: boolean;
+    remaining_days: number;
+    expires_at: string | null;
+    total_discount_amount: number;
+}
+
+export interface CustomerProfileResponse {
+    success: boolean;
+    message: string;
+    data: {
+        user: CustomerProfileUser;
+        growth: CustomerVipGrowthData;
+        points: number;
+        coupon_count: number;
+        order_count: number;
+        plus: CustomerPlusData;
+    }
+}

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

@@ -22,6 +22,8 @@ export interface GiftCardRespBody {
     per_page: number;
     total: number;
   };
+  retry_after?: number;
+  error?: string;
 }
 
 // restApiFetch返回外层结构

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

@@ -108,17 +108,17 @@ export interface OrderDetails {
     baseDiscountAmount: number;
     shippingAmount: number;
     baseShippingAmount: number;
-    vipPlusAmount: string;
-    vipDiscountAmount: string;
-    baseVipPlusAmount: string;
-    baseVipDiscountAmount: string;
+    vipPlusAmount: string | null;
+    vipDiscountAmount: string | null;
+    baseVipPlusAmount: string | null;
+    baseVipDiscountAmount: string | null;
     shippingInsuranceAmount: string;
     baseShippingInsuranceAmount: string;
     shippingInsuranceInvoiced: string;
     baseShippingInsuranceInvoiced: string;
-    giftcardNumber: string;
-    giftcardAmount: string;
-    baseGiftcardAmount: string;
+    giftcardNumber: string | null;
+    giftcardAmount: string | null;
+    baseGiftcardAmount: string | null;
     rewardPointsUsed: number;    
     rewardPointsAmount: number;
     baseRewardPointsAmount: number;

+ 15 - 8
src/utils/constants.ts

@@ -213,12 +213,19 @@ export const currencyCountryMap: Record<string, {icon: string;symbol: string; co
 };
 
 export const paymentMethodImageMap:Record<string, string> = {
-    klarna: '/image/payment/klarna.png',
-    afterpay: '/image/payment/afterpay.png',
-    applepay: '/image/payment/applepay.png',
-    paypal_smart_button: '/image/payment/paypal.png',
-    awxafterpay: '/image/payment/afterpay.png',
-    awxklarna: '/image/payment/klarna.png',
-    airwallex: '/image/payment/credit-card.png',
+    klarna: '/image/payment/klarna.webp',
+    afterpay: '/image/payment/afterpay.webp',
+    applepay: '/image/payment/applepay.webp',
+    paypal_smart_button: '/image/payment/paypal.webp',
+    awxafterpay: '/image/payment/afterpay.webp',
+    awxklarna: '/image/payment/klarna.webp',
+    airwallex: '/image/payment/credit.webp',
     default: '/image/payment/cash-icon.png'
-};
+};
+// 问题: plus金额,折扣,时间是前端写死的。写死金额只显示美金数字,不随货币改变;
+export const staticVipData = {
+    price: '$19.90',
+    linePrice: '$29.90',
+    days: '180',
+    discount: '5%'
+};

+ 18 - 0
src/utils/helper.ts

@@ -11,6 +11,24 @@ import { env } from '@/env';
 const __br = [100,115,118,45,50,48,50,53,46,48,52,46,49,57,45,55,101,50,57];
 export const __buildRevision = String.fromCharCode.apply(null, __br);
 
+// expireDate: "2027-01-30T22:40:33-08:00"
+export const getLeftDays = (expireDate: string) => {
+    const target = new Date(expireDate);
+    const now = new Date(); // 当前时间
+
+    // 计算相差的毫秒数
+    let diffMs = target.getTime() - now.getTime(); 
+    if(diffMs <= 0) {
+        diffMs = 0;
+    }
+
+    // 转换为天数 1.1天 显示2天
+    const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
+
+    return diffDays;
+
+}
+
 export const getCurrencyItem = (code: string, currencies: CurrencyItem[]) => {
     const currencyItem = currencies.find(currency => currency.code === code);
     return currencyItem;