PaymentMethodContinueTpPay.tsx 26 KB

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