CheckoutWrapper.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. "use client";
  2. import {useState, useEffect} from "react";
  3. import {useApolloClient} from "@apollo/client/react";
  4. import { GET_CHECKOUT_ADDRESSES,CREATE_CHECKOUT_ADDRESS } from "@/graphql";
  5. import { LoadingSpinner } from "@components/common/LoadingSpinner";
  6. // import { useForm, SubmitHandler } from "react-hook-form"
  7. import { useForm, FormProvider } from "react-hook-form"
  8. import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
  9. import {
  10. CheckoutAddressNode,
  11. Country,
  12. ShipAddressFormData,
  13. BillAddressFormData,
  14. FullAddressFormData
  15. } from "@/types/checkout/type";
  16. import AddressResultDisplay from "./AddressResultDisplay";
  17. import AddressResultDisplayLoading from "./AddressResultDisplayLoading";
  18. import CheckoutAddressModal from "./CheckoutAddressModal";
  19. import ShippingAddressCheckout from "./ShippingAddressCheckout";
  20. import BillingAddressCheckout from "./BillingAddressCheckout";
  21. import { useCustomToast } from "@/utils/hooks/useToast";
  22. /***
  23. * 不用useForShipping字段了
  24. * 以shipping address 为准,根据shippingaddress 设置billing address
  25. * 保存完地址之后要重新获取运输方式和支付方式
  26. */
  27. /**
  28. * 生成createCheckoutAddress接口的参数
  29. */
  30. function generateSaveCheckoutAddressParam(formData: FullAddressFormData) {
  31. const formDataShippingAddress:ShipAddressFormData = {
  32. shippingAddressId: formData.shippingAddressId,
  33. shippingEmail: formData.shippingEmail,
  34. shippingFirstName: formData.shippingFirstName,
  35. shippingLastName: formData.shippingLastName,
  36. shippingCompanyName: formData.shippingCompanyName,
  37. shippingAddress: formData.shippingAddress,
  38. shippingCountry: formData.shippingCountry,
  39. shippingState: formData.shippingState,
  40. shippingCity: formData.shippingCity,
  41. shippingPostcode: formData.shippingPostcode,
  42. shippingPhoneNumber: formData.shippingPhoneNumber,
  43. };
  44. let formDataBillingAddress = {
  45. billingAddressId: formData.billingAddressId,
  46. billingEmail: formData.billingEmail,
  47. billingFirstName: formData.billingFirstName,
  48. billingLastName: formData.billingLastName,
  49. billingCompanyName : formData.billingCompanyName,
  50. billingAddress: formData.billingAddress,
  51. billingCountry: formData.billingCountry,
  52. billingState: formData.billingState,
  53. billingCity: formData.billingCity,
  54. billingPostcode: formData.billingPostcode,
  55. billingPhoneNumber: formData.billingPhoneNumber,
  56. useForShipping: false,
  57. };
  58. if(formData.billingSameAsShipping) {
  59. formDataBillingAddress = {
  60. billingAddressId: formData.billingAddressId,
  61. billingEmail: formDataShippingAddress.shippingEmail,
  62. billingFirstName: formDataShippingAddress.shippingFirstName,
  63. billingLastName: formDataShippingAddress.shippingLastName,
  64. billingCompanyName : formDataShippingAddress.shippingCompanyName,
  65. billingAddress: formDataShippingAddress.shippingAddress,
  66. billingCountry: formDataShippingAddress.shippingCountry,
  67. billingState: formDataShippingAddress.shippingState,
  68. billingCity: formDataShippingAddress.shippingCity,
  69. billingPostcode: formDataShippingAddress.shippingPostcode,
  70. billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
  71. useForShipping: true,
  72. }
  73. }
  74. return {
  75. ...formDataShippingAddress,
  76. ...formDataBillingAddress
  77. };
  78. }
  79. /**
  80. * 对比shipping address与billing address的数据是否完全相同
  81. */
  82. function addressIsSame(billAddress: BillAddressFormData | null, shipAddress: ShipAddressFormData | null) {
  83. if(billAddress === null && shipAddress === null) {
  84. return true;
  85. } else if(billAddress !== null && shipAddress !== null) {
  86. return (
  87. billAddress.billingEmail === shipAddress.shippingEmail &&
  88. billAddress.billingFirstName === shipAddress.shippingFirstName &&
  89. billAddress.billingLastName === shipAddress.shippingLastName &&
  90. billAddress.billingCompanyName === shipAddress.shippingCompanyName &&
  91. billAddress.billingAddress === shipAddress.shippingAddress &&
  92. billAddress.billingCity === shipAddress.shippingCity &&
  93. billAddress.billingState === shipAddress.shippingState &&
  94. billAddress.billingCountry === shipAddress.shippingCountry &&
  95. billAddress.billingPostcode === shipAddress.shippingPostcode &&
  96. billAddress.billingPhoneNumber === shipAddress.shippingPhoneNumber
  97. );
  98. } else {
  99. return false;
  100. }
  101. }
  102. export default function CheckoutWrapper({
  103. loginEmail,
  104. countries
  105. }: {
  106. loginEmail: string;
  107. countries: Country[]
  108. }) {
  109. const { showToast } = useCustomToast();
  110. const apolloClient = useApolloClient();
  111. // useForm() 只会在组件初始化时读取一次 defaultValues
  112. const addressForm = useForm<FullAddressFormData>({
  113. mode: "onBlur", // 或 "onChange"
  114. reValidateMode: "onChange",
  115. });
  116. const [addressModalOpen, setAddressModalOpen] = useState(false);
  117. const [loadingData, setLoadingData] = useState(true);
  118. const [serverShippingAddress, setServerShippingAddress] = useState<ShipAddressFormData | null>(null);
  119. const [serverBillingAddress, setServerBillingAddress] = useState<BillAddressFormData | null>(null);
  120. const [addressSaving, setAddressSaving] = useState(false);
  121. // 获取checkout address
  122. function getCheckoutAddress(
  123. resolveCallback: (shippingAddress:ShipAddressFormData, billingAddress: BillAddressFormData) => void = () => {},
  124. rejectCallback: () => void = ()=> {}
  125. ) {
  126. return apolloClient.query({
  127. query: GET_CHECKOUT_ADDRESSES,
  128. fetchPolicy: "no-cache",
  129. context: {
  130. fetchOptions: {
  131. cache: 'no-store',
  132. },
  133. }
  134. }).then((res) => {
  135. const address = res.data?.collectionGetCheckoutAddresses?.edges?.map(
  136. (edge: { node: CheckoutAddressNode }) => edge.node
  137. ) || [];
  138. const billAddress = address.find((a: CheckoutAddressNode) => a?.addressType === "cart_billing") || null;
  139. const shipAddress = address.find((a: CheckoutAddressNode) => a?.addressType === "cart_shipping") || null;
  140. const shipCountry = shipAddress?.country || "US";
  141. const billCountry = billAddress?.country || "US";
  142. const defaultBillAddress: BillAddressFormData = {
  143. billingAddressId: billAddress?._id || "",
  144. billingEmail: billAddress?.email ?? loginEmail ?? "",
  145. billingFirstName: billAddress?.firstName || "",
  146. billingLastName: billAddress?.lastName || "",
  147. billingCompanyName: billAddress?.companyName || "",
  148. billingAddress: billAddress?.address || "",
  149. billingCountry: billCountry,
  150. billingState: billAddress?.state || "",
  151. billingCity: billAddress?.city || "",
  152. billingPostcode: billAddress?.postcode || "",
  153. billingPhoneNumber: normalizePhoneForForm(billAddress?.phone, billCountry) || "",
  154. billingSameAsShipping: true, // 这个字段前端维护,不传给后端(bagisto是以billing address为准,但是运营要以shipping address为准,所以前端自己维护billing address与shipping address 同步)
  155. };
  156. const defaultShipAddress: ShipAddressFormData = {
  157. shippingAddressId: shipAddress?._id || "",
  158. shippingEmail: shipAddress?.email ?? loginEmail ?? "",
  159. shippingFirstName: shipAddress?.firstName || "",
  160. shippingLastName: shipAddress?.lastName || "",
  161. shippingCompanyName: shipAddress?.companyName || "",
  162. shippingAddress: shipAddress?.address || "",
  163. shippingCountry: shipCountry,
  164. shippingState: shipAddress?.state || "",
  165. shippingCity: shipAddress?.city || "",
  166. shippingPostcode: shipAddress?.postcode || "",
  167. shippingPhoneNumber: normalizePhoneForForm(shipAddress?.phone, shipCountry) || "",
  168. };
  169. const billSameAsShip = addressIsSame(defaultBillAddress, defaultShipAddress);
  170. defaultBillAddress.billingSameAsShipping = billSameAsShip;
  171. console.log('GET_CHECKOUT_ADDRESSES res ---- ',defaultShipAddress,defaultBillAddress);
  172. resolveCallback(defaultShipAddress,defaultBillAddress);
  173. return {
  174. shippingAddress: defaultShipAddress,
  175. billingAddress: defaultBillAddress
  176. }
  177. }).catch((err) => {
  178. console.error('GET_CHECKOUT_ADDRESSES error ---- ',err);
  179. // setLoadingData(false);
  180. rejectCallback();
  181. return null;
  182. });
  183. }
  184. // 开发环境会执行两次 https://zh-hans.react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
  185. useEffect(() => {
  186. getCheckoutAddress((shippingAddress,billingAddress)=> {
  187. addressForm.reset({
  188. ...shippingAddress,
  189. ...billingAddress,
  190. });
  191. setServerShippingAddress({
  192. ...shippingAddress
  193. });
  194. setServerBillingAddress({
  195. ...billingAddress
  196. });
  197. setLoadingData(false);
  198. }, ()=> {
  199. setLoadingData(false);
  200. });
  201. }, []);
  202. const openAddressModal = () => {
  203. setAddressModalOpen(true);
  204. if(serverShippingAddress && serverBillingAddress) {
  205. addressForm.reset({
  206. ...serverShippingAddress,
  207. ...serverBillingAddress,
  208. });
  209. }
  210. };
  211. const closeAddressModal = (e:boolean) => {
  212. setAddressModalOpen(e);
  213. // 重置表单
  214. addressForm.reset();
  215. };
  216. const addressFormOnSubmit = async (formData: FullAddressFormData) => {
  217. // console.log('addressFormOnSubmit ---- ',formData); return;
  218. // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;还需要获取运输方式
  219. const saveAddressParam = generateSaveCheckoutAddressParam(formData);
  220. setAddressSaving(true);
  221. try {
  222. const res = await apolloClient.mutate({
  223. mutation: CREATE_CHECKOUT_ADDRESS,
  224. variables:saveAddressParam,
  225. });
  226. console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
  227. const queryAddressRes = await getCheckoutAddress();
  228. if(queryAddressRes !== null) {
  229. addressForm.resetField('shippingAddressId',{
  230. defaultValue: queryAddressRes.shippingAddress.shippingAddressId
  231. });
  232. addressForm.resetField('billingAddressId',{
  233. defaultValue: queryAddressRes.billingAddress.billingAddressId
  234. });
  235. setServerShippingAddress({
  236. ...queryAddressRes.shippingAddress
  237. });
  238. setServerBillingAddress({
  239. ...queryAddressRes.billingAddress
  240. });
  241. }
  242. setAddressModalOpen(false);
  243. } catch(err) {
  244. // 错误处理
  245. console.error("save address error", err);
  246. showToast('Save address failed. Please try again.', 'danger');
  247. } finally {
  248. setAddressSaving(false);
  249. }
  250. };
  251. // function tt() {
  252. // const arr = [
  253. // ['US','3803800217'],
  254. // ['CH','+4186043315'],
  255. // ['US','334-208-6177'],
  256. // ['AU','+61 404103617'],
  257. // ['US','1-9166705105'],
  258. // ['CA','819 384 3221'],
  259. // ['US','(409) 239-9482'],
  260. // ['US','1-(651) 421-2762'],
  261. // ['CH', '1-792930242'],
  262. // ['US', '+1 4570438892'],
  263. // ['US', '1-+1 (404) 276-1068'],
  264. // ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
  265. // ];
  266. // arr.forEach((item) => {
  267. // normalizePhoneForForm(item[1], item[0]);
  268. // });
  269. // }
  270. return (
  271. <section className="flex flex-col items-start justify-between lg:flex-row lg:justify-between">
  272. {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
  273. {loadingData ? <AddressResultDisplayLoading />
  274. :<AddressResultDisplay shippingAddress={serverShippingAddress}
  275. onAddressModalOpenClick={openAddressModal}
  276. />}
  277. <CheckoutAddressModal
  278. isOpen={addressModalOpen}
  279. onClose={closeAddressModal}
  280. body={
  281. <>
  282. <FormProvider {...addressForm}>
  283. <form>
  284. <ShippingAddressCheckout countries={countries} />
  285. <BillingAddressCheckout countries={countries} />
  286. </form>
  287. </FormProvider>
  288. </>
  289. }
  290. footer={
  291. <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
  292. onClick={addressForm.handleSubmit(addressFormOnSubmit)}
  293. >
  294. {addressSaving ? <LoadingSpinner /> : 'Save'}
  295. </button>
  296. }
  297. />
  298. </section>
  299. );
  300. };