ソースを参照

合并dev-afterpay

fogwind 1 週間 前
コミット
0ef86c8b64

+ 1 - 0
README.md

@@ -325,6 +325,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
 36. 学习set-state-in-effect,然后检查代码,看是否可以优化(特别是CommonModal组件,看能不能不使用useEffect实现)
 37. paypal provider 支付参数根据环境变量配置
+38. CartAndUserActions 组件在支付结果页多次挂载问题
 
 ## 关于购物车数据管理方案
 ssr + redux:

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

@@ -4,6 +4,9 @@ import { useRouter } from 'next/navigation'
 import PaypalButton from "./PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { PlaceOrderFunction } from "@/types/checkout/type";
+import { useCustomToast } from "@/utils/hooks/useToast";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 export default function CheckoutPlaceOrder({
     paymentMethod,
     clickPlaceOrder, // 校验运输方式,地址 
@@ -12,7 +15,8 @@ export default function CheckoutPlaceOrder({
     clickPlaceOrder: PlaceOrderFunction;
 }) {
     const router = useRouter();
-    
+    const { showToast } = useCustomToast();
+    const { createPaymentCallback } = usePlaceOrder();
     const paypalCreateOrder = async () => {
 
         const placeOrderRes = await clickPlaceOrder();
@@ -71,6 +75,66 @@ export default function CheckoutPlaceOrder({
         }
     };
 
+    const afterpayPlaceOrder = async () => { 
+        if(window.AfterPay) { 
+            const placeOrderRes = await clickPlaceOrder({
+                paymentSuccessUrl: window.location.origin + '/paymentresult/result',
+                paymentCancelUrl: window.location.origin + '/paymentresult/result'
+            });
+            
+            if(placeOrderRes.valid && placeOrderRes.data !== null) {
+                const afterpayToken = placeOrderRes.data.gatewayOrderId;
+                const webOrderId = placeOrderRes.data.orderId;
+                window.AfterPay.initialize({countryCode: "US"});
+                // 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);
+                    let 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.
+                        // 跳转结果页 取消
+                        router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=cancel');
+                    }
+                }
+                window.AfterPay.transfer({token: afterpayToken});
+            }
+        } else {
+            overlayLoading.stop();
+            showToast("Please wait for the Afterpay to load completely.","warning");
+        }
+    };
+
     return (
         <>
             <div className="relative w-full">
@@ -93,6 +157,13 @@ export default function CheckoutPlaceOrder({
                         Place Order
                     </button>
                 }
+                {paymentMethod === 'afterpay' &&
+                    <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                        onClick={afterpayPlaceOrder}
+                    >
+                        Place Order
+                    </button>
+                }
                 
 
             </div>

+ 9 - 2
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState, useRef } from "react";
+import {useState, useRef, useEffect } from "react";
 import { Payment } from "@airwallex/components-sdk";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
@@ -37,6 +37,7 @@ import { CartDetail,CartAddress } from "@/types/cart/type";
 import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { clearCart } from "@/store/slices/cart-slice";
+import {loadAfterpayScript} from "@/lib/Afterpay/afterpay";
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -481,7 +482,13 @@ export default function CheckoutWrapper({
         }
     };                    
     
-    
+    useEffect(() => {
+        const hasAfterpay = paymentMethodDatas.some(x=>x.method==="afterpay");
+
+        if(hasAfterpay){
+            loadAfterpayScript();
+        }
+    }, [paymentMethodDatas]);
 
 
     // function tt() {

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

@@ -1,7 +1,7 @@
 "use client";
 
 import Image from "next/image";
-import { use,useState } from "react";
+import { use,useState,useEffect } from "react";
 import { useForm, get, useWatch } from "react-hook-form";
 import { useRouter } from 'next/navigation'
 import { useCustomToast } from "@/utils/hooks/useToast";
@@ -13,6 +13,7 @@ import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import PaypalButton from "../PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import {loadAfterpayScript} from "@/lib/Afterpay/afterpay";
 
 /**@todo 优化目标:不使用 react-hook-form */
 export function PaymentMethodContinueTpPay({
@@ -22,14 +23,14 @@ export function PaymentMethodContinueTpPay({
 }: {
     paymentMethodsDataPromise: Promise<GraphqlRequestResult<RepayOrderPaymentMethodsData>>;
     defaultPaymentMethod: {method: string; methodTitle: string;}
-    orderId: string;
+    orderId: number;
 }) {
     const router = useRouter();
     const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
     const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
 
-    const { paymentRepay } = usePlaceOrder(); 
+    const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
     const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
         cardNumber: 'airwallex_cardNumber', 
@@ -192,8 +193,83 @@ export function PaymentMethodContinueTpPay({
         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;
+                window.AfterPay.initialize({countryCode: "US"});
+                // 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);
+                    let 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();
+            showToast("Please wait for the Afterpay to load completely.","warning");
+        }
+    };
+
     const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
+    useEffect(() => {
+        const hasAfterpay = paymentMethods.some(x=>x.method==="afterpay");
+
+        if(hasAfterpay){
+            loadAfterpayScript();
+        }
+    }, [paymentMethods]);
     return (
         <>
             <form name="shipping method" className="w-full box-border px-4 mt-4">
@@ -276,6 +352,13 @@ export function PaymentMethodContinueTpPay({
                             Pay
                         </button>
                     }
+                    {selectedPaymentMethod === 'afterpay' && 
+                        <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
+                            onClick={afterpayPlaceOrder}
+                        >
+                            Place Order
+                        </button>
+                    }
 
                 </div>
             </div>

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

@@ -79,7 +79,7 @@ export default async function ContinueToPay({searchParams}: {
             </div>
             <Suspense fallback={<LoadingPaymentMethod/>}> 
                 <PaymentMethodContinueTpPay 
-                    orderId={orderDetailData.incrementId}
+                    orderId={orderDetailData._id}
                     paymentMethodsDataPromise={paymentMethodsResponse}
                     defaultPaymentMethod={orderDetailData.payment}
                 />

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

@@ -21,7 +21,7 @@ export default function CancelOrFailure({
     const promiseRes = use(orderDetailPromise);
     const orderDetailRes = promiseRes.data?.customerOrder ?? null;
     const {cancelOrder} = usePlaceOrder();
-    const orderId = orderDetailRes?.incrementId;
+    const orderId = orderDetailRes?._id;
     const orderStatus = orderDetailRes?.status;
     const cancelOrderRef = useRef(false);
     useEffect(() => {

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

@@ -70,7 +70,7 @@ export default function OrderDetailWrapper({
                 </div>
                 <p className="text-ly-16 text-center mt-6">Your Order Has Been Received</p>
                 <p className="text-ly-16 mt-4 flex justify-center">
-                    <span className="px-3.75 leading-ly-28 bg-[#f0fffa]">Order #{orderDetailRes?.incrementId}</span>
+                    <span className="px-3.75 leading-ly-28 bg-[#f0fffa]">Order #{orderDetailRes?._id}</span>
                 </p>
                 <p className="text-ly-12 text-center mt-4 leading-ly-22">
                     Hello, {orderDetailRes?.customerFirstName} <br/>
@@ -105,7 +105,7 @@ export default function OrderDetailWrapper({
                 <div className="mt-6 flex justify-between">
                     <div className="text-ly-12 flex-none">Order number:</div>
                     <div className="w-40.5 text-ly-12 leading-ly-20 text-[#666666] flex-none">
-                        {orderDetailRes?.incrementId}
+                        {orderDetailRes?._id}
                     </div>
                 </div>
                 <div className="mt-6 flex justify-between">

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

@@ -6,6 +6,8 @@ export const GET_ORDER_DETAILS: TypedDocumentNode<OrderDetailsData> = gql`
   query GetCustomerOrder($id: ID!) {
   customerOrder(id: $id) { 
     incrementId
+    id
+    _id
     status
     channelName
     customerEmail

+ 33 - 0
src/lib/Afterpay/afterpay.ts

@@ -0,0 +1,33 @@
+// lib/afterpay.ts
+
+let loaded = false;
+
+export function loadAfterpayScript() {
+  return new Promise<void>((resolve, reject) => {
+
+    if (loaded || window.AfterPay) {
+      resolve();
+      return;
+    }
+
+
+    const script = document.createElement("script");
+    const scriptUrl = process.env.NODE_ENV === "production" ? "https://portal.afterpay.com/afterpay.js" : "https://portal.sandbox.afterpay.com/afterpay.js";
+
+    script.src = scriptUrl;
+
+    script.async = true;
+
+
+    script.onload = () => {
+      loaded = true;
+      resolve();
+    };
+
+
+    script.onerror = reject;
+
+
+    document.body.appendChild(script);
+  });
+}

+ 32 - 0
src/types/afterpay.d.ts

@@ -0,0 +1,32 @@
+interface AfterPaySDK {
+
+  initialize(options:{
+    countryCode:string;
+  }):void;
+
+
+  redirect(options:{
+    token:string;
+  }):void;
+
+
+  open():void;
+
+  onComplete?:
+    (event:any)=>void;
+
+  transfer(options:{
+    token:string;
+  }):void;
+}
+
+
+declare global {
+
+ interface Window {
+    AfterPay: AfterPaySDK;
+ }
+
+}
+
+export {};

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

@@ -78,6 +78,8 @@ export interface ProductItemAdditional {
 
 export interface OrderDetails {
     incrementId: string;
+    id: string;
+    _id: number;
     status: string;
     channelName: string;
     customerEmail: string;