PaymentMethodContinueTpPay.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. "use client";
  2. import clsx from "clsx";
  3. import { use,useState,useCallback } from "react";
  4. import { useForm, get, useWatch } from "react-hook-form";
  5. import { useRouter } from 'next/navigation'
  6. import { useCustomToast } from "@/utils/hooks/useToast";
  7. import AirwallexCardInput from "../PaymentMethodAdditional/AirwallexCardInput";
  8. import {
  9. airwallexManager,
  10. useAirwallexCardState
  11. } from "@/lib/Airwallex/useAirwallexCard";
  12. import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
  13. import type {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
  14. import type { OrderDetails } from "@/types/customer/type";
  15. import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
  16. import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
  17. import PaypalButton from "../PaymentButton/PaypalButton";
  18. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  19. import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
  20. import PaypalApplepayButton from "../PaymentButton/PaypalApplepayButton";
  21. import KlarnaButton from "../PaymentButton/KlarnaButton";
  22. import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
  23. import { useConfig } from '@utils/hooks/useConfig';
  24. import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
  25. /**@todo 优化目标:不使用 react-hook-form */
  26. export function PaymentMethodContinueTpPay({
  27. paymentMethodsDataPromise,
  28. defaultPaymentMethod,
  29. orderId,
  30. orderDetail
  31. }: {
  32. paymentMethodsDataPromise: Promise<FetchGraphqlResult<RepayOrderPaymentMethodsData>>;
  33. defaultPaymentMethod: {method: string; methodTitle: string;}
  34. orderId: number;
  35. orderDetail: OrderDetails;
  36. }) {
  37. const router = useRouter();
  38. const { showToast } = useCustomToast();
  39. const paymentMethodsData = use(paymentMethodsDataPromise);
  40. const paymentMethods = paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
  41. const { paymentRepay, createPaymentCallback } = usePlaceOrder();
  42. const {loadSdk} = usePaymentSDKContext();
  43. const {currencies, getCurrentCurrencyItem} = useConfig();
  44. const currentCurrency = getCurrentCurrencyItem();
  45. const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
  46. const orderCurrencyCode = orderCurrency?.code || currentCurrency.code;
  47. const airwallexCardState = useAirwallexCardState();
  48. const [isPay, setIsPay] = useState(false);
  49. const shippingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_shipping");
  50. const billingContact = getAddressFromOrderDetailAddressList(orderDetail.addresses,"order_billing");
  51. const {
  52. register,
  53. // watch,
  54. control,
  55. formState: { errors }
  56. } = useForm({
  57. mode: "onChange",
  58. reValidateMode: "onChange",
  59. defaultValues:{
  60. paymentMethod: defaultPaymentMethod.method
  61. }
  62. });
  63. // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
  64. const paymentMethodError = get(errors, "paymentMethod");
  65. // react 19中使用watch 会提示React Hook Form's `useForm()` API returns a `watch()` function which cannot be memoized safely.
  66. // const selectedPaymentMethod = watch("paymentMethod");
  67. const selectedPaymentMethod = useWatch({
  68. control,
  69. name:"paymentMethod",
  70. });
  71. const paypalRepayOrder = async () => {
  72. overlayLoading.start();
  73. const res = await paymentRepay({
  74. orderId: Number(orderId),
  75. paymentMethod: selectedPaymentMethod
  76. });
  77. overlayLoading.stop();
  78. if(!res.error && res.data !== null) {
  79. return {
  80. error: false,
  81. msg: '',
  82. orderId: res.data.gatewayOrderId,
  83. webOrderId: res.data.orderId
  84. };
  85. } else {
  86. confirmDialog({
  87. title: "Warning",
  88. content: res.msg + " Create order failed. Please try again.",
  89. noCancel: true,
  90. }).then(() => {});
  91. return {
  92. error: true,
  93. msg: '',
  94. orderId: '',
  95. webOrderId: ''
  96. };
  97. }
  98. };
  99. const airwallexRepay = async () => {
  100. setIsPay(true);
  101. overlayLoading.start();
  102. const res = await paymentRepay({
  103. orderId: Number(orderId),
  104. paymentMethod: selectedPaymentMethod,
  105. paymentSuccessUrl: window.location.origin + '/paymentresult/result'
  106. });
  107. if(!res.error && res.data !== null) {
  108. // 创建订单成功了,跳转支付页面
  109. window.location.href = res.data.gatewayOrderId;
  110. } else {
  111. //创建订单出错了
  112. overlayLoading.stop();
  113. confirmDialog({
  114. title: "Warning",
  115. content: res.msg + " Create order failed. Please try again.",
  116. noCancel: true,
  117. }).then(() => {});
  118. }
  119. setIsPay(false);
  120. };
  121. const validateAirwallexCard = () => {
  122. const res = airwallexManager.validateCard();
  123. if(res.msg) {
  124. showToast(res.msg, 'danger');
  125. }
  126. return res.valid;
  127. };
  128. const cardAirwallexRepay = async () => {
  129. setIsPay(true);
  130. overlayLoading.start();
  131. const validRes = validateAirwallexCard();
  132. if(validRes) {
  133. const res = await paymentRepay({
  134. orderId: Number(orderId),
  135. paymentMethod: selectedPaymentMethod,
  136. });
  137. if(!res.error && res.data !== null) {
  138. // 创建订单成功
  139. const cardNumberElement = airwallexManager.getCardNumberElement();
  140. const paymentIntentIdClientSecret = res.data.gatewayOrderId.split(':');
  141. const paymentIntentId = paymentIntentIdClientSecret[0];
  142. const paymentIntentClientSecret = paymentIntentIdClientSecret[1];
  143. cardNumberElement?.confirm({
  144. intent_id: paymentIntentId,
  145. client_secret: paymentIntentClientSecret,
  146. }).then((result) => {
  147. overlayLoading.stop();
  148. console.log('cardNumberElement.confirm result ---- ', result);
  149. // 支付成功 跳转页面
  150. router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=success');
  151. }).catch((error) => {
  152. overlayLoading.stop();
  153. console.log('cardNumberElement.confirm error ---- ', error);
  154. // 支付失败
  155. router.replace('/paymentresult/result?orderId=' + orderId + '&return_result=failure');
  156. });
  157. } else {
  158. //创建订单出错了
  159. overlayLoading.stop();
  160. confirmDialog({
  161. title: "Warning",
  162. content: res.msg + " Create order failed. Please try again.",
  163. noCancel: true,
  164. }).then(() => {});
  165. }
  166. }else {
  167. overlayLoading.stop();
  168. }
  169. setIsPay(false);
  170. };
  171. const afterpayPlaceOrder = async () => {
  172. setIsPay(true);
  173. overlayLoading.start();
  174. if(window.AfterPay) {
  175. const res = await paymentRepay({
  176. orderId: Number(orderId),
  177. paymentMethod: selectedPaymentMethod,
  178. paymentSuccessUrl: window.location.origin + '/paymentresult/result',
  179. paymentCancelUrl: window.location.origin + '/paymentresult/result'
  180. });
  181. if(!res.error && res.data !== null) {
  182. const afterpayToken = res.data.gatewayOrderId;
  183. const webOrderId = res.data.orderId;
  184. let countryCode = "US";
  185. if(orderCurrencyCode === "CAD") {
  186. countryCode = "CA";
  187. } else if(orderCurrencyCode === "AUD") {
  188. countryCode = "AU";
  189. } else if(orderCurrencyCode === "NZD") {
  190. countryCode = "NZ";
  191. } else if(orderCurrencyCode === "GBP") {
  192. countryCode = "GB";
  193. }
  194. window.AfterPay.initialize({countryCode: countryCode});
  195. // To avoid triggering browser anti-popup rules, the AfterPay.open()
  196. // function must be directly called inside the click event listener
  197. window.AfterPay.open();
  198. // If you don't already have a checkout token at this point, you can
  199. // AJAX to your backend to retrieve one here. The spinning animation
  200. // will continue until `AfterPay.transfer` is called.
  201. // If you fail to get a token you can call AfterPay.close()
  202. window.AfterPay.onComplete = async (event) => {
  203. console.log('window.AfterPay.onComplete event ---- ', event);
  204. const status: "success" | "cancel" | "failure" = event.data.status === "SUCCESS" ? 'success' : 'cancel';
  205. const resCallback = await createPaymentCallback({
  206. orderId: Number(webOrderId),
  207. gatewayOrderId: event.data.orderToken,
  208. status: status,
  209. });
  210. overlayLoading.stop();
  211. if (event.data.status == "SUCCESS") {
  212. // The customer confirmed the payment schedule.
  213. // The token is now ready to be captured from your server backend.
  214. // 跳转结果页 成功
  215. if(!resCallback.error) {
  216. // 后端capture成功,跳转到成功落地页
  217. router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
  218. } else {
  219. // capture 失败,提示错误
  220. confirmDialog({
  221. title: "Somthing Wrong",
  222. content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderId,
  223. noCancel: true,
  224. }).then(() => {
  225. router.replace('/');
  226. });
  227. }
  228. } else {
  229. // The customer cancelled the payment or closed the popup window.
  230. confirmDialog({
  231. title: "Payment Cancelled",
  232. content: "You canceled the payment.",
  233. noCancel: true,
  234. }).then(() => { });
  235. }
  236. }
  237. window.AfterPay.transfer({token: afterpayToken});
  238. } else {
  239. //创建订单出错了
  240. overlayLoading.stop();
  241. confirmDialog({
  242. title: "Warning",
  243. content: res.msg + " Create order failed. Please try again.",
  244. noCancel: true,
  245. }).then(() => {});
  246. }
  247. } else {
  248. overlayLoading.stop();
  249. showToast("Please wait for the Afterpay to load completely.","warning");
  250. }
  251. };
  252. const applepayPlaceOrder = async () => {
  253. const res = await paymentRepay({
  254. orderId: Number(orderId),
  255. paymentMethod: selectedPaymentMethod
  256. });
  257. if(!res.error && res.data !== null) {
  258. return {
  259. orderId: res.data.gatewayOrderId,
  260. webOrderId: res.data.orderId
  261. };
  262. } else {
  263. // 抛出错误让第三方支付能够捕获
  264. throw new Error("REPAY_ORDER_ERROR: " + res.msg);
  265. }
  266. };
  267. const validateCheckout = useCallback(async() => {
  268. return true;
  269. }, []);
  270. const klarnaPlaceOrder = async () => {
  271. overlayLoading.start();
  272. const resRepay = await paymentRepay({
  273. orderId:Number(orderId),
  274. paymentMethod: selectedPaymentMethod,
  275. paymentSuccessUrl: window.location.origin + '/paymentresult/result',
  276. paymentCancelUrl: window.location.origin + '/paymentresult/result'
  277. });
  278. if(!resRepay.error && resRepay.data !== null) {
  279. const klarnaToken = resRepay.data.gatewayOrderId;
  280. const webOrderId = resRepay.data.orderId;
  281. const billingAddress = billingContact;
  282. const shippingAddress = shippingContact;
  283. const klarnaClientToken = klarnaToken.split("|")[0];
  284. // const klarnaVerifyToken = klarnaToken.split("|")[1];
  285. window.Klarna.Payments.init({
  286. client_token: klarnaClientToken,
  287. });
  288. //klarna-widget-container
  289. window.Klarna.Payments.load({
  290. container: '#klarna-widget-container',
  291. }, (response) => {
  292. if(!response.show_form) { // klarna不可用
  293. // 跳转结果页
  294. router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=failure');
  295. } else {
  296. window.Klarna.Payments.authorize(
  297. {},
  298. {
  299. billing_address: {
  300. given_name: billingAddress.firstName,
  301. family_name: billingAddress.lastName,
  302. email: billingAddress.email,
  303. phone: billingAddress.phone,
  304. street_address: billingAddress.address,
  305. postal_code: billingAddress.postcode,
  306. city: billingAddress.city,
  307. country: billingAddress.country
  308. },
  309. shipping_address: {
  310. given_name: shippingAddress.firstName,
  311. family_name: shippingAddress.lastName,
  312. email: shippingAddress.email,
  313. street_address: shippingAddress.address,
  314. postal_code: shippingAddress.postcode,
  315. city: shippingAddress.city,
  316. phone: shippingAddress.phone,
  317. region: shippingAddress.state,
  318. country: shippingAddress.country
  319. },
  320. // customer: {
  321. // date_of_birth: "1941-03-21",
  322. // },
  323. },
  324. async function(res) {
  325. console.log('klarna authorize res ---- ',res);
  326. if(res.approved && res.authorization_token) { // 可以支付
  327. overlayLoading.stop();
  328. router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
  329. /*
  330. const resCallback = await createPaymentCallback({
  331. orderId: Number(webOrderId),
  332. gatewayOrderId: res.authorization_token,
  333. verifyToken: klarnaVerifyToken,
  334. status: "success",
  335. });
  336. // 跳转结果页
  337. overlayLoading.stop();
  338. if(!resCallback.error) {
  339. router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=success');
  340. } else {
  341. // callback 失败,提示错误
  342. confirmDialog({
  343. title: "Somthing Wrong",
  344. content: resCallback.msg + " Please contact customer service." + " OrderId: "+ webOrderId,
  345. noCancel: true,
  346. }).then(() => {
  347. router.replace('/');
  348. });
  349. }
  350. */
  351. } else {
  352. // if(res.show_form) { // klarna可用,但是有报错
  353. // } else {
  354. // // klarna 不可用 跳转结果页
  355. // }
  356. overlayLoading.stop();
  357. router.replace('/paymentresult/result?orderId=' + webOrderId + '&return_result=failure');
  358. }
  359. }
  360. );
  361. }
  362. });
  363. } else {
  364. //创建订单出错了
  365. overlayLoading.stop();
  366. confirmDialog({
  367. title: "Warning",
  368. content: resRepay.msg + " Create order failed. Please try again.",
  369. noCancel: true,
  370. }).then(() => {});
  371. }
  372. };
  373. const changePaymentMethod = async (method: string) => {
  374. if(method === 'klarna' ||
  375. method === 'afterpay' ||
  376. method === 'clearpay' ||
  377. method === 'applepay'
  378. ) {
  379. loadSdk(method);
  380. }
  381. };
  382. const genetePaymentTips = (payment: string) => {
  383. return getPaymentDescription({
  384. payment: payment,
  385. grandTotal: orderDetail.grandTotal,
  386. symble: orderCurrency?.symbol ?? currentCurrency.symbol
  387. })
  388. };
  389. const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
  390. return (
  391. <>
  392. <form name="shipping method" className="w-full box-border">
  393. <div className="mt-4 w-full">
  394. {paymentMethodsData.error && <p className="text-red-500 text-ly-12">{paymentMethodsData.error.message}</p>}
  395. <ul className="w-full">
  396. {
  397. paymentMethods.map((item) => {
  398. return (<li className="w-full mt-4" key={item.method}>
  399. <label htmlFor={"paymentmethod-" + item.method}
  400. className="group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
  401. >
  402. <p className="pr-2 flex-none relative">
  403. <img className="block h-8 w-auto"
  404. src={getPaymentMethodLogo(item.method)} alt={"Payment Method " + item.title}
  405. />
  406. </p>
  407. <p className="text-ly-14 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
  408. <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
  409. <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="1" width="14" height="14" rx="1" fill="#000000" ></rect><path stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5" strokeLinecap="square" d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
  410. <svg className="uncheck w-4 h-4 block group-has-[:checked]:hidden" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="1" width="14" height="14" rx="1" fill="#FFFFFF" ></rect><path fill="rgba(0, 0, 0, 1)" d="M2 0.5L14 0.5C14.8284 0.5 15.5 1.17157 15.5 2L15.5 14C15.5 14.8284 14.8284 15.5 14 15.5L2 15.5C1.17157 15.5 0.5 14.8284 0.5 14L0.5 2C0.5 1.17157 1.17157 0.5 2 0.5ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2L1.5 14C1.5 14.2761 1.72386 14.5 2 14.5L14 14.5C14.2761 14.5 14.5 14.2761 14.5 14L14.5 2C14.5 1.72386 14.2761 1.5 14 1.5L2 1.5Z"></path></svg>
  411. </div>
  412. <input id={"paymentmethod-" + item.method}
  413. className="absolute w-6 h-6 opacity-0"
  414. type="radio"
  415. value={item.method}
  416. {...register('paymentMethod', {
  417. required: 'Please select a shipping method.',
  418. onChange: (event) => {
  419. changePaymentMethod(event.target.value);
  420. },
  421. })}
  422. />
  423. </label>
  424. { item.method === "airwallex" &&
  425. <div className="w-full mt-3"
  426. style={{display: airwallexCardElementShow ? 'block' : 'none'}}
  427. >
  428. <AirwallexCardInput />
  429. </div>
  430. }
  431. </li>)
  432. })
  433. }
  434. </ul>
  435. {paymentMethodError && <p className="text-red-500 text-ly-12">{paymentMethodError.message}</p>}
  436. </div>
  437. </form>
  438. <div className="w-full box-border mt-4">
  439. <p className="text-ly-12 text-[#666666] leading-4.75">
  440. By providing your information, you agree to Asteria's Privacy Policy and Terms of Use.
  441. </p>
  442. <div className="mt-6 w-full">
  443. <div className={clsx("w-full",{
  444. "hidden": selectedPaymentMethod !== 'paypal_smart_button'
  445. })}>
  446. <PaypalButton
  447. createOrder={paypalRepayOrder}
  448. isRePay={true}
  449. />
  450. </div>
  451. {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
  452. <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
  453. onClick={airwallexRepay}
  454. disabled={isPay}
  455. >
  456. Pay
  457. </button>
  458. }
  459. {selectedPaymentMethod === 'airwallex' &&
  460. <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
  461. onClick={cardAirwallexRepay}
  462. disabled={isPay}
  463. >
  464. Pay
  465. </button>
  466. }
  467. {(selectedPaymentMethod === 'afterpay' || selectedPaymentMethod === 'clearpay') &&
  468. <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
  469. onClick={afterpayPlaceOrder}
  470. >
  471. Place Order
  472. </button>
  473. }
  474. <div className={clsx("w-full",{
  475. "hidden": selectedPaymentMethod !== 'applepay'
  476. })}>
  477. <PaypalApplepayButton
  478. grandTotal={orderDetail.grandTotal}
  479. billingContact={billingContact}
  480. shippingContact={shippingContact}
  481. validateCheckout={validateCheckout}
  482. createOrder={applepayPlaceOrder}
  483. isRePay={true}
  484. />
  485. </div>
  486. <div className={clsx("w-full",{
  487. "hidden": selectedPaymentMethod !== 'klarna'
  488. })}>
  489. <KlarnaButton
  490. clickHandler={klarnaPlaceOrder}
  491. />
  492. </div>
  493. </div>
  494. </div>
  495. </>
  496. );
  497. }