CheckoutAssress.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. "use client";
  2. import {type Ref, useState, useEffect, useCallback, useImperativeHandle, useMemo } from "react";
  3. import { useRouter } from 'next/navigation';
  4. import { useForm, FormProvider } from "react-hook-form";
  5. import { useCustomToast } from "@/utils/hooks/useToast";
  6. import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
  7. import {useLoginModal} from "@/providers/LoginModalProvider";
  8. import { useGetCustomerAddress } from "@utils/hooks/useGetCustomerAddress";
  9. import { EMAIL_REGEX } from "@utils/constants";
  10. import type {
  11. ShipAddressFormData,
  12. FullAddressFormData,
  13. CreateCheckoutAddressVariables,
  14. } from "@/types/checkout/type";
  15. import type { CustomerAddressItem,CheckEmailRegisteredData } from "@/types/customer/type";
  16. import type { CartDetail,CartAddress } from "@/types/cart/type";
  17. import { formatCartDetail } from "@/utils/cartDetailTools";
  18. import {clientFetch} from "@/lib/restApiClient";
  19. import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
  20. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  21. import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
  22. import AddressResultDisplay from "./AddressResultDisplay";
  23. import ShippingAddressCheckout from "./ShippingAddressCheckout";
  24. import BillingAddressCheckout from "./BillingAddressCheckout";
  25. import CommonModal from "@/components/theme/ui/CommonModal";
  26. export interface RefCheckoutAddressHandle {
  27. getAddressFormDate: () => void;
  28. validateAddressForm: () => Promise<boolean>;
  29. }
  30. /***
  31. * 不用useForShipping字段了
  32. * 以shipping address 为准,根据shippingaddress 设置billing address
  33. * 保存完地址之后要重新获取运输方式和支付方式
  34. */
  35. /**
  36. * 地址表单默认值
  37. */
  38. function getBillingAddressFormData(billingAddress:CartAddress) {
  39. const billingCountry = billingAddress.country || 'US';
  40. const billingPhoneNumber = normalizePhoneForForm(billingAddress.phone, billingCountry) || "";
  41. const res = {
  42. "billingAddressId": billingAddress.id,
  43. "billingEmail": billingAddress.email,
  44. "billingFirstName": billingAddress.firstName,
  45. "billingLastName": billingAddress.lastName,
  46. "billingCompanyName": "",
  47. "billingAddress": billingAddress.address,
  48. "billingCountry": billingCountry,
  49. "billingState": billingAddress.state,
  50. "billingCity": billingAddress.city,
  51. "billingPostcode": billingAddress.postcode,
  52. "billingPhoneNumber": billingPhoneNumber,
  53. };
  54. return res;
  55. }
  56. function getShippingAddressFormData(shippingAddress:CartAddress) {
  57. const shippingCountry = shippingAddress.country || 'US';
  58. const shippingPhoneNumber = normalizePhoneForForm(shippingAddress.phone, shippingCountry) || "";
  59. const res = {
  60. "shippingAddressId": shippingAddress.id,
  61. "shippingEmail": shippingAddress.email,
  62. "shippingFirstName": shippingAddress.firstName,
  63. "shippingLastName": shippingAddress.lastName,
  64. "shippingCompanyName": "",
  65. "shippingAddress": shippingAddress.address,
  66. "shippingCountry": shippingCountry,
  67. "shippingState": shippingAddress.state,
  68. "shippingCity": shippingAddress.city,
  69. "shippingPostcode": shippingAddress.postcode,
  70. "shippingPhoneNumber": shippingPhoneNumber,
  71. };
  72. return res;
  73. }
  74. function getAddressFormDataFromCart(cartDetail:CartDetail, loginEmail?: string) {
  75. const shippingAddress = cartDetail.shippingAddress;
  76. const billingAddress = cartDetail.billingAddress;
  77. const defaultValues = {
  78. "shippingAddressId": "",
  79. "shippingEmail": loginEmail ?? '',
  80. "shippingFirstName": "",
  81. "shippingLastName": "",
  82. "shippingCompanyName": "",
  83. "shippingAddress": "",
  84. "shippingCountry": "US",
  85. "shippingState": "",
  86. "shippingCity": "",
  87. "shippingPostcode": "",
  88. "shippingPhoneNumber": "",
  89. "billingAddressId": "",
  90. "billingEmail": loginEmail ?? '',
  91. "billingFirstName": "",
  92. "billingLastName": "",
  93. "billingCompanyName": "",
  94. "billingAddress": "",
  95. "billingCountry": "US",
  96. "billingState": "",
  97. "billingCity": "",
  98. "billingPostcode": "",
  99. "billingPhoneNumber": "",
  100. "billingSameAsShipping": true
  101. }
  102. if(billingAddress) {
  103. const bs = getBillingAddressFormData(billingAddress);
  104. defaultValues.billingAddressId = bs.billingAddressId;
  105. defaultValues.billingEmail = bs.billingEmail;
  106. defaultValues.billingFirstName = bs.billingFirstName;
  107. defaultValues.billingLastName = bs.billingLastName;
  108. defaultValues.billingCompanyName = bs.billingCompanyName;
  109. defaultValues.billingAddress = bs.billingAddress;
  110. defaultValues.billingCountry = bs.billingCountry;
  111. defaultValues.billingState = bs.billingState;
  112. defaultValues.billingCity = bs.billingCity;
  113. defaultValues.billingPostcode = bs.billingPostcode;
  114. defaultValues.billingPhoneNumber = bs.billingPhoneNumber;
  115. // defaultValues.billingSameAsShipping = true;
  116. }
  117. if(shippingAddress) {
  118. const ss = getShippingAddressFormData(shippingAddress);
  119. defaultValues.shippingAddressId = ss.shippingAddressId;
  120. defaultValues.shippingEmail = ss.shippingEmail;
  121. defaultValues.shippingFirstName = ss.shippingFirstName;
  122. defaultValues.shippingLastName = ss.shippingLastName;
  123. defaultValues.shippingCompanyName = ss.shippingCompanyName;
  124. defaultValues.shippingAddress = ss.shippingAddress;
  125. defaultValues.shippingCountry = ss.shippingCountry;
  126. defaultValues.shippingState = ss.shippingState;
  127. defaultValues.shippingCity = ss.shippingCity;
  128. defaultValues.shippingPostcode = ss.shippingPostcode;
  129. defaultValues.shippingPhoneNumber = ss.shippingPhoneNumber;
  130. }
  131. if( defaultValues.billingEmail === defaultValues.shippingEmail &&
  132. defaultValues.billingFirstName === defaultValues.shippingFirstName &&
  133. defaultValues.billingLastName === defaultValues.shippingLastName &&
  134. defaultValues.billingCompanyName === defaultValues.shippingCompanyName &&
  135. defaultValues.billingAddress === defaultValues.shippingAddress &&
  136. defaultValues.billingCountry === defaultValues.shippingCountry &&
  137. defaultValues.billingState === defaultValues.shippingState &&
  138. defaultValues.billingCity === defaultValues.shippingCity &&
  139. defaultValues.billingPostcode === defaultValues.shippingPostcode &&
  140. defaultValues.billingPhoneNumber === defaultValues.shippingPhoneNumber
  141. ) {
  142. defaultValues.billingSameAsShipping = true;
  143. }
  144. return defaultValues;
  145. }
  146. /**
  147. * 生成createCheckoutAddress接口的参数
  148. */
  149. function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateCheckoutAddressVariables {
  150. const formDataShippingAddress:ShipAddressFormData = {
  151. shippingAddressId: formData.shippingAddressId,
  152. shippingEmail: formData.shippingEmail,
  153. shippingFirstName: formData.shippingFirstName,
  154. shippingLastName: formData.shippingLastName,
  155. shippingCompanyName: formData.shippingCompanyName,
  156. shippingAddress: formData.shippingAddress,
  157. shippingCountry: formData.shippingCountry,
  158. shippingState: formData.shippingState,
  159. shippingCity: formData.shippingCity,
  160. shippingPostcode: formData.shippingPostcode,
  161. shippingPhoneNumber: formData.shippingPhoneNumber,
  162. };
  163. let formDataBillingAddress = {
  164. billingAddressId: formData.billingAddressId,
  165. billingEmail: formData.billingEmail,
  166. billingFirstName: formData.billingFirstName,
  167. billingLastName: formData.billingLastName,
  168. billingCompanyName : formData.billingCompanyName,
  169. billingAddress: formData.billingAddress,
  170. billingCountry: formData.billingCountry,
  171. billingState: formData.billingState,
  172. billingCity: formData.billingCity,
  173. billingPostcode: formData.billingPostcode,
  174. billingPhoneNumber: formData.billingPhoneNumber,
  175. useForShipping: false,
  176. };
  177. if(formData.billingSameAsShipping) {
  178. formDataBillingAddress = {
  179. billingAddressId: formData.billingAddressId,
  180. billingEmail: formDataShippingAddress.shippingEmail,
  181. billingFirstName: formDataShippingAddress.shippingFirstName,
  182. billingLastName: formDataShippingAddress.shippingLastName,
  183. billingCompanyName : formDataShippingAddress.shippingCompanyName,
  184. billingAddress: formDataShippingAddress.shippingAddress,
  185. billingCountry: formDataShippingAddress.shippingCountry,
  186. billingState: formDataShippingAddress.shippingState,
  187. billingCity: formDataShippingAddress.shippingCity,
  188. billingPostcode: formDataShippingAddress.shippingPostcode,
  189. billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
  190. useForShipping: true,
  191. }
  192. }
  193. return {
  194. ...formDataShippingAddress,
  195. ...formDataBillingAddress
  196. };
  197. }
  198. const myAddressPerPage = 50; // 每页的个数
  199. export function CheckoutAddress({
  200. ref,
  201. loginEmail,
  202. cartData,
  203. onSaveAddress,
  204. onCartChange
  205. }: {
  206. ref: Ref<RefCheckoutAddressHandle>;
  207. loginEmail: string;
  208. cartData: CartDetail;
  209. onSaveAddress: (saveRes: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => void;
  210. onCartChange: (data: CartDetail,dispatch: boolean) => void;
  211. }) {
  212. const router = useRouter();
  213. const {openLoginModal} = useLoginModal();
  214. const addressFormDefaultValues = useMemo(() => {
  215. return getAddressFormDataFromCart(cartData,loginEmail)
  216. },[cartData,loginEmail]);
  217. const {getCustomerAddress} = useGetCustomerAddress();
  218. const { showToast } = useCustomToast();
  219. const { saveCheckoutAddress } = useCheckoutAddress(loginEmail);
  220. // useForm() 只会在组件初始化时读取一次 defaultValues
  221. const addressForm = useForm<FullAddressFormData>({
  222. mode: "onChange", // 或 "onChange"
  223. reValidateMode: "onChange",
  224. defaultValues: addressFormDefaultValues
  225. });
  226. const [myAddressList, setMyAddressList] = useState<CustomerAddressItem[]>([]);
  227. const [showShipFormField, setShowShipFormField] = useState(false);
  228. const [showBillFormField, setShowBillFormField] = useState(false);
  229. const [addressFormModalOpen, setAddressFormModalOpen] = useState(false);
  230. useImperativeHandle(ref, () => {
  231. return {
  232. // 获取用户填写的地址
  233. getAddressFormDate: () => {
  234. return addressForm.getValues();
  235. },
  236. // 触发校验
  237. validateAddressForm: () => {
  238. return addressForm.trigger();
  239. },
  240. }
  241. },[addressForm]);
  242. const showShipFormFieldChange = useCallback((e: boolean) => {
  243. setShowShipFormField(e);
  244. },[]);
  245. const showBillFormFieldChange = useCallback((e: boolean) => {
  246. setShowBillFormField(e);
  247. },[]);
  248. const openAddressFormModal = () => {
  249. setAddressFormModalOpen(true);
  250. setShowShipFormField(false);
  251. setShowBillFormField(false);
  252. if(cartData.billingAddress && cartData.shippingAddress) {
  253. addressForm.reset({
  254. ...getAddressFormDataFromCart(cartData)
  255. });
  256. }
  257. };
  258. const closeAddressFormModal = () => {
  259. setAddressFormModalOpen(false);
  260. setShowShipFormField(false);
  261. setShowBillFormField(false);
  262. // 重置表单
  263. addressForm.reset();
  264. };
  265. const addressFormOnSubmit = async (formData: FullAddressFormData) => {
  266. // console.log('addressFormOnSubmit ---- ',formData); return;
  267. const saveAddressParam = generateSaveCheckoutAddressParam(formData);
  268. overlayLoading.start();
  269. try {
  270. const saveRes = await saveCheckoutAddress(saveAddressParam);
  271. console.log('CREATE_CHECKOUT_ADDRESS res ====== ',saveRes);
  272. if(!saveRes.error) {
  273. // 地址保存成功后重新获取运输方式和支付方式
  274. // 保存完地址之后,把保存后的地址id同步到表单里;
  275. const createAddressData = saveRes.data;
  276. if(!createAddressData) {
  277. // 提示用户出错了,刷新页面
  278. showToast('Something wrong. Please refresh the page.', 'danger');
  279. return;
  280. }
  281. addressForm.resetField('shippingAddressId',{
  282. defaultValue: createAddressData.shippingAddressId
  283. });
  284. addressForm.resetField('billingAddressId',{
  285. defaultValue: createAddressData.billingAddressId
  286. });
  287. // 同步地址到购物车详情
  288. const newBillingAddress = {
  289. id: String(createAddressData.billingAddressId),
  290. firstName: createAddressData.billingFirstName,
  291. lastName: createAddressData.billingLastName,
  292. email: createAddressData.billingEmail,
  293. address: createAddressData.billingAddress,
  294. city: createAddressData.billingCity,
  295. state: createAddressData.billingState,
  296. country: createAddressData.billingCountry,
  297. postcode: createAddressData.billingPostcode,
  298. phone: createAddressData.billingPhoneNumber
  299. };
  300. const newShippingAddress = {
  301. id: String(createAddressData.shippingAddressId),
  302. firstName: createAddressData.shippingFirstName,
  303. lastName: createAddressData.shippingLastName,
  304. email: createAddressData.shippingEmail,
  305. address: createAddressData.shippingAddress,
  306. city: createAddressData.shippingCity,
  307. state: createAddressData.shippingState,
  308. country: createAddressData.shippingCountry,
  309. postcode: createAddressData.shippingPostcode,
  310. phone: createAddressData.shippingPhoneNumber,
  311. };
  312. onSaveAddress({
  313. billingAddress: newBillingAddress,
  314. shippingAddress: newShippingAddress
  315. });
  316. } else {
  317. showToast(saveRes.msg, 'danger');
  318. }
  319. setAddressFormModalOpen(false);
  320. } catch(err) {
  321. // 错误处理
  322. console.error("save address error", err);
  323. showToast('Save address failed. Please try again.', 'danger');
  324. } finally {
  325. overlayLoading.stop();
  326. }
  327. };
  328. const noShipAddress = cartData.shippingAddress === null;
  329. const noBillAddress = cartData.billingAddress === null;
  330. // 登录弹窗登录后,获取用户地址列表,重置地址表单默认值
  331. useEffect(() => {
  332. if(cartData.isGuest) return;
  333. async function loadAddress() {
  334. overlayLoading.start();
  335. const res = await getCustomerAddress({
  336. first: myAddressPerPage
  337. });
  338. overlayLoading.stop();
  339. if(!res.error && res.data !== null) {
  340. setMyAddressList(res.data.list);
  341. } else {
  342. showToast(res.msg,"danger");
  343. }
  344. }
  345. loadAddress();
  346. },[showToast,cartData.isGuest]);
  347. useEffect(() => {
  348. if(cartData.isGuest) return;
  349. const timer = window.requestAnimationFrame(() => {
  350. addressForm.reset(addressFormDefaultValues);
  351. });
  352. return () => window.cancelAnimationFrame(timer)
  353. },[cartData.isGuest,addressFormDefaultValues]);
  354. const verifyEmailIsRegistered = async (value: string) => {
  355. if(!cartData.isGuest) return;
  356. if(!EMAIL_REGEX.test(value)) return;
  357. const param = new URLSearchParams({ email: value });
  358. const res = await clientFetch<CheckEmailRegisteredData>(`/api/customer/check-email?${param.toString()}`,{
  359. method: 'GET',
  360. });
  361. if(res.success && res.data.exists) { // 注册过
  362. openLoginModal({
  363. defaultEmail: value,
  364. onFinalResult: async (res) => {
  365. if(res.mergeCartSuccess) { // 购物车合并成功
  366. if(res.cartAfterMerge){
  367. const newCartDetail = formatCartDetail(res.cartAfterMerge);
  368. onCartChange(newCartDetail,false);
  369. }
  370. } else { // 购物车合并失败
  371. // 三种 情况: 登录成功; 注册成功;
  372. if(res.loginSuccess) {
  373. // 登录成功 需要刷新页面
  374. showToast('You have logged in successfully. But something wrong','danger');
  375. await confirmDialog({
  376. title: "Warning",
  377. content: "You have logged in successfully. But something wrong. You must refresh the page.",
  378. noCancel: true,
  379. });
  380. window.location.reload();
  381. }
  382. if(res.registerSuccess && !res.loginSuccess) {
  383. // 注册成功,去登录
  384. await confirmDialog({
  385. title: "Warning",
  386. content: "You have registered successfully. But something wrong. You have to login.",
  387. noCancel: true,
  388. });
  389. router.replace('/customer/login');
  390. }
  391. }
  392. }
  393. });
  394. }
  395. };
  396. // function tt() {
  397. // const arr = [
  398. // ['US','3803800217'],
  399. // ['CH','+4186043315'],
  400. // ['US','334-208-6177'],
  401. // ['AU','+61 404103617'],
  402. // ['US','1-9166705105'],
  403. // ['CA','819 384 3221'],
  404. // ['US','(409) 239-9482'],
  405. // ['US','1-(651) 421-2762'],
  406. // ['CH', '1-792930242'],
  407. // ['US', '+1 4570438892'],
  408. // ['US', '1-+1 (404) 276-1068'],
  409. // ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
  410. // ];
  411. // arr.forEach((item) => {
  412. // normalizePhoneForForm(item[1], item[0]);
  413. // });
  414. // }
  415. return (<>
  416. <AddressResultDisplay shippingAddress={cartData.shippingAddress || null}
  417. onAddressModalOpenClick={openAddressFormModal}
  418. />
  419. <CommonModal
  420. isOpen={addressFormModalOpen}
  421. onClose={closeAddressFormModal}
  422. header={
  423. <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
  424. <span>Shipping Address</span>
  425. <button className="w-6 h-6" onClick={closeAddressFormModal}>
  426. <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>
  427. </button>
  428. </div>
  429. }
  430. body={
  431. <div className="box-border w-full h-full p-3.75">
  432. <div>
  433. <FormProvider {...addressForm}>
  434. <form>
  435. <ShippingAddressCheckout
  436. noAddress={noShipAddress}
  437. isGuest={cartData.isGuest}
  438. customerAddressList={myAddressList}
  439. showFormField={showShipFormField}
  440. onShowFormFieldChange={showShipFormFieldChange}
  441. onEmailChange={verifyEmailIsRegistered}
  442. />
  443. <BillingAddressCheckout
  444. noAddress={noBillAddress}
  445. isGuest={cartData.isGuest}
  446. customerAddressList={myAddressList}
  447. showFormField={showBillFormField}
  448. onShowFormFieldChange={showBillFormFieldChange}
  449. />
  450. </form>
  451. </FormProvider>
  452. </div>
  453. </div>
  454. }
  455. footer={
  456. <div className="box-border w-full p-3.75">
  457. <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
  458. onClick={addressForm.handleSubmit(addressFormOnSubmit)}
  459. >
  460. Save
  461. </button>
  462. </div>
  463. }
  464. />
  465. </>);
  466. };