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