Kaynağa Gözat

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

fogwind 5 gün önce
ebeveyn
işleme
ec91eaa226
52 değiştirilmiş dosya ile 1872 ekleme ve 930 silme
  1. 2 0
      README.md
  2. BIN
      public/image/addressbilling-bg.webp
  3. BIN
      public/image/giftcard-bg.webp
  4. BIN
      public/image/payment/afterpay.webp
  5. BIN
      public/image/payment/applepay.webp
  6. BIN
      public/image/payment/credit.webp
  7. BIN
      public/image/payment/klarna.webp
  8. BIN
      public/image/payment/paypal.webp
  9. 2 2
      src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx
  10. 3 3
      src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx
  11. 21 5
      src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx
  12. 1 0
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  13. 2 5
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  14. 38 29
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  15. 68 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueAddressInfo.tsx
  16. 445 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueCheckout.tsx
  17. 131 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinuePaymentMethod.tsx
  18. 0 250
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  19. 231 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayWrapper.tsx
  20. 3 3
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsInPendingOrder.tsx
  21. 30 25
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  22. 17 15
      src/app/(checkout)/checkout/_components/LostInsurance/LostInsurance.tsx
  23. 7 4
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalButton.tsx
  24. 5 5
      src/app/(checkout)/checkout/_components/PaymentMethodAdditional/AirwallexCardInput.tsx
  25. 31 7
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  26. 3 3
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  27. 37 33
      src/app/(checkout)/checkout/_components/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx
  28. 3 3
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  29. 5 55
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  30. 26 24
      src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx
  31. 11 10
      src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx
  32. 31 23
      src/app/(checkout)/checkout/cart/page.tsx
  33. 6 26
      src/app/(checkout)/checkout/continuetopay/page.tsx
  34. 34 121
      src/app/(public)/customer/(has-dashboard)/account/_components/AccountHeader.tsx
  35. 42 0
      src/app/(public)/customer/(has-dashboard)/account/_components/VipPlusCard.tsx
  36. 73 14
      src/app/(public)/customer/(has-dashboard)/account/page.tsx
  37. 12 1
      src/app/globals.css
  38. 5 1
      src/app/layout.tsx
  39. 59 0
      src/components/cart/CartBackRefresh.tsx
  40. 40 0
      src/components/cart/UnpaidOrder/UnpaidOrderLoading.tsx
  41. 35 25
      src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx
  42. 43 0
      src/components/common/VipPlusRulesModal/VipPlusRulesModal.tsx
  43. 4 2
      src/components/theme/ui/kernel/confirm/api.ts
  44. 44 17
      src/components/theme/ui/kernel/confirm/host.tsx
  45. 4 2
      src/components/theme/ui/kernel/confirm/store.ts
  46. 222 203
      src/lib/Airwallex/airwallexInit.ts
  47. 2 0
      src/providers/ConfigProvider.tsx
  48. 52 0
      src/types/api/customer/profile/type.ts
  49. 2 0
      src/types/api/gift/my-gift-cards.ts
  50. 7 7
      src/types/customer/order.ts
  51. 15 7
      src/utils/constants.ts
  52. 18 0
      src/utils/helper.ts

+ 2 - 0
README.md

@@ -368,6 +368,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 55. next-auth 中设置的cookie过期时间存在漂移问题:比如8月1号用户登录,设置的30天过期(8月31号过期)。
     但是如果用户8月3号又访问网站,那么过期时间会以8月3号往后延30天。不知道为什么会是这种机制。
 
+56. pc端 loading.tsx
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 
@@ -422,6 +423,7 @@ ssr + redux:
 第一次渲染时在服务端获取数据存入redux,后续在客户端操作时更新redux。
 购物车页和结账页不从redux中取数据,在服务端重新获取。
 但是在购物车页和结账页对购物车数据做的修改需要同步到redux。
+> 该方案有缺陷,已修改
 
 ## 关于结账页地址
 如果用户在结账页新建一个地址,这个地址的type是 cart_billing/cart_shipping。

BIN
public/image/addressbilling-bg.webp


BIN
public/image/giftcard-bg.webp


BIN
public/image/payment/afterpay.webp


BIN
public/image/payment/applepay.webp


BIN
public/image/payment/credit.webp


BIN
public/image/payment/klarna.webp


BIN
public/image/payment/paypal.webp


Dosya farkı çok büyük olduğundan ihmal edildi
+ 2 - 2
src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx


Dosya farkı çok büyük olduğundan ihmal edildi
+ 3 - 3
src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx


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

