PaymentMethodCheckout.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. "use client";
  2. import { type Ref, useEffect, useImperativeHandle } from "react";
  3. import Link from "next/link";
  4. import clsx from "clsx";
  5. import { useForm, get } from "react-hook-form";
  6. import { usePayPalMessages } from "@paypal/react-paypal-js/sdk-v6";
  7. import {useStore} from "@/utils/hooks/useConfig";
  8. import {currencyCountryMap} from "@/utils/constants";
  9. import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
  10. import type { CheckoutPaymentMethod } from "@/types/checkout/type";
  11. import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
  12. import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
  13. /**
  14. * 关于如何确定默认选中哪个支付方式:
  15. * 从购物车详情中获取支付方式 A,支付方式列表中有A,那么把A标记为默认选中;如果支付方式列表中没有A,把paypal标记为默认选中;
  16. */
  17. export interface RefPaymentMethodsHandle {
  18. getSelectPaymentMethod: () => string;
  19. getSelectPaymentTitle: () => string;
  20. validatePaymentMethod: () => Promise<boolean>;
  21. }
  22. export function PaymentMethodCheckout({
  23. ref,
  24. paymentMethods,
  25. selectedPaymentMethod,
  26. grandTotal,
  27. onPaymentMethodChange,
  28. }: {
  29. ref: Ref<RefPaymentMethodsHandle>;
  30. paymentMethods: CheckoutPaymentMethod[];
  31. selectedPaymentMethod: string;
  32. grandTotal: number;
  33. onPaymentMethodChange: (value: string) => Promise<boolean>;
  34. }) {
  35. const { error:paypalMessageError } = usePayPalMessages({});
  36. const {store} = useStore();
  37. const currencySymbol = currencyCountryMap[store.currentCurrency].symbol;
  38. const airwallexCardState = useAirwallexCardState();
  39. const {
  40. register,
  41. getValues,
  42. trigger,
  43. reset,
  44. formState: { errors }
  45. } = useForm({
  46. mode: "onChange",
  47. reValidateMode: "onChange",
  48. // defaultValues: {
  49. // paymentMethod: defaultValue,
  50. // }
  51. });
  52. // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
  53. const paymentMethodError = get(errors, "paymentMethod");
  54. const paymentMethodOnChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
  55. onPaymentMethodChange(event.target.value);
  56. };
  57. useEffect(() => {
  58. let defaultValue = '';
  59. const findItem = paymentMethods.find((item) => item.method === selectedPaymentMethod);
  60. if(findItem !== undefined) {
  61. defaultValue = selectedPaymentMethod;
  62. } else {
  63. paymentMethods.forEach((item) => {
  64. if(item.method === 'paypal_smart_button') {
  65. defaultValue = item.method;
  66. }
  67. });
  68. }
  69. reset({ paymentMethod: defaultValue });
  70. },[paymentMethods,selectedPaymentMethod]);
  71. useImperativeHandle(ref, () => {
  72. return {
  73. // 获取用户选择的支付方式
  74. getSelectPaymentMethod: () => {
  75. return getValues('paymentMethod');
  76. },
  77. getSelectPaymentTitle: () => {
  78. const nowValue = getValues('paymentMethod');
  79. const findItem = paymentMethods.find((item) => item.method === nowValue);
  80. return findItem?.title ?? '';
  81. },
  82. // 触发校验
  83. validatePaymentMethod: () => {
  84. return trigger();
  85. }
  86. }
  87. },[paymentMethods]);
  88. const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
  89. const genetePaymentTips = (payment: string) => {
  90. return getPaymentDescription({
  91. payment: payment,
  92. grandTotal: grandTotal,
  93. symble: currencySymbol
  94. })
  95. };
  96. return (<>
  97. <form name="shipping method" className="w-full">
  98. <div className="mt-4 w-full">
  99. <ul className="w-full">
  100. {
  101. paymentMethods.map((item) => {
  102. return (
  103. <li key={item.method}
  104. className={clsx("w-full mt-3 first:mt-0",
  105. selectedPaymentMethod === 'airwallex' && item.method === 'airwallex' && ['bg-[#ECF7F4] border-1 border-ly-green rounded-xs pb-3.5']
  106. )}
  107. >
  108. <label htmlFor={"paymentmethod-" + item.method}
  109. className={clsx(
  110. "group box-border flex items-center relative px-4 py-3.5 bg-ly-lightgray",
  111. selectedPaymentMethod !== 'airwallex' && "has-checked:bg-[#ECF7F4] has-checked:border-1 has-checked:border-ly-green has-checked:rounded-xs",
  112. selectedPaymentMethod === 'airwallex' && "has-checked:bg-[#ECF7F4]"
  113. )}
  114. >
  115. <p className="mr-2 flex-none relative">
  116. <img
  117. className="block h-6 w-auto"
  118. src={getPaymentMethodLogo(item.method)}
  119. alt={`Payment Method ${item.title}`}
  120. />
  121. </p>
  122. <p className="text-ly-14 text-[#666666]">
  123. {genetePaymentTips(item.method)}
  124. </p>
  125. <div className="absolute top-1/2 -translate-y-1/2 right-4 flex items-center">
  126. <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>
  127. <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>
  128. </div>
  129. <input id={"paymentmethod-" + item.method}
  130. className="absolute w-6 h-6 opacity-0"
  131. type="radio"
  132. value={item.method}
  133. {...register('paymentMethod', {
  134. required: 'Please select a payment method.',
  135. onChange: (event) => {
  136. paymentMethodOnChange(event);
  137. },
  138. })}
  139. />
  140. </label>
  141. { item.method === "airwallex" &&
  142. <div className="w-full px-4"
  143. style={{display: airwallexOtherInfoShow ? 'block' : 'none'}}
  144. >
  145. <AirwallexCardInput />
  146. </div>
  147. }
  148. </li>
  149. )
  150. })
  151. }
  152. </ul>
  153. {paymentMethodError && <p className="text-red-500 text-ly-12">{paymentMethodError.message}</p>}
  154. </div>
  155. <p className="text-ly-14 mt-3 leading-ly-20">
  156. By placing this order, I agree to the <Link href="/return-policy">Return Policy</Link> and the <Link href="/terms-conditions">Terms & Conditions</Link>.
  157. </p>
  158. {!paypalMessageError &&
  159. <div className="mt-4">
  160. <paypal-message className="checkout-paypal-message"
  161. auto-bootstrap={true}
  162. amount={String(grandTotal)}
  163. currency-code={store.currentCurrency}
  164. // buyer-country="US" //这个属性是沙盒环境测试用的,正式环境不要设置这个参数
  165. />
  166. </div>
  167. }
  168. </form>
  169. </>);
  170. }