Kaynağa Gözat

接入klarna支付方式 --- v1

fogwind 20 saat önce
ebeveyn
işleme
c6b9b5e491

+ 1 - 1
README.md

@@ -334,7 +334,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 41. 货币切换功能,货币切换后货币符号没有修改问题
 42. 错误处理 https://nextjs.org/docs/app/getting-started/error-handling
 43. redux中的user删除
-44. 关于订单id,管理后台订单列表里显示的是increment_id(数据库里的字段名),前端页面上用的是id(数据库里的字段名)
+44. 关于订单id,管理后台订单列表里显示的是increment_id(数据库里的字段名),前端页面上用的是id(数据库里的字段名)对应接口里的orderId
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 

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

@@ -1,13 +1,15 @@
 "use client";
 
 import { useRouter } from 'next/navigation'
+import { Payment } from "@airwallex/components-sdk";
 import PaypalButton from "./PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import { PlaceOrderFunction } from "@/types/checkout/type";
+import { PlaceOrderFunction,KlarnaOtherData } 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";
 import { useConfig } from '@utils/hooks/useConfig';
+import KlarnaButton from "./PaymentButton/KlarnaButton";
 export default function CheckoutPlaceOrder({
     paymentMethod,
     clickPlaceOrder, 
@@ -25,7 +27,15 @@ export default function CheckoutPlaceOrder({
     const paypalCreateOrder = async () => {
         overlayLoading.start();
         const valid = await validateCheckout();
-        if(!valid) {overlayLoading.stop();}
+        if(!valid) {
+            overlayLoading.stop();
+            return {
+                error: true,
+                msg: '',
+                orderId: '',
+                webOrderId: ''
+            };
+        }
         // 校验通过
         const placeOrderRes = await clickPlaceOrder();
         if(placeOrderRes.data !== null) {
@@ -49,7 +59,10 @@ export default function CheckoutPlaceOrder({
     const airwallexPlaceOrder = async () => {
         overlayLoading.start();
         const valid = await validateCheckout();
-        if(!valid) {overlayLoading.stop();}
+        if(!valid) {
+            overlayLoading.stop();
+            return;
+        }
 
         const placeOrderRes = await clickPlaceOrder({
             paymentSuccessUrl: window.location.origin + '/paymentresult/result'
@@ -63,9 +76,12 @@ export default function CheckoutPlaceOrder({
     const cardAirwallexPlaceOrder = async () => {
         overlayLoading.start();
         const valid = await validateCheckout();
-        if(!valid) {overlayLoading.stop();}
+        if(!valid) {
+            overlayLoading.stop();
+            return;
+        }
 
-        const placeOrderRes = await clickPlaceOrder();
+        const placeOrderRes = await clickPlaceOrder<Payment.CardNumberElementType>();
         if(placeOrderRes.data !== null && placeOrderRes.otherData !== null) {
             const webOrderId = placeOrderRes.data.orderId;
             const cardNumberElement = placeOrderRes.otherData;
@@ -94,7 +110,10 @@ export default function CheckoutPlaceOrder({
         if(window.AfterPay) {
             overlayLoading.start();
             const valid = await validateCheckout();
-            if(!valid) {overlayLoading.stop();} 
+            if(!valid) {
+                overlayLoading.stop();
+                return;
+            } 
 
             const placeOrderRes = await clickPlaceOrder({
                 paymentSuccessUrl: window.location.origin + '/paymentresult/result',
@@ -166,6 +185,112 @@ export default function CheckoutPlaceOrder({
         }
     };
 
+    const klarnaPlaceOrder = async () => { 
+        overlayLoading.start();
+        const valid = await validateCheckout();
+        if(!valid) {
+            overlayLoading.stop();
+            return;
+        }
+
+        const placeOrderRes = await clickPlaceOrder<KlarnaOtherData>({
+            paymentSuccessUrl: window.location.origin + '/paymentresult/result',
+            paymentCancelUrl: window.location.origin + '/paymentresult/result'
+        });
+        
+        if(placeOrderRes.data !== null && placeOrderRes.otherData) {
+            const klarnaToken = placeOrderRes.data.gatewayOrderId;
+            const webOrderId = placeOrderRes.data.orderId;
+            const {billingAddress, shippingAddress} = placeOrderRes.otherData;
+
+            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) { // 可以支付
+
+                                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');
+                            }
+                        }
+                    );
+                    
+
+                }
+            });
+            
+        }
+        
+    };
+
     return (
         <>
             <div className="relative w-full">
@@ -195,6 +320,9 @@ export default function CheckoutPlaceOrder({
                         Place Order
                     </button>
                 }
+                {paymentMethod === 'klarna' &&
+                    <KlarnaButton clickHandler={klarnaPlaceOrder} />
+                }
                 
 
             </div>

+ 41 - 21
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,7 +1,6 @@
 "use client";
 
 import {useState, useRef, useEffect } from "react";
-import { Payment } from "@airwallex/components-sdk";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 import { useForm, FormProvider } from "react-hook-form";
@@ -9,9 +8,9 @@ import {
     ShipAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
-    ExtraMapPlaceOrderResult,
     PlaceOrderResult,
-    PlaceOrderFunction
+    PlaceOrderFunction,
+    CreatePaymentInitiateVariables
 } from "@/types/checkout/type";
 import AddressResultDisplay from "./AddressResultDisplay";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
@@ -36,8 +35,8 @@ 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";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
+import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -221,6 +220,9 @@ export default function CheckoutWrapper({
     loginEmail: string;
     cartDetailData: CartDetail;
 }) {
+
+    const {loadSdk} = usePaymentSDKContext();
+
     const { createOrder } = usePlaceOrder(); 
     const addressFormDefaultValues = getAddressFormDataFromCart(cartDetailData,loginEmail);
     const dispatch = useAppDispatch();
@@ -266,8 +268,19 @@ export default function CheckoutWrapper({
 
     const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
 
+    const changePaymentMethodToloadSdk = (method:string) => {
+        if(method === 'klarna' || 
+            method === 'afterpay' || 
+            method === 'clearpay' || 
+            method === 'applepay'
+        ) {
+            loadSdk(method);
+        }
+    };
 
-
+    useEffect(() => {
+        changePaymentMethodToloadSdk(selectedPaymentMethod);
+    }, [selectedPaymentMethod]);
 
     const openAddressModal = () => {
         setAddressModalOpen(true);
@@ -338,6 +351,7 @@ export default function CheckoutWrapper({
 
 
     const handlePaymentMethodChange =  async (method: string) => { 
+        changePaymentMethodToloadSdk(method);
         let shippingMethod = '-1';
         if(methodShiippingRef.current) {
             shippingMethod = methodShiippingRef.current.getSelectShipMethod();
@@ -461,7 +475,7 @@ export default function CheckoutWrapper({
         } 
     };
     // 下单
-    const handlePlaceOrder: PlaceOrderFunction = async (params) => { 
+    const handlePlaceOrder: PlaceOrderFunction = async <T,>(params?: CreatePaymentInitiateVariables): Promise<PlaceOrderResult<T>>  => { 
         // 校验地址
         // overlayLoading.start();
         // const validRes = await validateCheckout();
@@ -479,29 +493,35 @@ export default function CheckoutWrapper({
                 dispatch(clearCart());  
             }
             const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
-            const resultData:PlaceOrderResult<keyof ExtraMapPlaceOrderResult> = {
-                // valid: true, 
-                data: res.data,
-                otherData: null
-            }
+            let resultData: PlaceOrderResult<T>;
             if(pmethod === 'airwallex' && methodPaymentRef.current) {
                 const airwallexCardElements = methodPaymentRef.current.getAirwallexCardElements();
-                resultData.otherData = airwallexCardElements.cardNumber as Payment.CardNumberElementType;
+                const airCardNumber = airwallexCardElements.cardNumber as T;
+                resultData = {
+                    data: res.data, 
+                    otherData: airCardNumber
+                };
+            } else if(pmethod === 'klarna') {
+                const klaData = {
+                    billingAddress: cartData.billingAddress,
+                    shippingAddress: cartData.shippingAddress
+                } as T;
+                resultData = {
+                    data: res.data, 
+                    otherData: klaData
+                };
+            } else {
+                resultData = {
+                    data: res.data, 
+                    otherData: null
+                };
             }
             return resultData;
         // } else {
         //     overlayLoading.stop();
         //     return {valid: false, data: null, otherData: null};
         // }
-    };                    
-    
-    useEffect(() => {
-        const hasAfterpay = paymentMethodDatas.some(x=>x.method==="afterpay");
-        const hasClearpay = paymentMethodDatas.some(x=>x.method==="clearpay");
-        if(hasAfterpay || hasClearpay){
-            loadAfterpayScript();
-        }
-    }, [paymentMethodDatas]);
+    }
 
 
     // function tt() {

+ 142 - 8
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,7 +1,7 @@
 "use client";
 
 import Image from "next/image";
-import { use,useState,useEffect,useCallback } from "react";
+import { use,useState,useCallback } from "react";
 import { useForm, get, useWatch } from "react-hook-form";
 import { useRouter } from 'next/navigation'
 import { useCustomToast } from "@/utils/hooks/useToast";
@@ -14,10 +14,11 @@ import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import PaypalButton from "../PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import {loadAfterpayScript} from "@/lib/Afterpay/afterpay";
 import PaypalApplepayButton from "../PaymentButton/PaypalApplepayButton";
+import KlarnaButton from "../PaymentButton/KlarnaButton";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import { useConfig } from '@utils/hooks/useConfig';
+import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 
 /**@todo 优化目标:不使用 react-hook-form */
 export function PaymentMethodContinueTpPay({
@@ -38,6 +39,7 @@ export function PaymentMethodContinueTpPay({
 
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
+    const {loadSdk} = usePaymentSDKContext();
     const {store} = useConfig();
     const currentCurrencyCode  = store.currentCurrency;
 
@@ -275,6 +277,14 @@ export function PaymentMethodContinueTpPay({
                     }
                 }
                 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();
@@ -304,15 +314,131 @@ export function PaymentMethodContinueTpPay({
         return true;
     }, []);
 
-    const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
+    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) { // 可以支付
 
-    useEffect(() => {
-        const hasAfterpay = paymentMethods.some(x=>x.method==="afterpay");
+                                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('/');
+                                    });
+                                    
+                                }
 
-        if(hasAfterpay){
-            loadAfterpayScript();
+                            } 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(() => {});
+            }
+        
+    };
+
+    const changePaymentMethod = async (method: string) => { 
+        if(method === 'klarna' || 
+            method === 'afterpay' || 
+            method === 'clearpay' || 
+            method === 'applepay'
+        ) {
+            loadSdk(method);
         }
-    }, [paymentMethods]);
+    };
+
+
+    const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
+
     return (
         <>
             <form name="shipping method" className="w-full box-border px-4 mt-4">
@@ -344,6 +470,9 @@ export function PaymentMethodContinueTpPay({
                                         value={item.method}
                                         {...register('paymentMethod', {
                                             required: 'Please select a shipping method.',
+                                            onChange: (event) => {
+                                                changePaymentMethod(event.target.value);
+                                            },
                                         })}
                                     />
                                 </label> 
@@ -412,6 +541,11 @@ export function PaymentMethodContinueTpPay({
                             isRePay={true}
                         />
                     }
+                    {selectedPaymentMethod === 'klarna' &&
+                        <KlarnaButton 
+                            clickHandler={klarnaPlaceOrder}
+                        />
+                    }
 
                 </div>
             </div>

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


+ 0 - 4
src/app/layout.tsx

@@ -1,4 +1,3 @@
-import Script from 'next/script'
 import localFont from "next/font/local";
 import { cookies } from "next/headers";
 import "./globals.css";
@@ -120,9 +119,6 @@ export default async function RootLayout({
         "min-h-screen font-outfit text-foreground bg-background antialiased",
         outfit.variable
       )}>
-        {/* <Script src="https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js" /> */}
-        <Script src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js" strategy="beforeInteractive" />
-        
         <main>
           <ErrorBoundary>
             <PayPalWrapper clientId={paypalClientId}>

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

@@ -7,6 +7,7 @@ export const CREATE_PAYMENT_CALLBACK: TypedDocumentNode<CreatePaymentCallbackDat
       $gatewayOrderId: String
       $status: String
       $paymentSignature: String
+      $verifyToken: String
       
       $shippingFirstName: String
       $shippingLastName: String
@@ -34,6 +35,7 @@ export const CREATE_PAYMENT_CALLBACK: TypedDocumentNode<CreatePaymentCallbackDat
           gatewayOrderId: $gatewayOrderId
           status: $status
           paymentSignature: $paymentSignature
+          verifyToken: $verifyToken
           
           shippingFirstName: $shippingFirstName
           shippingLastName: $shippingLastName

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

@@ -1,33 +0,0 @@
-// 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);
-  });
-}

+ 3 - 0
src/providers/GlobalProviders.tsx

@@ -4,13 +4,16 @@ import { ReactNode } from "react";
 import { ThemeProvider } from "./ThemeProvider";
 import { ToastProvider } from "./ToastProvider";
 import { ApolloWrapper } from "./ApolloWrapper";
+import { PaymentSDKProvider } from "./PaymentSDKProvider";
 
 export function GlobalProviders({ children }: { children: ReactNode }) {
   return (
     <ThemeProvider>
         <ToastProvider>
           <ApolloWrapper>
+            <PaymentSDKProvider>
             {children}
+            </PaymentSDKProvider>
           </ApolloWrapper>
         </ToastProvider>
     </ThemeProvider>

+ 154 - 0
src/providers/PaymentSDKProvider.tsx

@@ -0,0 +1,154 @@
+"use client";
+
+import Script from "next/script";
+import {
+  createContext,
+  useContext,
+  useState
+} from "react";
+
+type SDKLoadState = "none" | "loading" | "error" | "success";
+type SDKForPaymentMethod = "klarna" | "afterpay" | "clearpay" | "applepay";
+type SDKName = "klarna" | "afterpay" | "applepay";
+interface PaymentSDKState {
+  klarna: SDKLoadState;
+  afterpay: SDKLoadState;
+  applepay: SDKLoadState;
+}
+
+interface PaymentContextProps {
+  loadSdk: (method:string) => void;
+  getSDKState: (method:SDKForPaymentMethod) => SDKLoadState | undefined;
+  loadSDKState: PaymentSDKState;
+}
+
+const klarnaSDKURL = "https://x.klarnacdn.net/kp/lib/v1/api.js";
+const afterpaySDKURL = process.env.NODE_ENV === "production" ? "https://portal.afterpay.com/afterpay.js" : "https://portal.sandbox.afterpay.com/afterpay.js";
+const applepaySDKURL = "https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js";
+
+const PaymentSDKContext = createContext<PaymentContextProps|null>(null);
+
+const normalizeSDKName = (method:string):SDKName| undefined =>{
+
+    switch(method){
+
+        case "clearpay":
+        case "afterpay":
+            return "afterpay";
+
+        case "klarna":
+            return "klarna";
+
+        case "applepay":
+            return "applepay";
+        default: 
+            return undefined;
+    }
+
+}
+export function PaymentSDKProvider({
+    children
+}: {
+    children:React.ReactNode;
+}){
+
+    const [loadSDKState, setLoadSDKState] = useState<PaymentSDKState>({
+        klarna: 'none', // none loading error success
+        afterpay: 'none', // none loading error success
+        applepay: 'none' // none loading error success
+    });
+
+    const loadSdk = (method:string) => {
+        const m = normalizeSDKName(method);
+        if(m) {
+            setLoadSDKState((prev) => {
+
+                if(prev[m] !== "none") {
+                    return prev;
+                }
+
+                return {
+                    ...prev,
+                    [m]:"loading"
+                };
+            });
+        }
+        
+    }
+    const onSdkLoad = (m:SDKName) => {
+        setLoadSDKState((prev) => {
+            return {
+                ...prev,
+                [m]: 'success'
+            }
+        });
+    };
+    const onSdkError = (m:SDKName) => {
+        setLoadSDKState((prev) => {
+            return {
+                ...prev,
+                [m]: 'error'
+            }
+        });
+    };
+    const getSDKState = (method:SDKForPaymentMethod) => {
+        const m = normalizeSDKName(method);
+        if(m) {
+            return loadSDKState[m];
+        } else {
+            return undefined;
+        }
+        
+    }
+
+    return (
+        <PaymentSDKContext.Provider
+            value={{
+                loadSdk,
+                getSDKState,
+                loadSDKState
+            }}
+        >
+            {loadSDKState.klarna !== "none" &&
+                <Script id="klarna-sdk" strategy="afterInteractive"
+                    src={klarnaSDKURL}
+                    onLoad={() => onSdkLoad('klarna')}
+                    onError={() => {
+                        onSdkError('klarna');
+                    }}
+                />
+            }
+            { loadSDKState.afterpay !== "none" &&
+                <Script id="afterpay-sdk" strategy="afterInteractive"
+                    src={afterpaySDKURL}
+                    onLoad={() => onSdkLoad('afterpay')}
+                    onError={() => {
+                        onSdkError('afterpay');
+                    }}
+                />
+            }
+            { loadSDKState.applepay !== "none" &&
+                <Script id="applepay-sdk" strategy="afterInteractive"
+                    src={applepaySDKURL}
+                    onLoad={() => onSdkLoad('applepay')}
+                    onError={() => {
+                        onSdkError('applepay');
+                    }}
+                />
+            }
+
+            {children}
+
+        </PaymentSDKContext.Provider>
+    )
+
+}
+
+export const usePaymentSDKContext = () => {
+    const context = useContext(PaymentSDKContext);
+    if (!context) {
+        throw new Error("usePaymentSDKContext must be used within a PaymentSDKProvider");
+    }
+
+    return context;
+};

+ 10 - 9
src/types/checkout/type.ts

@@ -1,4 +1,4 @@
-import {CartDetail} from "@/types/cart/type";
+import {CartDetail,CartAddress} from "@/types/cart/type";
 import { Payment } from '@airwallex/components-sdk';
 
 export type AddressType = "cart_billing" | "cart_shipping";
@@ -266,6 +266,7 @@ export interface CreatePaymentCallbackVariables {
   gatewayOrderId: string;
   status: "success" | "cancel" | "failure";
   paymentSignature?: string;
+  verifyToken?: string;
   
   // 快捷支付需要
   shippingFirstName?: string;
@@ -472,19 +473,19 @@ export interface CreatePaymentReplayVariables {
 
 
 // 结账页额外用到的类型
-export type ExtraMapPlaceOrderResult = {
-  AirwallexCad: Payment.CardNumberElementType ;
-  
-};
+export type AirwallexCadOtherData = Payment.CardNumberElementType;
 
-export type ExtraDataPlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> = ExtraMapPlaceOrderResult[Code];
-export interface PlaceOrderResult<Code extends keyof ExtraMapPlaceOrderResult> {
+export type KlarnaOtherData = {
+  billingAddress: CartAddress;
+  shippingAddress: CartAddress;
+}
+export interface PlaceOrderResult<T> {
     // valid: boolean; 
     data: CreatePaymentInitiate | null;
-    otherData: ExtraDataPlaceOrderResult<Code> | null;
+    otherData: T | null;
 }
 
-export type PlaceOrderFunction = (params?: CreatePaymentInitiateVariables) => Promise<PlaceOrderResult<keyof ExtraMapPlaceOrderResult>>
+export type PlaceOrderFunction = <T = null>(params?: CreatePaymentInitiateVariables) => Promise<PlaceOrderResult<T>>
 
 export interface AirwallexCardElements{
   cardNumber?: Payment.CardNumberElementType;

+ 42 - 0
src/types/klarna.d.ts

@@ -0,0 +1,42 @@
+
+interface KlarnaLoadResponse {
+    show_form: boolean;
+    error?: {
+      invalid_fields?: string[];
+    };
+}
+
+interface KlarnaAuthorizeResponse {
+    approved: boolean;
+    show_form: boolean;
+    authorization_token?: string;
+    error?: {
+      invalid_fields?: string[];
+    };
+}
+
+
+interface KlarnaPayments {
+    init: (options: { client_token: string }) => void;
+    load: (
+      options: { container: string; payment_method_category?: string },
+      callback?: (res: KlarnaLoadResponse) => void
+    ) => void;
+    authorize: (
+      options?: { payment_method_category?: string },
+      data?: Record<string, unknown>,
+      callback?: (res: KlarnaAuthorizeResponse) => void
+    ) => void;
+}
+
+declare global {
+
+ interface Window {
+    Klarna: {
+      Payments: KlarnaPayments;
+    };
+ }
+
+}
+// 这里的export {} 起到一个标记的作用,告诉Typescript,这个文件是一个模块,而不是一个全局变量。
+export {};