@@ -11,10 +11,12 @@ import type {CartDetail} from "@/types/cart/type";
 
 
 export default function CheckoutCoupon({
+    title = 'Discount',
     couponCode,
     discountAmount,
     onCouponChange
 }: {
+    title?: string;
     couponCode: string;
     discountAmount: string;
     onCouponChange: (payload:CartDetail) => void;
@@ -24,14 +26,20 @@ export default function CheckoutCoupon({
 
     const [codeValue, setCodeValue] = useState(couponCode);
     const [isSpread, setIsSpread] = useState(false);
+    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();
@@ -44,20 +52,27 @@ 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');
+            // showToast(saveRes.msg, 'danger');
+            setErrMsg(saveRes.msg);
         }
 
     };
     return (
         <>
             <div className="relative box-border w-full flex justify-between items-center">
-                <span className="text-ly-20 font-medium">Coupon Code</span>
+                <span className="text-ly-18 font-medium">{title}</span>
                
                 
                 <div className="flex items-center">
@@ -101,12 +116,13 @@ export default function CheckoutCoupon({
                         placeholder="Coupon Code"
                         onChange={codeInputChange}
                     />
-                    <button className="flex items-center justify-center text-ly-14 rounded-sm bg-ly-green text-white px-2 h-9 w-21"
+                    <button className="flex items-center justify-center text-ly-14 rounded-sm bg-ly-middlegreen text-white px-2 h-9 w-21"
                         onClick={submitCouponCode}
                     >
                         {couponCode && codeValue === couponCode ? 'Remove' : 'Apply'}
                     </button>
                 </div>
+                {errMsg && <p className="mt-3 text-ly-14 text-ly-errorcolor">{errMsg}</p>}
             </div>
 
         </>

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

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

@@ -103,9 +103,6 @@ export default function CheckoutProducts({
 
  
     return (<>
-        <div className="flex items-center text-ly-20 font-medium">
-            My Bag ({cartData.itemsQty > 1 ? String(cartData.itemsQty) + 'Items' : String(cartData.itemsQty) + 'Item'} )
-        </div>
         {/* <span className="text-ly-14 font-bold">{cartData.formattedSubtotal}</span> */}
      
         <div className="w-full max-h-100 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500/30">
@@ -114,14 +111,14 @@ export default function CheckoutProducts({
                     const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
                     const productOptions = product.options.edges.map((edge) => edge.node);
                     return (
-                        <div key={product.id} className="w-full flex gap-2 mt-4 pt-4 border-t-1 border-t-[#f0f0f0]">
+                        <div key={product.id} className="w-full flex gap-2 mt-4 first:mt-0 pb-4 border-b-1 border-[#f0f0f0] last:border-none">
                             <div className="relative w-33 h-44 flex-none">
                                 <Image fill className="object-cover" src={baseImage.medium_image_url || ''} alt={product.name} />
                             </div>
                             <div className="w-full flex flex-col justify-between">
                                 <div className="w-full">
                                     <div className="flex items-center gap-2 w-full">
-                                        <div className="w-full line-clamp-2 text-ly-14 text-ly-placeholder leading-5">
+                                        <div className="w-full line-clamp-2 text-ly-12 text-ly-placeholder leading-5">
                                             {product.name}
                                         </div>
                                         <button className="flex-none" onClick={() => deleteProduct(product.id)}>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 38 - 29
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx


Dosya farkı çok büyük olduğundan ihmal edildi
+ 68 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueAddressInfo.tsx


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

@@ -0,0 +1,445 @@
+"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 router = useRouter();
+    const { showToast } = useCustomToast();
+
+    const { paymentRepay, createPaymentCallback } = usePlaceOrder();
+
+    const orderCurrencyCode = orderDetail.orderCurrencyCode;
+    const orderId = orderDetail._id;
+
+    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 px-4 pb-6">
+                
+                <div className="w-full">
+                    <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
+                        </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
+                        </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}
+                        >
+                            Place Order
+                        </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>
+                <p className="text-ly-12 text-ly-placeholder leading-5 mt-2.5">
+                    If your payment fails, please check the error message. If none is displayed, please contact us.
+                </p>
+            </div>
+        </>
+    );
+  
+}
+
+

Dosya farkı çok büyük olduğundan ihmal edildi
+ 131 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinuePaymentMethod.tsx


+ 0 - 250
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -1,250 +0,0 @@
-"use client";
-
-
-import { useRouter } from 'next/navigation';
-import type { OrderDetails } from "@/types/customer/type";
-import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import ProductsInPendingOrder from "./ProductsInPendingOrder";
-import dynamic from "next/dynamic";
-import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
-import { Price } from "@components/theme/ui/Price";
-
-const CountDown = dynamic(() => import("@/components/common/CountDown"), {
-  ssr: false,
-});
-export default function ContinueToPayOrderInfo({
-    orderDetail
-}: {
-    orderDetail: OrderDetails;
-}) { 
-    
-    const router = useRouter();
-    
-
-    const countDownEnd = (new Date(orderDetail.createdAt)).getTime() + 24*3600*1000;
-
-    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
-    const billingAddress = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
-
-    const totalQty = orderDetail.totalQtyOrdered > 1 ? String(orderDetail.totalQtyOrdered) + 'Items' : '1 Item';
-    const productItems = orderDetail.items.edges.map((item) => {
-        return item.node;
-    });
-
-    // 倒计时结束跳转到首页
-    const handleCountDownEnd = () => { 
-        confirmDialog({
-            title: "Order Timeout",
-            content: "This order timeout, has been canceled. Will redirect to home page.",
-            noCancel: true,
-        }).then(() => {
-            router.replace('/');
-        });  
-    };
-
-
-    return(
-        <>
-            <div className="sticky top-15 z-10 box-border w-full h-14 px-4 flex items-center justify-between bg-ly-green">
-                <span className="text-ly-14 font-medium text-white">Awaiting For Payment</span>
-                <CountDown
-                    endTime={countDownEnd}
-                    onCountDownEnd={handleCountDownEnd}
-                />
-            </div>
-            
-            <div className="mt-6 box-border">
-                <h3 className="text-ly-20 font-medium">Shipping Address</h3>
-                <div className="w-full mt-4 box-border rounded-md border-1 px-4 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-14 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-6 h-6" 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-14 ml-2.5">
-                            {shippingAddress.address}, 
-                            {shippingAddress.city}, 
-                            {shippingAddress.state}, 
-                            {shippingAddress.postcode}, 
-                            {shippingAddress.country}
-                        </span>
-                    </div>
-                </div>  
-
-            </div>
-
-            <div className="mt-6 box-border">
-                <h3 className="text-ly-20 font-medium">Billing Address</h3>
-                <div className="w-full mt-4 box-border rounded-md border-1 px-4 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-14 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-6 h-6" 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-14 ml-2.5">
-                            {billingAddress.address}, 
-                            {billingAddress.city}, 
-                            {billingAddress.state}, 
-                            {billingAddress.postcode}, 
-                            {billingAddress.country}
-                        </span>
-                    </div>
-                </div>  
-
-            </div>
-             
-            <div className="w-full box-border mt-6 text-ly-20">
-                Order Summary({totalQty})      
-            </div>
-            <div className="relative w-full box-border mt-4">
-                <div className="w-full box-border max-h-90 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200">
-                    <ProductsInPendingOrder 
-                        products={productItems}
-                        currencyCode={orderDetail.orderCurrencyCode}
-                    />
-
-                </div> 
-            </div>
-            
-
-            <div className="box-border w-full mt-6">
-                <h3 className="text-ly-20 font-medium">Promotion Details</h3>
-                <div className="flex justify-between mt-4 first:mt-0">
-                    <p className="text-ly-14">Subtotal</p>
-                    <p className="text-ly-14">
-                        <Price 
-                            amount={orderDetail.subTotal}
-                            currencyCode={orderDetail.orderCurrencyCode}
-                        />
-                    </p>
-                </div>
-                {orderDetail.rewardPointsEarned > 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">You Earn</p>
-                        <p className="text-ly-14">{orderDetail.rewardPointsEarned} Reward Points</p>
-                    </div>
-                }
-                {orderDetail.shippingAmount !== 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Shipping</p>
-                        <p className="text-ly-14">
-                            <Price 
-                                amount={orderDetail.shippingAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {Number(orderDetail?.shippingInsuranceAmount) > 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Shipping</p>
-                        <p className="text-ly-14">
-                            <Price 
-                                amount={orderDetail.shippingInsuranceAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {orderDetail.taxAmount !== 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Tax</p>
-                        <p className="text-ly-14">
-                            <Price 
-                                amount={orderDetail.taxAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {(orderDetail.vipPlusAmount !== '0.00' && orderDetail.vipPlusAmount !== null)  &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Plus Fee</p>
-                        <p className="text-ly-14">
-                            <Price 
-                                amount={orderDetail.vipPlusAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {orderDetail.discountAmount !== 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Discount</p>
-                        <p className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">
-                            -<Price 
-                                amount={orderDetail.discountAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {(orderDetail.giftcardAmount !== '0.00' && orderDetail.giftcardAmount !== null) &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Gift Card</p>
-                        <p className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">
-                            -<Price 
-                                amount={orderDetail.giftcardAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {(orderDetail.vipDiscountAmount !== '0.00' && orderDetail.vipDiscountAmount !== null) &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">Plus Save</p>
-                        <p className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">
-                            -<Price 
-                                amount={orderDetail.vipDiscountAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-                {orderDetail.rewardPointsAmount > 0 &&
-                    <div className="flex justify-between mt-4 first:mt-0">
-                        <p className="text-ly-14">You Redeem ({orderDetail.rewardPointsUsed} Reward Points)</p>
-                        <p className="flex items-center h-6 px-2 bg-ly-gold text-ly-14">
-                            -<Price 
-                                amount={orderDetail.rewardPointsAmount}
-                                currencyCode={orderDetail.orderCurrencyCode}
-                            />
-                        </p>
-                    </div>
-                }
-
-                {/* <div className="flex justify-between mt-3 first:mt-0">
-                    <p className="text-ly-12 leading-ly-20">You Earn</p>
-                    <p className="text-ly-12 leading-ly-20">600 Reward Points</p>
-                </div> */}
-                <div className="flex justify-between mt-4 first:mt-0">
-                    <p className="text-ly-16">Total</p>
-                    <p className="text-ly-16 font-bold">
-                        <Price 
-                            amount={orderDetail.grandTotal}
-                            currencyCode={orderDetail.orderCurrencyCode}
-                        />
-                    </p>
-                </div>
-            </div>
-        </>
-    );
-}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 231 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayWrapper.tsx


+ 3 - 3
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsInPendingOrder.tsx

@@ -19,7 +19,7 @@ export default function ProductsInPendingOrder({
                 const baseImage = JSON.parse(product.baseImage);
                 return (
                     <div key={product.id} 
-                        className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0"
+                        className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 first:mt-0"
                     >
                         <div className="relative flex-initial w-22 h-30 mr-2">
                             <Image className="object-cover" fill
@@ -29,10 +29,10 @@ export default function ProductsInPendingOrder({
                         </div>
                         <div className="flex-1 flex flex-col justify-between ">
                             <div>
-                                <p className="text-ly-14 leading-ly-20 line-clamp-2 text-ly-placeholder">
+                                <p className="text-ly-12 leading-ly-20 line-clamp-2 text-ly-placeholder">
                                     {product.name}
                                 </p>
-                                <p className="text-ly-14 leading-ly-20 mt-3">
+                                <p className="text-ly-12 leading-ly-20 mt-2">
                                     {getProductAdditionalInfo(JSON.parse(product.additional))}
                                 </p>
                             </div>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 30 - 25
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx


+ 17 - 15
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-4">
+        <div className="relative box-border w-full bg-[#ECF7F4] px-4 py-3">
             <div className="flex justify-between items-center">
                 <span className="text-ly-14">
                     {title}
@@ -62,20 +62,22 @@ export default function LostInsurance({
                     {cartData.formattedShippingInsuranceAmount}
                 </span>
             </div>
-            <div className="mt-2.5 text-ly-14 leading-4.5 text-ly-placeholder">
-                Increased compensation for courier packages issued within 30 days.
-            </div>
-            <div className="absolute bottom-4 right-4 flex justify-between items-center">
-                <label className="group relative w-9 h-5.5 flex-none">
-                    <input className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
-                        type="checkbox" checked={cartData.useShippingInsurance}
-                        onChange={(e) => clickSwitchButton(e.target.checked)}
-                    />
-                    <span className="transition-all box-border block px-0.5 w-full h-full rounded-full flex items-center bg-[#c2c2c2] group-has-[:checked]:bg-[#363636]">
-                        <span className="transition-all block w-4.5 h-4.5 rounded-full bg-white group-has-[:checked]:ml-3.5"></span>
-                    </span>
-                </label>
-                
+            <div className="flex items-center justify-between mt-3">
+                <div className="text-ly-12 text-ly-placeholder">
+                    Increased compensation for courier packages issued within 30 days.
+                </div>
+                <div className="flex justify-between items-center">
+                    <label className="group relative w-9 h-5.5 flex-none">
+                        <input className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
+                            type="checkbox" checked={cartData.useShippingInsurance}
+                            onChange={(e) => clickSwitchButton(e.target.checked)}
+                        />
+                        <span className="transition-all box-border block px-0.5 w-full h-full rounded-full flex items-center bg-[#c2c2c2] group-has-[:checked]:bg-[#363636]">
+                            <span className="transition-all block w-4.5 h-4.5 rounded-full bg-white group-has-[:checked]:ml-3.5"></span>
+                        </span>
+                    </label>
+                    
+                </div>
             </div>
 
             

+ 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"></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"></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"></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>
     )
 }

+ 31 - 7
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,7 +1,10 @@
 "use client";
 
 import { type Ref, useEffect, useImperativeHandle } from "react";
+import Link from "next/link";
+import clsx from "clsx";
 import { useForm, get } from "react-hook-form";
+import { usePayPalMessages } from "@paypal/react-paypal-js/sdk-v6";
 import {useStore} from "@/utils/hooks/useConfig";
 import {currencyCountryMap} from "@/utils/constants";
 import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
@@ -31,6 +34,7 @@ export function PaymentMethodCheckout({
     grandTotal: number;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
+    const { error:paypalMessageError } = usePayPalMessages({});
 
     const {store} = useStore();
     const currencySymbol = currencyCountryMap[store.currentCurrency].symbol;
@@ -114,18 +118,26 @@ 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 first:mt-0", 
+                                    selectedPaymentMethod === 'airwallex' && item.method === 'airwallex' && ['bg-[#ECF7F4] border-1 border-ly-green rounded-xs pb-3.5']
+                                )}
+                            >
                                 <label htmlFor={"paymentmethod-" + item.method}
-                                    className="group box-border flex items-center relative bg-ly-lightgray px-4 py-3.5 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
+                                    className={clsx(
+                                        "group box-border flex items-center relative px-4 py-3.5 bg-ly-lightgray",
+                                        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">
+                                    <p className="mr-2 flex-none relative">
                                         <img
-                                            className="block h-8 w-auto"
+                                            className="block h-6 w-auto"
                                             src={getPaymentMethodLogo(item.method)}
                                             alt={`Payment Method ${item.title}`}
                                         />
                                     </p>
-                                    <p className="text-ly-14 text-[#666666] ml-1">
+                                    <p className="text-ly-14 text-[#666666]">
                                         {genetePaymentTips(item.method)}
                                     </p>
 
@@ -146,7 +158,7 @@ export function PaymentMethodCheckout({
                                     />
                                 </label>
                                 { item.method === "airwallex" &&
-                                    <div className="w-full mt-3"
+                                    <div className="w-full px-4"
                                         style={{display: airwallexOtherInfoShow ? 'block' : 'none'}}
                                     >
                                         <AirwallexCardInput />
@@ -160,7 +172,19 @@ export function PaymentMethodCheckout({
                 
                 {paymentMethodError && <p className="text-red-500 text-ly-12">{paymentMethodError.message}</p>}  
             </div>
-            <p className="text-ly-12 mt-3 text-[#666666] leading-ly-20">By providing your information, you agree to Asteria's Privacy Policy and Terms of Use.</p>
+            <p className="text-ly-14 mt-3 leading-ly-20">
+                By placing this order, I agree to the <Link href="/return-policy">Return Policy</Link> and the <Link href="/terms-conditions">Terms & Conditions</Link>.
+            </p>
+            {!paypalMessageError && 
+                <div className="mt-4">
+                    <paypal-message className="checkout-paypal-message"
+                        auto-bootstrap={true}
+                        amount={String(grandTotal)}
+                        currency-code={store.currentCurrency}
+                        // buyer-country="US" //这个属性是沙盒环境测试用的,正式环境不要设置这个参数
+                    />
+                </div>
+            }
                 
         </form>
     </>);

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

@@ -15,7 +15,7 @@ export default function PromotionsDetails({cartData}:{
     return (
         <div className="w-full box-border">
             <ul className="w-full">
-                <li className="flex justify-between items-center text-ly-14 mt-4 first:mt-0">
+                <li className="flex justify-between items-center text-ly-14">
                     <span>Subtotal</span>
                     <span>{cartData.formattedSubtotal}</span>
                 </li>
@@ -45,7 +45,7 @@ export default function PromotionsDetails({cartData}:{
                 }
                 {cartData.rewardPointsAmount !== 0 &&
                     <li className="flex justify-between items-center text-ly-14 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>
                 }
@@ -67,7 +67,7 @@ export default function PromotionsDetails({cartData}:{
                         <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
                     </li>
                 }
-                <li className="flex justify-between items-center text-ly-14 mt-4 first:mt-0 pt-6 border-t-1 border-[#f0f0f0]">
+                <li className="flex justify-between items-center text-ly-14 mt-4 first:mt-0 pt-4 border-t-1 border-[#f0f0f0]">
                     <span>Grand Total</span>
                     <span className="text-ly-24 font-bold">{cartData.formattedGrandTotal}</span>
                 </li>

+ 37 - 33
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,12 +33,13 @@ export default function RewardPointsRedeemMoney({
 }) {
     const cartUsedPointsStr = String(cartUsedPoints);
     const router = useRouter();
+    const {store} = useStore();
     const {openLoginModal} = useLoginModal();
     const {saveCheckoutCart} = useSaveCheckoutCart();
     const { showToast } = useCustomToast();
 
     const [usedPoints, setUsedPoints] = useState(cartUsedPointsStr);
-    const amount = Number(formatRewardPointsAmount.replace(/^\D+/ig,''));
+    const amount = Number(formatRewardPointsAmount.replace(/\,/g,'').replace(/^\D+/ig,''));
 
 
     const pointsInputChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -102,45 +109,42 @@ export default function RewardPointsRedeemMoney({
                 </div>
             :
             <div className="w-full">
-                <div className="relative box-border w-full flex justify-between items-center">
-                    <span className="text-ly-20 font-medium">Reward points redeem</span>
+                <div className="relative box-border w-full text-ly-18 font-medium">
+                    Redeem Points
+                </div>
                 
-                    
-                    <div className="flex items-center">
+                <div className="box-border w-full mt-4 p-4 bg-linear-to-b from-[#D9FFF2] to-[#ffffff]">
+                    <p className="text-ly-14">Available Points:</p>  
+                    <p className="mt-1">
+                        <span className="text-ly-48 leading-12 font-bold">{hasRewardPoints}</span>
+                        <span className="tetx-ly-18 ml-2">≈${calculateRewardPointsMoney(hasRewardPoints,store.rewardPointsRate)}</span>
+                    </p>
+                </div>
+                <div className="flex justify-between items-center mt-3 box-border w-full border-1 h-11 rounded-sm pl-3 pr-1 bg-white">
+                    <input className="focus:outline-none text-ly-14 flex-auto"
+                        type="text"
+                        value={usedPoints === '0' ? '' : usedPoints}
+                        placeholder="Reward Points"
+                        onChange={pointsInputChange}
+                    />
+                    <div className="flex-none flex items-center justify-end">
+                        
                         {amount > 0 &&
-                        <span className="flex items-center text-ly-14 px-2 flex-none h-6 bg-ly-gold mr-2">
+                        <span className="flex items-center text-ly-14 px-0.5 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> */}
+          
+                        <button className="flex items-center justify-center text-ly-12 rounded-sm bg-ly-middlegreen text-white w-19 h-9"
+                            onClick={submitPointsUse}
+                        >
+                            {cartUsedPoints > 0 ? 'Cancel' : 'Apply'}
+                        </button>
                     </div>
                 </div>
-                
-                <div className="w-full mt-4">
-                    <div className="box-border w-full bg-ly-middlegray p-4 rounded-md">
-                        
-                        <p className="text-ly-14">
-                            You have <span className="font-bold">{hasRewardPoints} Reward Points</span>  available.  100 point = $1. Input points value below to redeem.
-                        </p>
-                        <div className="flex justify-between items-center mt-3 box-border w-full border-1 h-12 rounded-sm pl-4 pr-1 bg-white">
-                            <input className="focus:outline-none text-ly-14"
-                                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-4 h-9"
-                                onClick={submitPointsUse}
-                            >
-                                {cartUsedPoints > 0 ? 'Cancel' : 'Apply'}
-                            </button>
-                        </div>
-                    </div>
-                    
-                </div>
+                 <p className="text-ly-12 mt-2 text-ly-placeholder">
+                    Exchange rate: 100 Points = ${100 * store.rewardPointsRate}
+                </p>
             </div>
             }
 

Dosya farkı çok büyük olduğundan ihmal edildi
+ 3 - 3
src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx


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

@@ -5,19 +5,13 @@ import { useRouter } from 'next/navigation';
 import type {CartDetail} from "@/types/cart/type";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
+import { getLeftDays } from "@/utils/helper";
+import { staticVipData } from "@/utils/constants";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 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 {useLoginModal} from "@/providers/LoginModalProvider";
-
-// 问题: plus金额,折扣,时间是前端写死的。写死金额只显示美金数字,不随货币改变;
-const staticVipData = {
-    price: '$19.90',
-    linePrice: '$29.90',
-    days: '180',
-    discount: '5%'
-};
+import { VipPlusRulesModal } from "@/components/common/VipPlusRulesModal/VipPlusRulesModal";
 
 export default function CheckoutVipPlus({
     cartData,
@@ -116,25 +110,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 && 
@@ -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">
@@ -192,32 +167,7 @@ export default function CheckoutVipPlus({
             </div>
         }
 
-        <CommonModal 
-            destroyOnClose={true}
-            isOpen={isShow}
-            onClose={toggleModal}
-            contentClassName={"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-144.5 rounded-xl bg-white px-6"}
-            body={
-                <div className="relative py-4">
-                    <div className="text-center text-ly-16 font-bold">Asteriahair Plus</div>  
-                    <p className="text-center text-ly-14 leading-5 mt-5">
-                        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-14 leading-5 mt-3">
-                        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-14 leading-5 mt-3">
-                        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-14 leading-5 mt-3">
-                        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)} />
 
     </>);
 }

+ 26 - 24
src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState, useCallback } from "react";
+import {useState, useCallback, Suspense } from "react";
 import clsx from "clsx";
 import Image from "next/image";
 import { useRouter } from 'next/navigation';
@@ -20,7 +20,8 @@ import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import CheckoutGiftCardGate from "../../_components/GiftCard/CheckoutGiftCardGate";
 import CheckoutVipPlus from "../../_components/VipPlus/CheckoutVipPlus";
 import CheckoutCoupon from "../../_components/CheckoutCoupon/CheckoutCoupon";
-import { UnpaidOrderSection } from "./UnpaidOrderSection";
+import { UnpaidOrderSection } from "@/components/cart/UnpaidOrder/UnpaidOrderSection";
+import { UnpaidOrderLoading } from "@/components/cart/UnpaidOrder/UnpaidOrderLoading";
 import { Price } from "@components/theme/ui/Price";
 
 function computeGrandTotal(cartData: CartDetail) {
@@ -37,7 +38,7 @@ function computeGrandTotal(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);
 // }
 
 /**
@@ -45,12 +46,10 @@ function computeGrandTotal(cartData: CartDetail) {
  */
 export function CartWrapper({
     cartDetailData,
-    hasUnPaidOrder,
-    unpaidOrder
+    unpaidOrderPromise
 }: {
     cartDetailData: CartDetail;
-    hasUnPaidOrder: boolean;
-    unpaidOrder: Omit<OrderDetails, 'addresses'> | null;
+    unpaidOrderPromise: Promise<{success: boolean;orderDetail: Omit<OrderDetails, "addresses"> | null;expiried: boolean | null;error?:any;}> | null;
 }) {
     const router = useRouter();
     const {openLoginModal} = useLoginModal();
@@ -214,27 +213,29 @@ export function CartWrapper({
                 </div>
             </div>
         }
-        <div className="flex justify-between items-start max-w-312 w-full mt-6 px-6 mx-auto">
+        <div className="flex justify-between items-start max-w-312 w-full mt-6 mx-auto">
             <section className="flex-initial box-border w-190.5">
-                
-                {(hasUnPaidOrder && unpaidOrder !== null) &&
-                   <div className="w-full mb-6">
-                        <UnpaidOrderSection 
-                            orderDetail={unpaidOrder}
-                        />
-                    </div>
-                }
-                
                 <div className="box-border w-full pb-4 border-b-1 font-medium text-ly-20">
                     My Bags({cartData.itemsQty})
                 </div>
+                
+                
+                
 
-                <div className="mt-4 box-border px-4">
+                <div className="mt-6 box-border px-4">
+                    {(unpaidOrderPromise !== null) &&
+                
+                        <Suspense fallback={<UnpaidOrderLoading />}>
+                            <UnpaidOrderSection 
+                                orderDetailPromise={unpaidOrderPromise}
+                            />
+                        </Suspense>
+                    }
                     {normalProducts.map((product) => {
                         const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
                         const productOptions = product.options.edges.map((edge) => edge.node);
                         return (
-                            <div key={product.id} className="w-full flex gap-4 mt-6 first:mt-0 pb-6 border-b-1 border-b-[#f0f0f0] last:border-none">
+                            <div key={product.id} className="w-full flex gap-4 mt-6 first:mt-4 pt-6 first:pt-0 border-t-1 border-[#f0f0f0] first:border-none">
                                 <div className="relative w-33 h-44 flex-none">
                                     <Image className="object-cover" fill
                                         src={baseImage.medium_image_url || ''} 
@@ -326,8 +327,8 @@ export function CartWrapper({
                 </div> */}
 
             </section>
-            <section className="flex-none box-border w-97.5 ml-4 border-1">
-                <div className="box-border w-full border-b-1 p-4">
+            <section className="flex-none box-border ml-4 w-97.5 border-1 border-[#ACBDB8] rounded-sm">
+                <div className="box-border w-full border-b-1 p-4 border-[#ACBDB8]">
                     <div className="flex items-center justify-between" onClick={toggleAmountDetail}>
                         <div className="flex items-center text-ly-20">
                             {cartData.isVip &&
@@ -399,8 +400,9 @@ export function CartWrapper({
                     />
                 </div>
 
-                <div className="box-border p-4 border-t-1">
+                <div className="box-border p-4 border-t-1 border-[#ACBDB8]">
                     <CheckoutCoupon 
+                        title="Coupon Code"
                         couponCode={cartData.couponCode || ''}
                         discountAmount={cartData.formattedDiscountAmount}
                         onCouponChange={onCartDataChange}
@@ -408,7 +410,7 @@ export function CartWrapper({
                 </div>
 
                 {!cartData.isGuest &&
-                    <div className="box-border p-4 border-t-1">
+                    <div className="box-border p-4 border-t-1 border-[#ACBDB8]">
                         <CheckoutGiftCardGate 
                             giftcardNumber={cartData.giftcardNumber || ''}
                             formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
@@ -418,7 +420,7 @@ export function CartWrapper({
                 }
 
 
-                <div className="w-full box-border border-t-1 p-4">
+                <div className="w-full box-border border-t-1 p-4 border-[#ACBDB8]">
                     
                     <div className="flex justify-between">
                         {/* <button className="flex justify-center items-center w-41 h-12 bg-[#ffc43a]  rounded-3xl" type="button">

+ 11 - 10
src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx

@@ -1,31 +1,32 @@
 "use client";
 
-import { useState } from "react";
+import { useState, Suspense } from "react";
 import Image from "next/image";
 import clsx from "clsx";
 import { useRouter } from 'next/navigation';
 import type { OrderDetails } from "@/types/customer/type";
-import { UnpaidOrderSection } from "./UnpaidOrderSection";
+import { UnpaidOrderSection } from "@/components/cart/UnpaidOrder/UnpaidOrderSection";
+import { UnpaidOrderLoading } from "@/components/cart/UnpaidOrder/UnpaidOrderLoading";
 
 export function EmptyCart({
     isGuest,
-    hasUnPaidOrder,
-    unpaidOrder
+    unpaidOrderPromise
 }:{
     isGuest: boolean;
-    hasUnPaidOrder: boolean;
-    unpaidOrder: Omit<OrderDetails, 'addresses'> | null;
+    unpaidOrderPromise: Promise<{success: boolean;orderDetail: Omit<OrderDetails, "addresses"> | null;expiried: boolean | null;error?:any;}> | null;
 }) { 
 
     const router = useRouter();
     const [showTips, setShowTips] = useState(true);
     return (
         <div className="w-full box-border mt-12">
-            {(hasUnPaidOrder && unpaidOrder !== null) &&
+            {(unpaidOrderPromise !== null) &&
                 <div className="w-130 mx-auto">
-                    <UnpaidOrderSection 
-                        orderDetail={unpaidOrder}
-                    />
+                    <Suspense fallback={<UnpaidOrderLoading />}>
+                        <UnpaidOrderSection 
+                            orderDetailPromise={unpaidOrderPromise}
+                        />
+                    </Suspense>
                 </div>
             }
             <div className="w-50 h-50 mx-auto mt-6">

+ 31 - 23
src/app/(checkout)/checkout/cart/page.tsx

@@ -24,27 +24,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;expiried: boolean | null;error?:any;}> | 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;
             }
-            
-        } catch(err) {
-            console.error('get unpaid order:', err)
-        }
+            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 {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
         query: GET_CART_ITEM
     });
@@ -55,24 +66,21 @@ export default async function CheckoutPage() {
         cartDatahash = createHash('sha256').update(JSON.stringify(cartDetails)).digest('hex');
     }
 
-    const hasUnPaidOrder = (unpaidOrder !== null && !unpaidOrderExpiried);
 
     return (<>
         <PromiseInCart />
-        <div className="mt-6 mb-30 max-w-312 mx-auto px-6">
+        <div className="mb-30 max-w-312 mx-auto px-6">
             
             {!cartDetails && 
                 <EmptyCart 
                     isGuest={isGuest}
-                    hasUnPaidOrder={hasUnPaidOrder}
-                    unpaidOrder={unpaidOrder}
+                    unpaidOrderPromise={unpaidOrderPromise}
                 />
             }
             {cartDetails &&
                 <CartWrapper key={cartDatahash}
                     cartDetailData={cartDetails}
-                    hasUnPaidOrder={hasUnPaidOrder}
-                    unpaidOrder={unpaidOrder}
+                    unpaidOrderPromise={unpaidOrderPromise}
                 />
             }
         </div>

+ 6 - 26
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,15 +1,13 @@
-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 { ContinueToPayWrapper } from "../_components/ContinueToPay/ContinueToPayWrapper";
+
 
 
 // 只能存在一个待支付订单
@@ -81,28 +79,10 @@ export default async function ContinueToPay({searchParams}: {
                 </Link>
             </div>
         :
-            <div className="flex justify-between items-start box-border max-w-312 mx-auto mb-30 mt-6 px-6">
-                <div className="flex-initial w-164 mr-4">
-                    <ContinueToPayOrderInfo orderDetail={orderDetailData} />
-                </div>
-            
-                
-            
-                <div className="flex-initial w-120 box-border">
-                    <h3 className="text-ly-20 font-medium">Payment Method</h3>
-                    <Suspense fallback={<LoadingPaymentMethod/>}> 
-                        <PaymentMethodContinueTpPay 
-                            orderId={orderDetailData._id}
-                            orderDetail={orderDetailData}
-                            paymentMethodsDataPromise={paymentMethodsResponse}
-                            defaultPaymentMethod={orderDetailData.payment}
-                        />
-                    </Suspense>
-
-                </div>
-                
-                
-            </div>
+            <ContinueToPayWrapper 
+                orderDetailData={orderDetailData}
+                paymentMethodsPromise={paymentMethodsResponse}
+            />
         }
     </>);
   

+ 34 - 121
src/app/(public)/customer/(has-dashboard)/account/_components/AccountHeader.tsx

@@ -2,13 +2,10 @@
 
 import Image from "next/image";
 import Link from "next/link";
+import type {CustomerProfileUser} from "@/types/api/customer/profile/type";
 
 interface AccountHeaderProps {
-  user: {
-    name: string;
-    email: string;
-    image_url?: string | null;
-  };
+  user: CustomerProfileUser;
 
   points: number;
   coupon_count: number;
@@ -22,7 +19,7 @@ export default function AccountHeader({
   // wishlist_count,
 }: AccountHeaderProps) {
   return (
-    <div className="relative h-[154px] ">
+    <div className="relative">
       {/* green header */}
       <div
        className="text-center relative pb-8 pt-6 pl-6 pr-8 h-[154px]  bg-[url('/image/account/accountbg.png')] bg-no-repeat bg-size-[100%_100%]"
@@ -30,53 +27,23 @@ export default function AccountHeader({
         <div className="flex items-center justify-between">
           {/* user */}
           <div className="flex items-center gap-3">
-            <div
-              className="
-              flex
-              h-[70px]
-              w-[70px]
-              items-center
-              justify-center
-              rounded-full
-              bg-white
-              overflow-hidden
-              "
-            >
+            <div className="flex h-[70px] w-[70px] items-center justify-center rounded-full bg-white overflow-hidden">
               <Image className="block" src={ user.image_url || "/image/account/202608271601.png" } alt="userphoto" width={48} height={48} />
             </div>
 
             <div className="text-white">
-              <h2
-                className="
-                text-[24px]
-                font-medium
-                "
-              >
+              <h2 className="text-[24px] font-medium">
                 {user.name}
               </h2>
 
-              <p
-                className="
-                mt-1
-                text-sm
-                "
-              >
+              <p className="mt-1 text-sm">
                 {user.email}
               </p>
             </div>
           </div>
 
           {/* VIP */}
-          <button
-            className="
-            rounded-full
-            bg-white
-            px-4
-            py-2
-            text-black
-            flex
-            "
-          >
+          <button className="rounded-full bg-white px-4 py-2 flex">
             VIP Perks Access
             <span className="ml-3"><svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
           </button>
@@ -84,95 +51,41 @@ export default function AccountHeader({
       </div>
 
       {/* statistics card */}
-      <div
-        className="
-        absolute
-        left-1/2
-        top-[120px]
-        flex
-        justify-center
-        gap-6
-        items-center
-        w-[90%]
-        -translate-x-1/2
-        rounded-[20px]
-        bg-white
-        py-6
-        shadow-[0_4px_20px_rgba(0,0,0,.08)]
-        "
-      >
-        <Link
-          href="/customer/account/mypoints"
-          className="
-         
-          text-center
-          w-[210px]
-          h-[76px]
-           bg-[#F0FFFA]
-          rounded-2xl
-          pt-3
-          "
-        >
-          <strong
-            className="
-            block
-            text-2xl
-            "
+      <div className="relative w-full flex justify-center -mt-9">
+        <div className="flex justify-center gap-6 items-center w-[90%] rounded-[20px] bg-white py-6 shadow-[0_4px_20px_rgba(0,0,0,.08)]">
+          <Link
+            href="/customer/account/mypoints"
+            className="text-center w-[210px] h-[76px] bg-[#F0FFFA] rounded-2xl pt-3"
           >
-            {points}
-          </strong>
+            <strong className="block text-2xl">
+              {points}
+            </strong>
 
-          <span className="flex justify-center leading-6 items-center">Points <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
-        </Link>
+            <span className="flex justify-center leading-6 items-center">Points <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
+          </Link>
 
-        <Link
-          href="/customer/account/mycoupon"
-          className="
-          
-          text-center
-          w-[210px]
-          h-[76px]
-          bg-[#F0FFFA]
-           rounded-2xl
-           pt-3
-          "
-        >
-          <strong
-            className="
-            block
-            text-2xl
-            
-            "
+          <Link
+            href="/customer/account/mycoupon"
+            className="text-center w-[210px] h-[76px] bg-[#F0FFFA] rounded-2xl pt-3"
           >
-            {coupon_count}
-          </strong>
+            <strong className="block text-2xl">
+              {coupon_count}
+            </strong>
 
-          <span className="flex justify-center leading-6 items-center">Coupon <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
-        </Link>
+            <span className="flex justify-center leading-6 items-center">Coupon <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
+          </Link>
 
-        <Link
-          href="/customer/account/wishlist"
-          className="
-      
-          text-center
-          w-[210px]
-          h-[76px]
-          bg-[#F0FFFA]
-           rounded-2xl
-           pt-3
-          "
-        >
-          <strong
-            className="
-            block
-            text-2xl
-            "
+          <Link
+            href="/customer/account/wishlist"
+            className="text-center w-[210px] h-[76px] bg-[#F0FFFA] rounded-2xl pt-3"
           >
-           {0}
-          </strong>
+            <strong className="block text-2xl">
+            {0}
+            </strong>
 
-          <span className="flex justify-center leading-6 items-center">Wishlist <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
-        </Link>
+            <span className="flex justify-center leading-6 items-center">Wishlist <svg xmlns="http://www.w3.org/2000/svg"  width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="square"  d="M9 18L15 12L9 6"></path></svg></span>
+          </Link>
+        </div>
       </div>
     </div>
   );

Dosya farkı çok büyük olduğundan ihmal edildi
+ 42 - 0
src/app/(public)/customer/(has-dashboard)/account/_components/VipPlusCard.tsx


+ 73 - 14
src/app/(public)/customer/(has-dashboard)/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 { UnpaidOrderLoading } from "@/components/cart/UnpaidOrder/UnpaidOrderLoading";
+import { VipPlusCard } from "./_components/VipPlusCard";
 // import Link from "next/link";
 const orderNavs = [
   {
@@ -33,24 +41,75 @@ const orderNavs = [
 ];
 
 const MyAccountPage =async () => {
- const res = await restApiFetch({
-  api: `/customer/profile`,
-  method: "GET",
-  isRoute: false,
-});
-console.log('accountres-----------------------------:',res);
+  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,
+  });
+
 
   return (
     <div className="min-h-screen bg-white  ">
       <h1 className="font-normal text-2xl leading-ly-28 mb-6">My Account</h1>
-      <AccountHeader
-        user={res.body.data.user}
-        points={res.body.data.points}
-        coupon_count={res.body.data.coupon_count}
-      />
+      <div className="w-full mb-9">
+        <AccountHeader
+            user={res.body.data.user}
+            points={res.body.data.points}
+            coupon_count={res.body.data.coupon_count}
+        />
+      </div>
+      {unpaidOrderPromise &&
+          <Suspense fallback={<UnpaidOrderLoading />}>
+            
+            <UnpaidOrderSection 
+                orderDetailPromise={unpaidOrderPromise}
+            />
+            
+          </Suspense>
+
+      }
+      {res.body.data.plus.is_plus_member && 
+        <div className="box-border w-full mt-9">
+            <VipPlusCard customerPlusInfo={res.body.data.plus} />
+        </div>
+      }
       {/* 我的订单 */}
-      <div className="mt-[136px]">
-        <h3 className="mt-8 font-medium leading-ly-26 text-ly-18">My Order ({res.body.data.order_count})</h3>
+      <div className="w-full mt-9">
+        <h3 className="font-medium leading-ly-26 text-ly-18">My Order ({res.body.data.order_count})</h3>
         <div className="flex flex-wrap justify-between mt-5 ">
           {orderNavs.map((item, index) => (
             <a

+ 12 - 1
src/app/globals.css

@@ -69,7 +69,7 @@
 
   --color-ly-inputborder: #e5e5e5;
   --color-ly-placeholder: #a6a6a6;
-  --color-ly-errorcolor: #ff0000;
+  --color-ly-errorcolor: #FF4D4F;
 
   --color-ly-lightgray: #f9f9f9;
   --color-ly-middlegray: #f2f2f2;
@@ -497,4 +497,15 @@
       left: auto;
       bottom:auto;
   }
+
+  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;
+  }
 }

+ 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.
@@ -120,7 +121,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;
+}

+ 40 - 0
src/components/cart/UnpaidOrder/UnpaidOrderLoading.tsx

@@ -0,0 +1,40 @@
+
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+
+export function UnpaidOrderLoading() {
+
+    return (<>
+        <div className="relative w-full box-border p-4">
+            <div className="flex items-center h-6 w-60 bg-skeletonbg"></div>
+            <div className="relative w-full mt-6 flex gap-4">
+
+                <div className="min-w-0 w-fit max-w-[calc(100%-2.5rem-1rem)] flex-none overflow-x-auto scrollbar-thin scrollbar-thumb-gray-200">
+                    <div className="flex w-max gap-4">
+                        {[1,2].map((i) => {
+
+                            return(
+                            <div className="relative w-33.75 h-45 flex-none bg-skeletonbg" key={i}>
+                                <Image className="object-cover" fill
+                                    src={NOT_IMAGE}
+                                    alt="placeholder"
+                                />
+                            </div>
+                            );
+                        })}
+                    </div>  
+                </div>
+                <button className="flex-none flex justify-center items-center w-10 h-45 bg-skeletonbg rounded-sm" type="button"></button>
+            </div>
+
+            <div className=" flex items-center justify-end w-full mt-4">
+                <span className="h-5 w-40 mr-4 bg-skeletonbg"></span>
+                <button className="flex-none flex justify-center items-center w-41 h-12 bg-skeletonbg rounded-3xl">
+                </button>
+            </div>
+
+            <button className="absolute top-4 right-4 w-6 h-6 bg-skeletonbg"></button>
+        </div>
+
+    </>);
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 35 - 25
src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx


+ 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-144.5 rounded-xl bg-white px-6"}
+            body={
+                <div className="relative py-4">
+                    <div className="text-center text-ly-16 font-bold">Asteriahair Plus</div>  
+                    <p className="text-center text-ly-14 leading-5 mt-5">
+                        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-14 leading-5 mt-3">
+                        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-14 leading-5 mt-3">
+                        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-14 leading-5 mt-3">
+                        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>
+            }
+        />
+
+    </>);
+}

+ 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();

+ 44 - 17
src/components/theme/ui/kernel/confirm/host.tsx

@@ -1,23 +1,24 @@
 "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 {queue} = useConfirmQueue();
 
-  const current = queue[0];
+    const current = queue[0];
 
-//   if (!current) return null;
+    //   if (!current) return null;
 
-  const close = (result: boolean) => {
-    current.resolve(result);
+    const close = (result: boolean) => {
+        current.resolve(result);
 
-    confirmStore.setState({
-      queue: queue.slice(1),
-    });
-  };
+        confirmStore.setState({
+            queue: queue.slice(1),
+        });
+    };
 
   return (
     <AnimatePresence>
@@ -29,16 +30,42 @@ export function ConfirmHost() {
         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-80 p-3 bg-white rounded-xl">
-                    <h3 className="text-center text-ly-18 font-bold mb-6">{current.title}</h3>
+            <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 gap-3 mt-6">
-                        {!current.noOk && <button className="w-35.5 h-12 flex justify-center items-center rounded-3xl border-1 text-ly-16 font-medium" onClick={() => close(true)}>Yes</button>}
-                        {!current.noCancel && <button className="w-35.5 h-12 flex justify-center items-center rounded-3xl bg-ly-green text-white text-ly-16 font-medium" onClick={() => close(false)}>Cancel</button>}
-                    </div>
+
+                }
+
+                <div className="w-full flex justify-center flex-wrap gap-4 mt-4">
+                    {!current.noOk && <button type="button" 
+                        className={clsx("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", !current.noCancel && "grow ")} 
+                        onClick={() => close(true)}
+                    >
+                        {current.okBtnTxt ? current.okBtnTxt : 'Yes'}
+                    </button>}
+                    {!current.noCancel && <button type="button" 
+                        className={clsx("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", !current.noOk && "grow")} 
+                        onClick={() => close(false)}
+                    >
+                        {current.cancelBtnTxt ? current.cancelBtnTxt : 'Cancel'}
+                    </button>}
                 </div>
-            {/* </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>
+
         </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;
 };
 

+ 222 - 203
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
 // ============================================================
@@ -60,26 +61,19 @@ let initPromise: Promise<InitResult> | null = null;
 
 
 export function airwallexInit() {
+    if (!initPromise) {
 
+        initPromise = init({
+            env: env.NEXT_PUBLIC_APP_ENV !== 'production' ? 'demo' : 'prod',
 
-  if (!initPromise) {
+            enabledElements: ['payments'],
+        }).catch((err) => {
+            initPromise = null;
+            throw err;
+        });
 
-    initPromise = init({
-      env:
-        env.NEXT_PUBLIC_APP_ENV !== 'production'
-          ? 'demo'
-          : 'prod',
-
-      enabledElements: ['payments'],
-    }).catch((err) => {
-      initPromise = null;
-      throw err;
-    });
-
-  }
-
-
-  return initPromise;
+    }
+    return initPromise;
 }
 
 // ============================================================
@@ -92,199 +86,224 @@ class AirwallexManager {
     // --------------------------------------------------------
     // Airwallex Elements
     // --------------------------------------------------------
-  private elements: AirwallexCardElements = {};
-
-  private createPromise: Promise<AirwallexCardElements> | null = null;
-
-  // 引入使用者计数(现在是全局单例模式,这里的全局指的是以页面的生命周期为准,对于卡支付输入框,全局只有一个地方可以初始化,每次初始化后,全局只有一个实例)
-  // 所以这里引入的refCount是一个过度防御
-  // 当把airwallex的卡支付初始化与卡支付输入框的状态订阅拆分后,这个refCount已不那么需要(可以删除),但还是保留
-  private refCount = 0;
-
-  // --------------------------------------------------------
-  // React external store
-  // --------------------------------------------------------
-  private snapshot: AirwallexCardSnapshot = {
-      ...initialSnapshot,
-  };
-
-  private listeners = new Set<() => void>(); // 集合类型,用来保存监听器
-
-  // 参数listener是react内部的监听函数
-  // 当把subscribe传给useSyncExternalStore后,react在内部调用subscribe时把自己的监听函数传给subscribe
-  subscribe = (listener: () => void) => {
-      // 把react的监听函数保存到集合
-      this.listeners.add(listener);
-      
-      // 取消订阅时(比如组件卸载时),删除保存的监听函数
-      return () => {
-          this.listeners.delete(listener);
-      };
-  };
+    private elements: AirwallexCardElements = {};
+
+    private createPromise: Promise<AirwallexCardElements> | null = null;
+
+    // 引入使用者计数(现在是全局单例模式,这里的全局指的是以页面的生命周期为准,对于卡支付输入框,全局只有一个地方可以初始化,每次初始化后,全局只有一个实例)
+    // 所以这里引入的refCount是一个过度防御
+    // 当把airwallex的卡支付初始化与卡支付输入框的状态订阅拆分后,这个refCount已不那么需要(可以删除),但还是保留
+    private refCount = 0;
+
+    // --------------------------------------------------------
+    // React external store
+    // --------------------------------------------------------
+    private snapshot: AirwallexCardSnapshot = {
+        ...initialSnapshot,
+    };
+
+    private listeners = new Set<() => void>(); // 集合类型,用来保存监听器
+
+    // 参数listener是react内部的监听函数
+    // 当把subscribe传给useSyncExternalStore后,react在内部调用subscribe时把自己的监听函数传给subscribe
+    subscribe = (listener: () => void) => {
+        // 把react的监听函数保存到集合
+        this.listeners.add(listener);
+        
+        // 取消订阅时(比如组件卸载时),删除保存的监听函数
+        return () => {
+            this.listeners.delete(listener);
+        };
+    };
 
   // react传给subscribe的监听函数里会调用getSnapshot重新获取数据快照
-  getSnapshot = () => {
-      return this.snapshot;
-  };
-  private emit() {
-
-      this.listeners.forEach((listener) => {
-          listener();
-      });
-  }
-  private updateSnapshot( patch: Partial<AirwallexCardSnapshot> ) {
-      this.snapshot = {
-          ...this.snapshot,
-          ...patch,
-      };
-      // 修改数据后,触发react的监听器,监听器里重现获取数据快照,数据变了,react重新渲染组件
-      this.emit();
-  }
-
-  private resetSnapshot() {
-      this.snapshot = {
-          ...initialSnapshot,
-      };
-
-      this.emit();
-  }
+    getSnapshot = () => {
+        return this.snapshot;
+    };
+    private emit() {
+
+        this.listeners.forEach((listener) => {
+            listener();
+        });
+    }
+    private updateSnapshot( patch: Partial<AirwallexCardSnapshot> ) {
+        this.snapshot = {
+            ...this.snapshot,
+            ...patch,
+        };
+        // 修改数据后,触发react的监听器,监听器里重现获取数据快照,数据变了,react重新渲染组件
+        this.emit();
+    }
+
+    private resetSnapshot() {
+        this.snapshot = {
+            ...initialSnapshot,
+        };
+
+        this.emit();
+    }
   // 
-  setRefCount() {
-    this.refCount++;
-    console.log(
-        '[Airwallex] acquire:',
-        this.refCount
-    );
-  }
-  release() {
-    if (this.refCount === 0) {
-        return;
+    setRefCount() {
+        this.refCount++;
+        console.log(
+            '[Airwallex] acquire:',
+            this.refCount
+        );
     }
+    release() {
+        if (this.refCount === 0) {
+            return;
+        }
 
-    this.refCount--;
+        this.refCount--;
 
-    console.log(
-        '[Airwallex] release:',
-        this.refCount
-    );
+        console.log(
+            '[Airwallex] release:',
+            this.refCount
+        );
+
+        if (this.refCount === 0) {
+            console.log('_____________________________AirwallexCard destory ___________________________________')
+            this.destroy();
+        }
 
-    if (this.refCount === 0) {
-      console.log('_____________________________AirwallexCard destory ___________________________________')
-        this.destroy();
     }
 
-  }
 
+    async createElements() {
+
+        if (this.elements.cardNumber) {
+            return this.elements;
+        }
+
+        if (this.createPromise) {
+            return this.createPromise;
+        }
+        
+        this.createPromise = Promise.all([
+            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 = {
+                cardNumber,
+                cvc,
+                expiry
+            };
+
+            return this.elements;
+
+        }).catch((error) => {
+
+            // 创建失败,允许下一次重新创建
+            this.createPromise = null;
 
-  async createElements() {
+            throw error;
+        });
 
-    if (this.elements.cardNumber) {
+        return this.createPromise;
 
-      return this.elements;
     }
 
-    if (this.createPromise) {
+    mount(containerIds: {
+        cardNumber: string;
+        cvc: string;
+        expiry: string;
+    }) {
+        this.elements.cardNumber?.mount(containerIds.cardNumber);
+        this.elements.cvc?.mount(containerIds.cvc);
+        this.elements.expiry?.mount(containerIds.expiry);
+    }
+
+    on<EventCode extends Payment.SplitElementEventCode>(
+        el: ElementType, 
+        event: EventCode, 
+        handler: Payment.SplitElementEventHandler<EventCode>
+    ) {
 
-      return this.createPromise;
+        el.on(event, handler);
     }
-    
-    this.createPromise = Promise.all([
-      createElement("cardNumber"),
-      createElement("cvc"),
-      createElement("expiry")
-    ]).then(([cardNumber,cvc,expiry]) => {
-
-      this.elements = {
-        cardNumber,
-        cvc,
-        expiry
-      };
-
-      return this.elements;
-
-    }).catch((error) => {
-
-        // 创建失败,允许下一次重新创建
-        this.createPromise = null;
-
-        throw error;
-    });
-
-    return this.createPromise;
-
-  }
-
-  mount(containerIds: {
-    cardNumber: string;
-    cvc: string;
-    expiry: string;
-  }) {
-    this.elements.cardNumber?.mount(containerIds.cardNumber);
-    this.elements.cvc?.mount(containerIds.cvc);
-    this.elements.expiry?.mount(containerIds.expiry);
-  }
-
-  on<EventCode extends Payment.SplitElementEventCode>(
-    el: ElementType, 
-    event: EventCode, 
-    handler: Payment.SplitElementEventHandler<EventCode>
-  ) {
-
-    el.on(event, handler);
-  }
-
-  elementsBindHandler() {
-    const { cardNumber, cvc, expiry } = this.elements;
-    if(!cardNumber || !cvc || !expiry) {
-      throw new Error('Airwallex elements have not been created!');
+
+    elementsBindHandler() {
+        const { cardNumber, cvc, expiry } = this.elements;
+        if(!cardNumber || !cvc || !expiry) {
+            throw new Error('Airwallex elements have not been created!');
+        }
+        cardNumber.on("focus", (e) => {
+            this.handleFocus(e)
+        });
+        cardNumber.on("blur", (e) => {
+            this.handleBlur(e);
+        });
+        cardNumber.on("change", (e) => {
+            this.handleChange(e);
+        });
+        cardNumber.on("ready", (e) => {
+            this.handleReady(e);
+        });
+
+        cvc.on("focus", (e) => {
+            this.handleFocus(e);
+        });
+        cvc.on("blur", (e) => {
+            this.handleBlur(e);
+        });
+        cvc.on("change", (e) => {
+            this.handleChange(e);
+        });
+        cvc.on("ready", (e) => {
+            this.handleReady(e);
+        });
+
+        expiry.on("focus", (e) => {
+            this.handleFocus(e);
+        });
+        expiry.on("blur", (e) => {
+            this.handleBlur(e);
+        });
+        expiry.on("change", (e) => {
+            this.handleChange(e);
+        });
+        expiry.on("ready", (e) => {
+            this.handleReady(e);
+        });
+
     }
-    cardNumber.on("focus", (e) => {
-      this.handleFocus(e)
-    });
-    cardNumber.on("blur", (e) => {
-      this.handleBlur(e);
-    });
-    cardNumber.on("change", (e) => {
-      this.handleChange(e);
-    });
-    cardNumber.on("ready", (e) => {
-      this.handleReady(e);
-    });
-
-    cvc.on("focus", (e) => {
-      this.handleFocus(e)
-    });
-    cvc.on("blur", (e) => {
-      this.handleBlur(e);
-    });
-    cvc.on("change", (e) => {
-      this.handleChange(e);
-    });
-    cvc.on("ready", (e) => {
-      this.handleReady(e);
-    });
-
-    expiry.on("focus", (e) => {
-      this.handleFocus(e)
-    });
-    expiry.on("blur", (e) => {
-      this.handleBlur(e);
-    });
-    expiry.on("change", (e) => {
-      this.handleChange(e);
-    });
-    expiry.on("ready", (e) => {
-      this.handleReady(e);
-    });
-
-  }
 
   // ========================================================
   // Airwallex event handlers
   // ========================================================
 
     handleReady(event: any) {
-      console.log('-----------------airwallex card ready-----------------');
+        console.log('-----------------airwallex card ready-----------------');
         const { type } = event.detail;
 
         if (type === 'cardNumber') {
@@ -487,28 +506,28 @@ class AirwallexManager {
 
     
 
-  destroy() {
-    const { cardNumber, cvc, expiry } = this.elements;
+    destroy() {
+        const { cardNumber, cvc, expiry } = this.elements;
 
-    [cardNumber, cvc, expiry].forEach((el) => {
-      if (!el) return;
-      el.unmount();
-      el.destroy();
-    });
+        [cardNumber, cvc, expiry].forEach((el) => {
+            if (!el) return;
+            el.unmount();
+            el.destroy();
+        });
 
-    this.elements = {};
-    this.createPromise=null;
-    this.resetSnapshot();
+        this.elements = {};
+        this.createPromise=null;
+        this.resetSnapshot();
 
-  }
+    }
 
-  getElements() {
-    return this.elements;
-  }
+    getElements() {
+        return this.elements;
+    }
 
-  getCardNumberElement() {
-      return this.elements.cardNumber;
-  }
+    getCardNumberElement() {
+        return this.elements.cardNumber;
+    }
 }
 
 export const airwallexManager = new AirwallexManager();

+ 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 - 7
src/utils/constants.ts

@@ -214,12 +214,20 @@ 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;