PaypalApplepayButton.tsx 13 KB

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