PaypalApplepayButton.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. "use client";
  2. import {useRef,useEffect,useCallback} from "react";
  3. import { useRouter } from 'next/navigation';
  4. // import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
  5. import {
  6. usePayPal,
  7. useEligibleMethods,
  8. INSTANCE_LOADING_STATE,
  9. useApplePayOneTimePaymentSession,
  10. type ConfirmOrderResponse,
  11. type UseApplePayOneTimePaymentSessionProps,
  12. } from "@paypal/react-paypal-js/sdk-v6";
  13. import { useAppSelector } from "@/store/hooks";
  14. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  15. import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
  16. import { CartAddress } from "@/types/cart/type";
  17. import { useConfig } from "@/utils/hooks/useConfig";
  18. import {selectCurrentCurrency} from "@/store/selectors/currentConfig";
  19. import {ApplePayAddressContact} from "@/lib/Applepay/type";
  20. // import {getPaypalApplePayConfig, type PaypalApplePaySession} from "@/lib/Applepay/applepay";
  21. export default function PaypalApplepayButton({
  22. createOrder,
  23. validateCheckout,
  24. billingContact,
  25. shippingContact,
  26. grandTotal,
  27. isRePay
  28. }:{
  29. validateCheckout: () => Promise<boolean>;
  30. createOrder: () => Promise<{orderId: string;webOrderId: string;}>;
  31. billingContact: CartAddress | null;
  32. shippingContact: CartAddress | null;
  33. grandTotal: number;
  34. isRePay?: boolean;
  35. }) {
  36. const router = useRouter();
  37. // const {createPaymentCallback} = usePlaceOrder();
  38. const {countries,store} = useConfig();
  39. const currentCurrency = useAppSelector(selectCurrentCurrency);
  40. const currencyCode = currentCurrency.code;
  41. const storeName = store.storeName;
  42. const gatewayOrderIdRef = useRef("");
  43. const webOrderIdRef = useRef("");
  44. // const [configCountryCode, setConfigCountryCode] = useState('');
  45. // paypal sdk 是否加载完成
  46. const { loadingStatus, isHydrated } = usePayPal();
  47. // Fetch eligibility(资格) for one-time payment flow
  48. const {
  49. error: eligibilityError,
  50. eligiblePaymentMethods,
  51. isLoading: isEligibilityLoading,
  52. } = useEligibleMethods({
  53. payload: {
  54. currencyCode: currencyCode, // 需要根据用户选择的货币走
  55. paymentFlow: "ONE_TIME_PAYMENT", // 一次性支付
  56. },
  57. });
  58. const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
  59. const isApplepayEligible = eligiblePaymentMethods?.isEligible("applepay");
  60. const applePayConfig = eligiblePaymentMethods?.getDetails("applepay");
  61. // 支付回调配置
  62. /**
  63. * when the user has authorized the Apple Pay payment with Touch ID, Face ID, or a passcode.
  64. * ApplePaySession onpaymentauthorized 时触发 */
  65. const configCreateOrder = useCallback(async () => {
  66. const res = await createOrder();
  67. gatewayOrderIdRef.current = res.orderId;
  68. webOrderIdRef.current = res.webOrderId;
  69. return res;
  70. },[createOrder]);
  71. /**await paypal.Applepay().confirmOrder() 后调onApprove */
  72. const onApprove = useCallback((data: ConfirmOrderResponse) => {
  73. console.log("applepay approved:", data);
  74. /**
  75. * approveApplePayPayment: {
  76. * id: string; //gatewayOrderId
  77. * status: "APPROVED"
  78. * }
  79. */
  80. overlayLoading.stop();
  81. router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success');
  82. },[router]);
  83. /**ApplePaySession oncancel */
  84. const onCancel = useCallback(() => {
  85. console.log("applepay cancelled:");
  86. confirmDialog({
  87. title: "Payment cancelled",
  88. content: "You canceled the payment.",
  89. noCancel: true,
  90. }).then(() => { });
  91. },[]);
  92. /**
  93. * 1. ApplePaySession onvalidatemerchant 出错
  94. * 2. ApplePaySession completePayment 出错
  95. * 3. createOrder->paypal.Applepay().confirmOrder()->onApprove() 链条出错
  96. * 4. 用户点击后的整个链条出错
  97. */
  98. const onError = useCallback((error: Error) => {
  99. console.error("applepay error:", error);
  100. if(!isRePay) {
  101. overlayLoading.stop();
  102. if(webOrderIdRef.current || gatewayOrderIdRef.current) {
  103. // 订单创建成功了,跳转二次支付页面
  104. confirmDialog({
  105. title: "Payment failed",
  106. content: error.message,
  107. noCancel: true,
  108. }).then(() => {
  109. router.replace('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure');
  110. });
  111. } else {
  112. // 订单创建失败,留在当前页面
  113. confirmDialog({
  114. title: "Payment failed",
  115. content: error.message + " Please try again.",
  116. noCancel: true,
  117. }).then(() => { });
  118. }
  119. } else {
  120. // 登录用户 二次支付页面
  121. confirmDialog({
  122. title: "Payment Failed",
  123. content: error.message,
  124. noCancel: true,
  125. }).then(() => { });
  126. }
  127. },[router,isRePay]);
  128. const addressContact: ApplePayAddressContact = {};
  129. if(billingContact) {
  130. const billCountry = countries.find((item) => item.code === billingContact.country);
  131. addressContact.billingContact = {
  132. addressLines: [billingContact.address],
  133. administrativeArea: billingContact.state,
  134. country: billCountry?.name,
  135. countryCode: billingContact.country,
  136. emailAddress: billingContact.email,
  137. familyName: billingContact.lastName,
  138. givenName: billingContact.firstName,
  139. locality: billingContact.city,
  140. postalCode:billingContact.postcode,
  141. }
  142. }
  143. if(shippingContact) {
  144. const shipCountry = countries.find((item) => item.code === shippingContact.country);
  145. addressContact.shippingContact = {
  146. addressLines: [shippingContact.address],
  147. administrativeArea: shippingContact.state,
  148. country: shipCountry?.name,
  149. countryCode: shippingContact.country,
  150. emailAddress: shippingContact.email,
  151. familyName: shippingContact.lastName,
  152. givenName: shippingContact.firstName,
  153. locality: shippingContact.city,
  154. postalCode:shippingContact.postcode,
  155. }
  156. }
  157. console.log('applePayConfig ---- ', applePayConfig?.config);
  158. /*
  159. useEffect(() => {
  160. if(!sdkInstance || !isHydrated) return;
  161. const getConfig = async () => {
  162. const paypalSdkApplePayPaymentSession = await sdkInstance.createApplePayOneTimePaymentSession() as PaypalApplePaySession;
  163. const res = await getPaypalApplePayConfig(paypalSdkApplePayPaymentSession);
  164. console.log('res ---- ', res);
  165. setConfigCountryCode(res.countryCode);
  166. };
  167. getConfig();
  168. }, [isHydrated, sdkInstance]);
  169. */
  170. const merchantCountry = applePayConfig?.config.merchantCountry;
  171. if(!isApplepayEligible) {
  172. return <p className="text-ly-12 text-ly-errorcolor text-center">Applepay is not available on your device.</p>;
  173. }
  174. return (
  175. <div className="box-border w-full">
  176. {isLoading || !isHydrated || isEligibilityLoading || applePayConfig === undefined ?
  177. <p className="text-ly-12 text-center">Applepay is loading...</p>
  178. :
  179. <ApplePayButtonBox
  180. onClickHandler={validateCheckout}
  181. eligibilityError={eligibilityError}
  182. applePayConfig={applePayConfig.config}
  183. applePaySessionVersion={4}
  184. paymentRequest={{
  185. countryCode: merchantCountry === 'C2' ? 'CN' : merchantCountry as string,
  186. currencyCode: currencyCode,
  187. requiredBillingContactFields: ["postalAddress"],
  188. requiredShippingContactFields: ["name", "email", "postalAddress"],
  189. total: {
  190. label: storeName,
  191. amount: String(grandTotal),
  192. type: "final",
  193. },
  194. ...addressContact
  195. }}
  196. createOrder={configCreateOrder}
  197. onApprove={onApprove}
  198. onCancel={onCancel}
  199. onError={onError}
  200. />
  201. }
  202. </div>
  203. );
  204. }
  205. // ApplePayButtonBox是 @paypal/react-paypal-js/sdk-v6 中的<ApplePayOneTimePaymentButton>组件的代码复制过来的
  206. function ApplePayButtonBox({
  207. eligibilityError,
  208. onClickHandler, // 点击苹果支付按钮
  209. ...hookProps
  210. }: {
  211. eligibilityError: Error | null;
  212. onClickHandler: () => Promise<boolean>;
  213. } & UseApplePayOneTimePaymentSessionProps
  214. ) {
  215. const { error, handleClick, handleDestroy } = useApplePayOneTimePaymentSession(hookProps);
  216. const buttonRef = useRef<HTMLElement>(null);
  217. const handleClickRef = useRef(handleClick);
  218. // handleClickRef.current = handleClick;
  219. useEffect(() => {
  220. handleClickRef.current = handleClick;
  221. }, [handleClick]);
  222. // Apple's <apple-pay-button> manages its own enabled/disabled state internally
  223. // via canMakePayments(); we deliberately don't add an SDK-level disabled layer
  224. // (merchants control presentation themselves). React's onClick also doesn't
  225. // work on the element due to its shadow DOM, so we attach the handler directly.
  226. useEffect(() => {
  227. const el = buttonRef.current;
  228. if (!el) { return; }
  229. const applepayButtonClick = () => {
  230. onClickHandler().then((res) => {
  231. if(res) {
  232. handleClickRef.current().catch(() => {
  233. // Errors are captured by the hook's setError
  234. });
  235. }
  236. }).catch((err) => {
  237. console.error(err);
  238. });
  239. };
  240. el.addEventListener("click", applepayButtonClick);
  241. return () => el.removeEventListener("click", applepayButtonClick);
  242. }, [onClickHandler]);
  243. useEffect(() => {
  244. if (error) {
  245. console.error(error);
  246. }
  247. }, [error]);
  248. // Cleanup on unmount
  249. useEffect(() => {
  250. return () => {
  251. handleDestroy();
  252. };
  253. }, [handleDestroy]);
  254. return (
  255. <>
  256. <apple-pay-button
  257. ref={buttonRef}
  258. buttonstyle="black"
  259. type="pay"
  260. locale="en"
  261. />
  262. { eligibilityError && <p className="text-ly-errorcolor text-ly-12 text-center">{eligibilityError.message}</p> }
  263. </>
  264. );
  265. };