CheckoutWrapper.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. "use client";
  2. import {useState, useRef} from "react";
  3. import { LoadingSpinner } from "@components/common/LoadingSpinner";
  4. import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
  5. import { useForm, FormProvider } from "react-hook-form";
  6. import {
  7. Country,
  8. ShipAddressFormData,
  9. FullAddressFormData,
  10. CreateCheckoutAddressVariables,
  11. } from "@/types/checkout/type";
  12. import AddressResultDisplay from "./AddressResultDisplay";
  13. import ShippingAddressCheckout from "./ShippingAddressCheckout";
  14. import BillingAddressCheckout from "./BillingAddressCheckout";
  15. import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
  16. import { useCustomToast } from "@/utils/hooks/useToast";
  17. import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
  18. import {useCheckoutPaymentMethod} from "@/utils/hooks/useCheckoutPaymentMethod";
  19. import {useCheckoutShippingMethod} from "@/utils/hooks/useCheckoutShippingMethod";
  20. import { useAppDispatch } from "@/store/hooks";
  21. import { updateCart } from "@/store/slices/cart-slice";
  22. import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
  23. import LoadingPaymentMethod from "./LoadingPaymentMethod";
  24. import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
  25. import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
  26. import CommonModal from "@/components/theme/ui/CommonModal";
  27. import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
  28. import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
  29. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  30. import { CartDetail,CartAddress } from "@/types/cart/type";
  31. import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
  32. /***
  33. * 不用useForShipping字段了
  34. * 以shipping address 为准,根据shippingaddress 设置billing address
  35. * 保存完地址之后要重新获取运输方式和支付方式
  36. * 变更运输方式后重新获取支付方式
  37. * @todo 购物车为空 跳转到空购物车页面
  38. */
  39. /**
  40. * 地址表单默认值
  41. */
  42. function getBillingAddressFormData(billingAddress:CartAddress) {
  43. const billingCountry = billingAddress.country || 'US';
  44. const billingPhoneNumber = normalizePhoneForForm(billingAddress.phone, billingCountry) || "";
  45. const res = {
  46. "billingAddressId": billingAddress.id,
  47. "billingEmail": billingAddress.email,
  48. "billingFirstName": billingAddress.firstName,
  49. "billingLastName": billingAddress.lastName,
  50. "billingCompanyName": "",
  51. "billingAddress": billingAddress.address,
  52. "billingCountry": billingCountry,
  53. "billingState": billingAddress.state,
  54. "billingCity": billingAddress.city,
  55. "billingPostcode": billingAddress.postcode,
  56. "billingPhoneNumber": billingPhoneNumber,
  57. };
  58. return res;
  59. }
  60. function getShippingAddressFormData(shippingAddress:CartAddress) {
  61. const shippingCountry = shippingAddress.country || 'US';
  62. const shippingPhoneNumber = normalizePhoneForForm(shippingAddress.phone, shippingCountry) || "";
  63. const res = {
  64. "shippingAddressId": shippingAddress.id,
  65. "shippingEmail": shippingAddress.email,
  66. "shippingFirstName": shippingAddress.firstName,
  67. "shippingLastName": shippingAddress.lastName,
  68. "shippingCompanyName": "",
  69. "shippingAddress": shippingAddress.address,
  70. "shippingCountry": shippingCountry,
  71. "shippingState": shippingAddress.state,
  72. "shippingCity": shippingAddress.city,
  73. "shippingPostcode": shippingAddress.postcode,
  74. "shippingPhoneNumber": shippingPhoneNumber,
  75. };
  76. return res;
  77. }
  78. function getAddressFormDataFromCart(cartDetail:CartDetail, loginEmail?: string) {
  79. const shippingAddress = cartDetail.shippingAddress;
  80. const billingAddress = cartDetail.billingAddress;
  81. const defaultValues = {
  82. "shippingAddressId": "",
  83. "shippingEmail": loginEmail ?? '',
  84. "shippingFirstName": "",
  85. "shippingLastName": "",
  86. "shippingCompanyName": "",
  87. "shippingAddress": "",
  88. "shippingCountry": "US",
  89. "shippingState": "",
  90. "shippingCity": "",
  91. "shippingPostcode": "",
  92. "shippingPhoneNumber": "",
  93. "billingAddressId": "",
  94. "billingEmail": loginEmail ?? '',
  95. "billingFirstName": "",
  96. "billingLastName": "",
  97. "billingCompanyName": "",
  98. "billingAddress": "",
  99. "billingCountry": "US",
  100. "billingState": "",
  101. "billingCity": "",
  102. "billingPostcode": "",
  103. "billingPhoneNumber": "",
  104. "billingSameAsShipping": true
  105. }
  106. if(billingAddress) {
  107. const bs = getBillingAddressFormData(billingAddress);
  108. defaultValues.billingAddressId = bs.billingAddressId;
  109. defaultValues.billingEmail = bs.billingEmail;
  110. defaultValues.billingFirstName = bs.billingFirstName;
  111. defaultValues.billingLastName = bs.billingLastName;
  112. defaultValues.billingCompanyName = bs.billingCompanyName;
  113. defaultValues.billingAddress = bs.billingAddress;
  114. defaultValues.billingCountry = bs.billingCountry;
  115. defaultValues.billingState = bs.billingState;
  116. defaultValues.billingCity = bs.billingCity;
  117. defaultValues.billingPostcode = bs.billingPostcode;
  118. defaultValues.billingPhoneNumber = bs.billingPhoneNumber;
  119. // defaultValues.billingSameAsShipping = true;
  120. }
  121. if(shippingAddress) {
  122. const ss = getShippingAddressFormData(shippingAddress);
  123. defaultValues.shippingAddressId = ss.shippingAddressId;
  124. defaultValues.shippingEmail = ss.shippingEmail;
  125. defaultValues.shippingFirstName = ss.shippingFirstName;
  126. defaultValues.shippingLastName = ss.shippingLastName;
  127. defaultValues.shippingCompanyName = ss.shippingCompanyName;
  128. defaultValues.shippingAddress = ss.shippingAddress;
  129. defaultValues.shippingCountry = ss.shippingCountry;
  130. defaultValues.shippingState = ss.shippingState;
  131. defaultValues.shippingCity = ss.shippingCity;
  132. defaultValues.shippingPostcode = ss.shippingPostcode;
  133. defaultValues.shippingPhoneNumber = ss.shippingPhoneNumber;
  134. }
  135. if( defaultValues.billingEmail === defaultValues.shippingEmail &&
  136. defaultValues.billingFirstName === defaultValues.shippingFirstName &&
  137. defaultValues.billingLastName === defaultValues.shippingLastName &&
  138. defaultValues.billingCompanyName === defaultValues.shippingCompanyName &&
  139. defaultValues.billingAddress === defaultValues.shippingAddress &&
  140. defaultValues.billingCountry === defaultValues.shippingCountry &&
  141. defaultValues.billingState === defaultValues.shippingState &&
  142. defaultValues.billingCity === defaultValues.shippingCity &&
  143. defaultValues.billingPostcode === defaultValues.shippingPostcode &&
  144. defaultValues.billingPhoneNumber === defaultValues.shippingPhoneNumber
  145. ) {
  146. defaultValues.billingSameAsShipping = true;
  147. }
  148. return defaultValues;
  149. }
  150. /**
  151. * 生成createCheckoutAddress接口的参数
  152. */
  153. function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateCheckoutAddressVariables {
  154. const formDataShippingAddress:ShipAddressFormData = {
  155. shippingAddressId: formData.shippingAddressId,
  156. shippingEmail: formData.shippingEmail,
  157. shippingFirstName: formData.shippingFirstName,
  158. shippingLastName: formData.shippingLastName,
  159. shippingCompanyName: formData.shippingCompanyName,
  160. shippingAddress: formData.shippingAddress,
  161. shippingCountry: formData.shippingCountry,
  162. shippingState: formData.shippingState,
  163. shippingCity: formData.shippingCity,
  164. shippingPostcode: formData.shippingPostcode,
  165. shippingPhoneNumber: formData.shippingPhoneNumber,
  166. };
  167. let formDataBillingAddress = {
  168. billingAddressId: formData.billingAddressId,
  169. billingEmail: formData.billingEmail,
  170. billingFirstName: formData.billingFirstName,
  171. billingLastName: formData.billingLastName,
  172. billingCompanyName : formData.billingCompanyName,
  173. billingAddress: formData.billingAddress,
  174. billingCountry: formData.billingCountry,
  175. billingState: formData.billingState,
  176. billingCity: formData.billingCity,
  177. billingPostcode: formData.billingPostcode,
  178. billingPhoneNumber: formData.billingPhoneNumber,
  179. useForShipping: false,
  180. };
  181. if(formData.billingSameAsShipping) {
  182. formDataBillingAddress = {
  183. billingAddressId: formData.billingAddressId,
  184. billingEmail: formDataShippingAddress.shippingEmail,
  185. billingFirstName: formDataShippingAddress.shippingFirstName,
  186. billingLastName: formDataShippingAddress.shippingLastName,
  187. billingCompanyName : formDataShippingAddress.shippingCompanyName,
  188. billingAddress: formDataShippingAddress.shippingAddress,
  189. billingCountry: formDataShippingAddress.shippingCountry,
  190. billingState: formDataShippingAddress.shippingState,
  191. billingCity: formDataShippingAddress.shippingCity,
  192. billingPostcode: formDataShippingAddress.shippingPostcode,
  193. billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
  194. useForShipping: true,
  195. }
  196. }
  197. return {
  198. ...formDataShippingAddress,
  199. ...formDataBillingAddress
  200. };
  201. }
  202. export default function CheckoutWrapper({
  203. loginEmail,
  204. countries,
  205. cartDetailData,
  206. }: {
  207. loginEmail: string;
  208. countries: Country[];
  209. cartDetailData: CartDetail;
  210. }) {
  211. const addressFormDefaultValues = getAddressFormDataFromCart(cartDetailData,loginEmail);
  212. const dispatch = useAppDispatch();
  213. const [cartData, setCartData] = useState<CartDetail>(cartDetailData);
  214. const {saveCheckoutCart} = useSaveCheckoutCart();
  215. const {
  216. data: shippingMethodDatas,
  217. loading: shippingMethodLoading,
  218. error: shippingMethodError,
  219. getShippingMethod,
  220. } = useCheckoutShippingMethod();
  221. const selectedPaymentMethod = cartData.paymentMethod || 'paypal_smart_button';
  222. const selectedShippingRate = cartData.selectedShippingRate;
  223. const {
  224. data: paymentMethodDatas,
  225. loading: paymentMethodLoading,
  226. error: paymentMethodError,
  227. getPaymentMethod,
  228. } = useCheckoutPaymentMethod();
  229. const { showToast } = useCustomToast();
  230. const { getCheckoutAddress, saveCheckoutAddress } = useCheckoutAddress(loginEmail);
  231. const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
  232. const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
  233. // useForm() 只会在组件初始化时读取一次 defaultValues
  234. const addressForm = useForm<FullAddressFormData>({
  235. mode: "onBlur", // 或 "onChange"
  236. reValidateMode: "onChange",
  237. defaultValues: addressFormDefaultValues
  238. });
  239. const [addressModalOpen, setAddressModalOpen] = useState(false);
  240. const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
  241. const openAddressModal = () => {
  242. setAddressModalOpen(true);
  243. if(cartData.billingAddress && cartData.shippingAddress) {
  244. addressForm.reset({
  245. ...getAddressFormDataFromCart(cartData)
  246. });
  247. }
  248. };
  249. const closeAddressModal = (e:boolean) => {
  250. setAddressModalOpen(e);
  251. // 重置表单
  252. addressForm.reset();
  253. };
  254. const addressFormOnSubmit = async (formData: FullAddressFormData) => {
  255. // console.log('addressFormOnSubmit ---- ',formData); return;
  256. if(addressSaving) {
  257. return;
  258. }
  259. const saveAddressParam = generateSaveCheckoutAddressParam(formData);
  260. setAddressSaving(true);
  261. try {
  262. const res = await saveCheckoutAddress(saveAddressParam);
  263. console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
  264. // 地址保存成功后重新获取运输方式和支付方式
  265. await getShippingMethod();
  266. await getPaymentMethod();
  267. // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
  268. const queryAddressRes = await getCheckoutAddress();
  269. if(queryAddressRes !== null) {
  270. addressForm.resetField('shippingAddressId',{
  271. defaultValue: queryAddressRes.shippingAddress.shippingAddressId
  272. });
  273. addressForm.resetField('billingAddressId',{
  274. defaultValue: queryAddressRes.billingAddress.billingAddressId
  275. });
  276. // 同步地址到购物车详情
  277. const newBillingAddress = billingAddressToCartAddress(queryAddressRes.billingAddress);
  278. const newShippingAddress = shippingAddressToCartAddress(queryAddressRes.shippingAddress);
  279. dispatch(updateCart({
  280. billingAddress: newBillingAddress,
  281. shippingAddress: newShippingAddress
  282. }));
  283. setCartData(Object.assign({...cartData},{
  284. billingAddress: {...newBillingAddress},
  285. shippingAddress: {...newShippingAddress}
  286. }));
  287. }
  288. setAddressModalOpen(false);
  289. } catch(err) {
  290. // 错误处理
  291. console.error("save address error", err);
  292. showToast('Save address failed. Please try again.', 'danger');
  293. } finally {
  294. setAddressSaving(false);
  295. }
  296. };
  297. const handlePaymentMethodChange = async (method: string) => {
  298. let shippingMethod = '-1';
  299. if(methodShiippingRef.current) {
  300. shippingMethod = methodShiippingRef.current.getSelectShipMethod();
  301. }
  302. overlayLoading.start();
  303. const saveRes = await saveCheckoutCart({
  304. shippingMethod: shippingMethod,
  305. paymentMethod: method,
  306. couponCode: '-1'
  307. });
  308. overlayLoading.stop();
  309. if(!saveRes.error) {
  310. if(saveRes.data) {
  311. const newCartDetail = formatCartDetail(saveRes.data);
  312. dispatch(updateCart(newCartDetail));
  313. setCartData({...newCartDetail});
  314. }
  315. } else {
  316. showToast(saveRes.msg, 'danger');
  317. }
  318. };
  319. const handleShippingMethodChange = async (method: string) => {
  320. overlayLoading.start();
  321. const saveRes = await saveCheckoutCart({
  322. shippingMethod: method,
  323. paymentMethod: '-1',
  324. couponCode: '-1'
  325. });
  326. overlayLoading.stop();
  327. if(!saveRes.error) {
  328. if(saveRes.data) {
  329. const newCartDetail = formatCartDetail(saveRes.data);
  330. dispatch(updateCart(newCartDetail));
  331. setCartData({...newCartDetail});
  332. }
  333. // 保存成功之后需要重新获取支付方式
  334. getPaymentMethod();
  335. } else {
  336. showToast(saveRes.msg, 'danger');
  337. }
  338. };
  339. // 下单
  340. // 1. 校验地址 运输方式,支付方式;
  341. // 2. 根据购物车详情判断是否需要传下单参数: 地址,运输方式,支付方式
  342. const handlePlaceOrder = async () => {
  343. // 校验地址
  344. const addressValid = await addressForm.trigger();
  345. if(!addressValid) {
  346. showToast('Please fill in the address information correctly.', 'danger');
  347. return false;
  348. }
  349. if(methodShiippingRef.current) {
  350. const shippingMethodValid = await methodShiippingRef.current.validateShipMethod();
  351. if(!shippingMethodValid) {
  352. showToast('Please select a shipping method.', 'danger');
  353. return false;
  354. }
  355. } else {
  356. showToast('Please wait for shipping method loading.', 'danger');
  357. return false;
  358. }
  359. if(methodPaymentRef.current) {
  360. const paymentMethodValid = await methodPaymentRef.current.validatePaymentMethod();
  361. if(!paymentMethodValid) {
  362. showToast('Please select a payment method.', 'danger');
  363. return false;
  364. }
  365. } else {
  366. showToast('Please wait for payment method loading.', 'danger');
  367. return false;
  368. }
  369. if(!cartData) {
  370. showToast('Please add product to your cart.', 'danger');
  371. return false;
  372. }
  373. //如果购物车详情没有保存过支付方式,则需要保存支付方式
  374. if(cartData.paymentMethod === null) {
  375. const pmethod = methodPaymentRef?.current.getSelectPaymentMethod();
  376. const saveRes = await saveCheckoutCart({
  377. shippingMethod: '-1',
  378. paymentMethod: pmethod,
  379. couponCode: '-1'
  380. });
  381. if(!saveRes.error) {
  382. if(saveRes.data) {
  383. const newCartDetail = formatCartDetail(saveRes.data);
  384. dispatch(updateCart(newCartDetail));
  385. setCartData({...newCartDetail});
  386. }
  387. }
  388. }
  389. return true;
  390. };
  391. // function tt() {
  392. // const arr = [
  393. // ['US','3803800217'],
  394. // ['CH','+4186043315'],
  395. // ['US','334-208-6177'],
  396. // ['AU','+61 404103617'],
  397. // ['US','1-9166705105'],
  398. // ['CA','819 384 3221'],
  399. // ['US','(409) 239-9482'],
  400. // ['US','1-(651) 421-2762'],
  401. // ['CH', '1-792930242'],
  402. // ['US', '+1 4570438892'],
  403. // ['US', '1-+1 (404) 276-1068'],
  404. // ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
  405. // ];
  406. // arr.forEach((item) => {
  407. // normalizePhoneForForm(item[1], item[0]);
  408. // });
  409. // }
  410. return (
  411. <section className="w-full">
  412. <div>
  413. {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
  414. {/* {loadingAddress ?
  415. <AddressResultDisplayLoading />
  416. :
  417. <AddressResultDisplay shippingAddress={cartData.shippingAddress || null}
  418. onAddressModalOpenClick={openAddressModal}
  419. />
  420. } */}
  421. <AddressResultDisplay shippingAddress={cartData.shippingAddress || null}
  422. onAddressModalOpenClick={openAddressModal}
  423. />
  424. <CommonModal
  425. isOpen={addressModalOpen}
  426. onClose={closeAddressModal}
  427. header={
  428. <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
  429. <span>Shipping Address</span>
  430. <button className="w-6 h-6" onClick={() => closeAddressModal(false)}>
  431. <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
  432. </button>
  433. </div>
  434. }
  435. body={
  436. <div className="box-border w-full h-full p-3.75">
  437. <p className="text-ly-12 leading-ly-20">
  438. Select a billing address from your address book or enter a new address.
  439. </p>
  440. <div>
  441. <FormProvider {...addressForm}>
  442. <form>
  443. <ShippingAddressCheckout countries={countries} />
  444. <BillingAddressCheckout countries={countries} />
  445. </form>
  446. </FormProvider>
  447. </div>
  448. </div>
  449. }
  450. footer={
  451. <div className="box-border w-full p-3.75">
  452. <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
  453. onClick={addressForm.handleSubmit(addressFormOnSubmit)}
  454. >
  455. {addressSaving ? <LoadingSpinner /> : 'Save'}
  456. </button>
  457. </div>
  458. }
  459. />
  460. </div>
  461. <div className="mt-6 box-border px-4">
  462. <h3 className="text-ly-24 font-medium">Shipping Method</h3>
  463. {shippingMethodLoading ?
  464. <CheckoutShippingMethodLoading />
  465. :
  466. <ShippingMethodCheckout
  467. ref={methodShiippingRef}
  468. shipMethods={shippingMethodDatas}
  469. errorMsg={shippingMethodError}
  470. selectedShippingRate={selectedShippingRate}
  471. onShipMethodChange={handleShippingMethodChange}
  472. />
  473. }
  474. </div>
  475. <div className="mt-6 box-border px-4">
  476. <h3 className="text-ly-24 font-medium">Payment Method</h3>
  477. { paymentMethodLoading ?
  478. <LoadingPaymentMethod />
  479. :
  480. <PaymentMethodCheckout
  481. ref={methodPaymentRef}
  482. errorMsg={paymentMethodError}
  483. paymentMethods={paymentMethodDatas}
  484. selectedPaymentMethod={selectedPaymentMethod}
  485. onPaymentMethodChange={handlePaymentMethodChange}
  486. />
  487. }
  488. </div>
  489. <div className="mt-3 box-border px-4 pb-8">
  490. <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
  491. <div className="mt-6 w-full">
  492. { paymentMethodLoading ?
  493. <LoadingCheckoutPlaceOrder />
  494. :
  495. <CheckoutPlaceOrder
  496. paymentMethod={selectedPaymentMethod}
  497. clickPlaceOrder={handlePlaceOrder}
  498. />
  499. }
  500. </div>
  501. </div>
  502. </section>
  503. );
  504. };