| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- "use client";
- import {useState, useEffect, useRef} from "react";
- import { LoadingSpinner } from "@components/common/LoadingSpinner";
- import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
- import { useForm, FormProvider } from "react-hook-form";
- import {
- Country,
- ShipAddressFormData,
- BillAddressFormData,
- FullAddressFormData,
- CreateCheckoutAddressVariables,
- } from "@/types/checkout/type";
- import AddressResultDisplay from "./AddressResultDisplay";
- import AddressResultDisplayLoading from "./AddressResultDisplayLoading";
- import ShippingAddressCheckout from "./ShippingAddressCheckout";
- import BillingAddressCheckout from "./BillingAddressCheckout";
- import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
- import { useCustomToast } from "@/utils/hooks/useToast";
- import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
- import {useCheckoutPaymentMethod} from "@/utils/hooks/useCheckoutPaymentMethod";
- import {useCheckoutShippingMethod} from "@/utils/hooks/useCheckoutShippingMethod";
- import { useAppDispatch,useAppStore } from "@/store/hooks";
- import { updateCart } from "@/store/slices/cart-slice";
- import { useCartDetail } from "@utils/hooks/useCartDetail";
- import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
- import LoadingPaymentMethod from "./LoadingPaymentMethod";
- import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
- import CommonModal from "@/components/theme/ui/CommonModal";
- import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
- import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
- import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
- import { CartDetail } from "@/types/cart/type";
- /***
- * 不用useForShipping字段了
- * 以shipping address 为准,根据shippingaddress 设置billing address
- * 保存完地址之后要重新获取运输方式和支付方式
- * 变更运输方式后重新获取支付方式
- * @todo 购物车为空 跳转到空购物车页面
- */
- /**
- * 生成createCheckoutAddress接口的参数
- */
- function generateSaveCheckoutAddressParam(formData: FullAddressFormData):CreateCheckoutAddressVariables {
- const formDataShippingAddress:ShipAddressFormData = {
- shippingAddressId: formData.shippingAddressId,
- shippingEmail: formData.shippingEmail,
- shippingFirstName: formData.shippingFirstName,
- shippingLastName: formData.shippingLastName,
- shippingCompanyName: formData.shippingCompanyName,
- shippingAddress: formData.shippingAddress,
- shippingCountry: formData.shippingCountry,
- shippingState: formData.shippingState,
- shippingCity: formData.shippingCity,
- shippingPostcode: formData.shippingPostcode,
- shippingPhoneNumber: formData.shippingPhoneNumber,
- };
- let formDataBillingAddress = {
- billingAddressId: formData.billingAddressId,
- billingEmail: formData.billingEmail,
- billingFirstName: formData.billingFirstName,
- billingLastName: formData.billingLastName,
- billingCompanyName : formData.billingCompanyName,
- billingAddress: formData.billingAddress,
- billingCountry: formData.billingCountry,
- billingState: formData.billingState,
- billingCity: formData.billingCity,
- billingPostcode: formData.billingPostcode,
- billingPhoneNumber: formData.billingPhoneNumber,
- useForShipping: false,
- };
- if(formData.billingSameAsShipping) {
- formDataBillingAddress = {
- billingAddressId: formData.billingAddressId,
- billingEmail: formDataShippingAddress.shippingEmail,
- billingFirstName: formDataShippingAddress.shippingFirstName,
- billingLastName: formDataShippingAddress.shippingLastName,
- billingCompanyName : formDataShippingAddress.shippingCompanyName,
- billingAddress: formDataShippingAddress.shippingAddress,
- billingCountry: formDataShippingAddress.shippingCountry,
- billingState: formDataShippingAddress.shippingState,
- billingCity: formDataShippingAddress.shippingCity,
- billingPostcode: formDataShippingAddress.shippingPostcode,
- billingPhoneNumber: formDataShippingAddress.shippingPhoneNumber,
- useForShipping: true,
- }
- }
- return {
- ...formDataShippingAddress,
- ...formDataBillingAddress
- };
-
- }
- export default function CheckoutWrapper({
- loginEmail,
- countries,
- }: {
- loginEmail: string;
- countries: Country[],
- }) {
- const dispatch = useAppDispatch();
- const { cartData } = useCartDetail();
- const {saveCheckoutCart} = useSaveCheckoutCart();
-
- const {
- data: shippingMethodDatas,
- loading: shippingMethodLoading,
- error: shippingMethodError,
- getShippingMethod,
- } = useCheckoutShippingMethod();
- const selectedPaymentMethod = cartData?.paymentMethod ?? 'paypal_smart_button';
-
- const {
- data: paymentMethodDatas,
- loading: paymentMethodLoading,
- error: paymentMethodError,
- getPaymentMethod,
- } = useCheckoutPaymentMethod();
- const { showToast } = useCustomToast();
- const { getCheckoutAddress, saveCheckoutAddress } = useCheckoutAddress(loginEmail);
- const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
- const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
- // useForm() 只会在组件初始化时读取一次 defaultValues
- const addressForm = useForm<FullAddressFormData>({
- mode: "onBlur", // 或 "onChange"
- reValidateMode: "onChange",
- defaultValues: {
- "shippingAddressId": "",
- "shippingEmail": loginEmail,
- "shippingFirstName": "",
- "shippingLastName": "jack",
- "shippingCompanyName": "",
- "shippingAddress": "",
- "shippingCountry": "US",
- "shippingState": "",
- "shippingCity": "",
- "shippingPostcode": "",
- "shippingPhoneNumber": "",
- "billingAddressId": "",
- "billingEmail": "",
- "billingFirstName": "",
- "billingLastName": "",
- "billingCompanyName": "",
- "billingAddress": "",
- "billingCountry": "US",
- "billingState": "",
- "billingCity": "",
- "billingPostcode": "",
- "billingPhoneNumber": "",
- "billingSameAsShipping": true
- }
- });
- const [addressModalOpen, setAddressModalOpen] = useState(false);
- const [loadingAddress, setLoadingAddress] = useState(true); // 初始加载地址数据时的loading状态
- const serverShippingAddress = useRef<ShipAddressFormData | null>(null);
- const serverBillingAddress = useRef<BillAddressFormData | null>(null);
- const [addressSaving, setAddressSaving] = useState(false); // 保存地址时的loading状态
- // 开发环境会执行两次 https://zh-hans.react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
- useEffect(() => {
- getCheckoutAddress((shippingAddress,billingAddress)=> {
- addressForm.reset({
- ...shippingAddress,
- ...billingAddress,
- });
- serverShippingAddress.current = shippingAddress;
- serverBillingAddress.current = billingAddress;
-
- setLoadingAddress(false);
- }, ()=> {
- setLoadingAddress(false);
- });
- }, []);
- const openAddressModal = () => {
- setAddressModalOpen(true);
- if(serverShippingAddress.current && serverBillingAddress.current) {
- addressForm.reset({
- ...serverShippingAddress.current,
- ...serverBillingAddress.current,
- });
- }
-
- };
- const closeAddressModal = (e:boolean) => {
- setAddressModalOpen(e);
- // 重置表单
- addressForm.reset();
- };
- const addressFormOnSubmit = async (formData: FullAddressFormData) => {
- // console.log('addressFormOnSubmit ---- ',formData); return;
- if(addressSaving) {
- return;
- }
- const saveAddressParam = generateSaveCheckoutAddressParam(formData);
- setAddressSaving(true);
- try {
- const res = await saveCheckoutAddress(saveAddressParam);
- console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
- // 地址保存成功后重新获取运输方式和支付方式
- getShippingMethod();
- getPaymentMethod();
-
- // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
- const queryAddressRes = await getCheckoutAddress();
- if(queryAddressRes !== null) {
- addressForm.resetField('shippingAddressId',{
- defaultValue: queryAddressRes.shippingAddress.shippingAddressId
- });
- addressForm.resetField('billingAddressId',{
- defaultValue: queryAddressRes.billingAddress.billingAddressId
- });
- serverShippingAddress.current = queryAddressRes.shippingAddress;
- serverBillingAddress.current = queryAddressRes.billingAddress;
- // 同步地址到购物车详情
- dispatch(updateCart({
- billingAddress: billingAddressToCartAddress(queryAddressRes.billingAddress),
- shippingAddress: shippingAddressToCartAddress(queryAddressRes.shippingAddress)
- }));
- }
- setAddressModalOpen(false);
- } catch(err) {
- // 错误处理
- console.error("save address error", err);
-
- showToast('Save address failed. Please try again.', 'danger');
- } finally {
- setAddressSaving(false);
- }
-
- };
- const handlePaymentMethodChange = async (method: string,title: string) => {
- let shippingMethod = '-1';
- if(methodShiippingRef.current) {
- shippingMethod = methodShiippingRef.current.getSelectShipMethod();
- }
- overlayLoading.start();
- const saveRes = await saveCheckoutCart({
- shippingMethod: shippingMethod,
- paymentMethod: method,
- couponCode: '-1'
- });
- overlayLoading.stop();
- if(!saveRes.error) {
- if(saveRes.data) {
- const newCartDetail = formatCartDetail(saveRes.data);
- dispatch(updateCart(newCartDetail));
- }
- } else {
- showToast(saveRes.msg, 'danger');
- }
- /*
- const res = await savePaymentMethod({ paymentMethod: method });
- console.log('savePaymentMethod res ====== ',res);
- if(!res.error) {
- // 更新paymentMethod paymentMethodTitle
- dispatch(updateCart({
- paymentMethod: method,
- paymentMethodTitle: title
- }));
- } else {
- showToast(res.msg, 'danger');
- }
- */
- };
- const handleShippingMethodChange = async (method: string) => {
- overlayLoading.start();
- const saveRes = await saveCheckoutCart({
- shippingMethod: method,
- paymentMethod: '-1',
- couponCode: '-1'
- });
- overlayLoading.stop();
- if(!saveRes.error) {
- if(saveRes.data) {
- const newCartDetail = formatCartDetail(saveRes.data);
- dispatch(updateCart(newCartDetail));
- }
- // 保存成功之后需要重新获取支付方式
- getPaymentMethod();
- } else {
- showToast(saveRes.msg, 'danger');
- }
- /*
- const res = await saveShippingMethod({ shippingMethod: method });
- if(!res.error) {
- // 保存成功之后需要重新获取购物车详情和支付方式
- getCartDetail();
- getPaymentMethod();
- } else {
- showToast(res.msg, 'danger');
- }*/
- };
- // 下单
- // 1. 校验地址 运输方式,支付方式;
- // 2. 根据购物车详情判断是否需要传下单参数: 地址,运输方式,支付方式
- const handlePlaceOrder = async () => {
- // 校验地址
- const addressValid = await addressForm.trigger();
- if(!addressValid) {
- showToast('Please fill in the address information correctly.', 'danger');
- return false;
- }
- if(methodShiippingRef.current) {
- const shippingMethodValid = await methodShiippingRef.current.validateShipMethod();
- if(!shippingMethodValid) {
- showToast('Please select a shipping method.', 'danger');
- return false;
- }
- } else {
- showToast('Please wait for shipping method loading.', 'danger');
- return false;
- }
- if(methodPaymentRef.current) {
- const paymentMethodValid = await methodPaymentRef.current.validatePaymentMethod();
- if(!paymentMethodValid) {
- showToast('Please select a payment method.', 'danger');
- return false;
- }
- } else {
- showToast('Please wait for payment method loading.', 'danger');
- return false;
- }
- if(!cartData) {
- showToast('Please add product to your cart.', 'danger');
- return false;
- }
-
- //如果购物车详情没有保存过支付方式,则需要保存支付方式
- if(cartData?.paymentMethod === null) {
- const pmethod = methodPaymentRef?.current.getSelectPaymentMethod();
- const saveRes = await saveCheckoutCart({
- shippingMethod: '-1',
- paymentMethod: pmethod,
- couponCode: '-1'
- });
- if(!saveRes.error) {
- if(saveRes.data) {
- const newCartDetail = formatCartDetail(saveRes.data);
- dispatch(updateCart(newCartDetail));
- }
- }
- }
- return true;
- };
-
- // function tt() {
- // const arr = [
- // ['US','3803800217'],
- // ['CH','+4186043315'],
- // ['US','334-208-6177'],
- // ['AU','+61 404103617'],
- // ['US','1-9166705105'],
- // ['CA','819 384 3221'],
- // ['US','(409) 239-9482'],
- // ['US','1-(651) 421-2762'],
- // ['CH', '1-792930242'],
- // ['US', '+1 4570438892'],
- // ['US', '1-+1 (404) 276-1068'],
- // ['CH', '+49 015164340021'], // 国家与phonecode不一致情况
- // ];
- // arr.forEach((item) => {
- // normalizePhoneForForm(item[1], item[0]);
- // });
- // }
- return (
- <section className="w-full">
- <div>
- {/* <button className="w-25 h-8 bg-amber-500 flex items-center justify-center" onClick={tt}>test</button> */}
- {loadingAddress ?
- <AddressResultDisplayLoading />
- :
- <AddressResultDisplay shippingAddress={cartData?.shippingAddress ?? null}
- onAddressModalOpenClick={openAddressModal}
- />
- }
- <CommonModal
- isOpen={addressModalOpen}
- onClose={closeAddressModal}
- header={
- <div className="w-full box-border p-3.75 h-14.5 flex justify-between items-center">
- <span>Shipping Address</span>
- <button className="w-6 h-6" onClick={() => closeAddressModal(false)}>
- <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>
- </button>
- </div>
- }
- body={
- <div className="box-border w-full h-full p-3.75">
- <p className="text-ly-12 leading-ly-20">
- Select a billing address from your address book or enter a new address.
- </p>
- <div>
- <FormProvider {...addressForm}>
- <form>
- <ShippingAddressCheckout countries={countries} />
- <BillingAddressCheckout countries={countries} />
- </form>
- </FormProvider>
- </div>
- </div>
- }
- footer={
- <div className="box-border w-full p-3.75">
- <button className="block flex justify-center items-center w-full h-12 bg-ly-green rounded-3xl text-white text-ly-16 font-bold"
- onClick={addressForm.handleSubmit(addressFormOnSubmit)}
- >
- {addressSaving ? <LoadingSpinner /> : 'Save'}
- </button>
- </div>
- }
- />
- </div>
- <div className="mt-6 box-border px-4">
- <h3 className="text-ly-24 font-medium">Shipping Method</h3>
- {shippingMethodLoading ?
-
- <CheckoutShippingMethodLoading />
- :
- <ShippingMethodCheckout
- ref={methodShiippingRef}
- shipMethods={shippingMethodDatas}
- errorMsg={shippingMethodError}
- onShipMethodChange={handleShippingMethodChange}
- />
- }
- </div>
- <div className="mt-6 box-border px-4">
- <h3 className="text-ly-24 font-medium">Payment Method</h3>
- { paymentMethodLoading ?
- <LoadingPaymentMethod />
- :
- <PaymentMethodCheckout
- ref={methodPaymentRef}
- errorMsg={paymentMethodError}
- paymentMethods={paymentMethodDatas}
- onPaymentMethodChange={handlePaymentMethodChange}
- />
- }
-
- </div>
- <div className="mt-3 box-border px-4">
- <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
- <div className="mt-6 w-full">
- <CheckoutPlaceOrder
- paymentMethod={selectedPaymentMethod}
- clickPlaceOrder={handlePlaceOrder}
- />
- </div>
- </div>
- </section>
-
- );
- };
|