PaypalButton.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. "use client";
  2. import {useRef} from "react";
  3. import { redirect, RedirectType } from 'next/navigation';
  4. import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
  5. import {
  6. usePayPal,
  7. useEligibleMethods,
  8. INSTANCE_LOADING_STATE,
  9. usePayPalOneTimePaymentSession,
  10. usePayPalGuestPaymentSession,
  11. usePayLaterOneTimePaymentSession,
  12. type OnApproveDataOneTimePayments,
  13. type OnErrorData,
  14. type OnCompleteData,
  15. type OnCancelDataOneTimePayments,
  16. } from "@paypal/react-paypal-js/sdk-v6";
  17. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  18. import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
  19. export default function PaypalButton({
  20. createOrder,
  21. onPaymentClick,
  22. isRePay
  23. }:{
  24. createOrder: () => Promise<{orderId: string;webOrderId: string;error: boolean;msg: string;}>;
  25. onPaymentClick: () => Promise<boolean>;
  26. isRePay?: boolean;
  27. }) {
  28. const {createPaymentCallback} = usePlaceOrder();
  29. const gatewayOrderIdRef = useRef("");
  30. const webOrderIdRef = useRef("");
  31. // paypal sdk 是否加载完成
  32. const { loadingStatus } = usePayPal();
  33. // Fetch eligibility(资格) for one-time payment flow
  34. const {
  35. error: eligibilityError,
  36. eligiblePaymentMethods,
  37. isLoading: isEligibilityLoading,
  38. } = useEligibleMethods({
  39. payload: {
  40. currencyCode: "USD", // @todo 需要根据用户选择的货币走
  41. paymentFlow: "ONE_TIME_PAYMENT",
  42. },
  43. });
  44. const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
  45. const isPayLaterEligible =
  46. !isEligibilityLoading && eligiblePaymentMethods?.isEligible("paylater");
  47. // const isCreditEligible =
  48. // !isEligibilityLoading && eligiblePaymentMethods?.isEligible("credit");
  49. const handlerPaypalClick = async (buttonHandlerClick: () => Promise<void | {
  50. redirectURL?: string | undefined;
  51. }>) => {
  52. overlayLoading.start();
  53. const vaild = await onPaymentClick();
  54. console.log('789----', vaild);
  55. if(vaild) {
  56. const res = await createOrder();
  57. if(res.error) {
  58. overlayLoading.stop();
  59. confirmDialog({
  60. title: "Warning",
  61. content: res.msg + " Create order failed. Please try again.",
  62. noCancel: true,
  63. }).then(() => {});
  64. } else {
  65. gatewayOrderIdRef.current = res.orderId;
  66. webOrderIdRef.current = res.webOrderId;
  67. return buttonHandlerClick();
  68. }
  69. } else {
  70. overlayLoading.stop();
  71. }
  72. }
  73. // paypal支付回调配置
  74. const paypalOnHandlerConfig = {
  75. createOrder: async () => {
  76. // const res = await createOrder();
  77. // gatewayOrderIdRef.current = res.orderId;
  78. // webOrderIdRef.current = res.webOrderId;
  79. const res = {
  80. orderId: gatewayOrderIdRef.current,
  81. webOrderId: webOrderIdRef.current
  82. };
  83. return res;
  84. },
  85. onApprove: async (data: OnApproveDataOneTimePayments) => {
  86. console.log("Payment approved:", data);
  87. const resCallback = await createPaymentCallback({
  88. orderId: Number(webOrderIdRef.current),
  89. gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
  90. status: 'success',
  91. });
  92. if(!resCallback.error) {
  93. // 支付成功,跳转到成功落地页
  94. confirmDialog({
  95. title: "Payment Success",
  96. content: "Your payment was successful. Will redirect to success page.",
  97. noCancel: true,
  98. }).then(() => {
  99. redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=success', RedirectType.replace);
  100. });
  101. } else {
  102. // callback 失败,提示错误
  103. confirmDialog({
  104. title: "Somthing Wrong",
  105. content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderIdRef.current,
  106. noCancel: true,
  107. }).then(() => {
  108. redirect('/', RedirectType.replace);
  109. });
  110. }
  111. },
  112. onCancel: async (data: OnCancelDataOneTimePayments) => {
  113. /**
  114. * {oederId: paypalOrderId}
  115. */
  116. console.log("Payment cancelled:", data);
  117. await createPaymentCallback({
  118. orderId: Number(webOrderIdRef.current),
  119. gatewayOrderId: data.orderId || gatewayOrderIdRef.current,
  120. status: 'cancel',
  121. });
  122. if(!isRePay) {
  123. overlayLoading.stop();
  124. redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=cancel', RedirectType.replace);
  125. /*
  126. if(isGuest) {
  127. await cancelOrder({
  128. orderId: Number(webOrderIdRef.current),
  129. });
  130. // 重新获取购物车
  131. dispatch(fetchCartDetail());
  132. confirmDialog({
  133. title: "Order Cancelled",
  134. content: "Your order has been cancelled. Will redirect to home page.",
  135. noCancel: true,
  136. }).then(() => {
  137. redirect('/' , RedirectType.replace);
  138. });
  139. } else {
  140. // 登录用户 跳转二次支付页面 提醒用户可以二次支付
  141. confirmDialog({
  142. title: "Payment cancelled",
  143. content: "Will redirect to unpaied page.",
  144. noCancel: true,
  145. }).then(() => {
  146. redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
  147. });
  148. }
  149. */
  150. } else {
  151. // 登录用户在二次支付页面
  152. confirmDialog({
  153. title: "Payment cancelled",
  154. content: "You canceled the payment.",
  155. noCancel: true,
  156. }).then(() => { });
  157. }
  158. },
  159. onError: async (data: OnErrorData) => {
  160. console.error("Payment error:", data);
  161. /**
  162. * code: string;
  163. name: string;
  164. isRecoverable: boolean;
  165. */
  166. await createPaymentCallback({
  167. orderId: Number(webOrderIdRef.current),
  168. gatewayOrderId: gatewayOrderIdRef.current,
  169. status: 'failure',
  170. });
  171. if(!isRePay) {
  172. overlayLoading.stop();
  173. redirect('/paymentresult/result?orderId=' + webOrderIdRef.current + '&return_result=failure', RedirectType.replace);
  174. /*
  175. if(isGuest) {
  176. await cancelOrder({
  177. orderId: Number(webOrderIdRef.current),
  178. });
  179. // 重新获取购物车
  180. dispatch(fetchCartDetail());
  181. confirmDialog({
  182. title: "Payment Failed",
  183. content: data.message + " Will redirect to home page.",
  184. noCancel: true,
  185. }).then(() => {
  186. redirect('/', RedirectType.replace);
  187. });
  188. } else {
  189. // 弹出错误提示弹窗:显示错误信息
  190. // 如果是登录用户,跳转二次支付页面 提醒用户可以二次支付
  191. confirmDialog({
  192. title: "Payment Failed",
  193. content: data.message + " Will redirect to unpaied page.",
  194. noCancel: true,
  195. }).then(() => {
  196. redirect('/checkout/continuetopay?orderid=' + webOrderIdRef.current, RedirectType.replace);
  197. });
  198. }
  199. */
  200. } else {
  201. // 登录用户 二次支付页面
  202. confirmDialog({
  203. title: "Payment Failed",
  204. content: data.message,
  205. noCancel: true,
  206. }).then(() => { });
  207. }
  208. },
  209. onComplete: (data: OnCompleteData) => {
  210. overlayLoading.stop();
  211. console.log("Payment session completed");
  212. console.log("On Complete data:", data);
  213. },
  214. // onShippingAddressChange: (data) => {},
  215. // onShippingOptionsChange: (data) => {
  216. // }
  217. };
  218. const {
  219. // isPending: paypalButtonPending,
  220. error: paypalButtonError,
  221. handleClick: paypalButtonHandleClick,
  222. } = usePayPalOneTimePaymentSession({
  223. presentationMode: "auto",
  224. ...paypalOnHandlerConfig
  225. });
  226. const {
  227. buttonRef: guestButtonRef,
  228. // isPending: guestButtonPending,
  229. error: guestButtonError,
  230. handleClick: guestButtonHandleClick,
  231. } = usePayPalGuestPaymentSession(paypalOnHandlerConfig);
  232. const payLaterDetails = eligiblePaymentMethods?.getDetails("paylater");
  233. const {
  234. // isPending: paylaterButtonPending,
  235. // error: paylaterButtonError,
  236. handleClick: paylaterButtonHandleClick,
  237. } = usePayLaterOneTimePaymentSession({
  238. presentationMode: "auto",
  239. ...paypalOnHandlerConfig
  240. });
  241. return (
  242. <div className="w-full">
  243. {
  244. isLoading ? (
  245. <div className="test-ly-12 font-medium text-center">
  246. Loading paypal payment methods...
  247. </div>
  248. ) : eligibilityError ? (
  249. <div className="test-ly-12 font-medium text-center text-ly-errorcolor">
  250. Failed to load paypal payment options. Please refresh the page.
  251. </div>
  252. ) : (
  253. <>
  254. {paypalButtonError ?
  255. <p className="text-ly-errorcolor text-ly-12">Error: {paypalButtonError.message}</p>
  256. :
  257. <paypal-button onClick={() => {
  258. return handlerPaypalClick(paypalButtonHandleClick);
  259. }} type="pay" className="w-full" />
  260. }
  261. {guestButtonError ?
  262. <p className="text-ly-errorcolor text-ly-12">Error: {guestButtonError.message}</p>
  263. :
  264. <paypal-basic-card-container className="w-full mt-3">
  265. <paypal-basic-card-button ref={guestButtonRef} onClick={() => handlerPaypalClick(guestButtonHandleClick)} />
  266. </paypal-basic-card-container>
  267. }
  268. {isPayLaterEligible &&
  269. <paypal-pay-later-button className="w-full mt-3"
  270. onClick={() => handlerPaypalClick(paylaterButtonHandleClick)}
  271. countryCode={payLaterDetails?.countryCode}
  272. productCode={payLaterDetails?.productCode}
  273. />
  274. }
  275. </>
  276. )
  277. }
  278. </div>
  279. );
  280. }