| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- "use client";
- import { useMemo, useState, useEffect, useCallback } from "react";
- import { useQuery } from "@apollo/client/react";
- import clsx from "clsx";
- import {
- useFormContext,
- useWatch,
- Controller
- } from "react-hook-form";
- import {
- formatFinalPhoneValue,
- getPhoneCodeBycountryCode
- } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
- import { GET_COUNTRY_STATES } from "@/graphql";
- import {
- EMAIL_REGEX,
- IS_VALID_INPUT,
- IS_VALID_PHONECODE,
- IS_VALID_FULL_PHONE
- } from "@utils/constants";
- import { useConfig } from "@utils/hooks/useConfig";
- import type { CustomerAddressItem } from "@/types/customer/type";
- import InputText from "@/components/theme/ui/InputText";
- import Select from "@/components/theme/ui/Select";
- import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
- import { LoadingSpinner } from "@components/common/LoadingSpinner";
- import {LyDropDown} from "@/components/theme/ui/LyDropDown";
- export default function BillingAddressCheckout ({
- noAddress,
- isGuest,
- showFormField,
- onShowFormFieldChange,
- customerAddressList
- }: {
- noAddress: boolean;
- isGuest: boolean;
- showFormField: boolean;
- onShowFormFieldChange: (e: boolean) => void;
- customerAddressList: CustomerAddressItem[];
- }) {
- const {countries} = useConfig();
- const {
- getValues,
- setValue,
- register,
- clearErrors,
- formState: { errors },
- control
- } = useFormContext() // retrieve all hook methods
- const countriesOptions = useMemo(() => {
- return countries.map((country) => ({
- value: country.code,
- label: country.name,
- id: String(country._id),
- }))
- },[countries]);
- const [
- billingSameAsShipping,
- selectedCountryCode,
- firstName,
- lastName,
- streetAddress,
- stateProvince,
- postCode,
- city,
- phoneNumber
- ] = useWatch({
- control,
- name: [
- 'billingSameAsShipping',
- 'billingCountry',
- 'billingFirstName',
- 'billingLastName',
- 'billingAddress',
- 'billingState',
- 'billingPostcode',
- 'billingCity',
- 'billingPhoneNumber'
- ]
- });
- useEffect(() => {
- if (billingSameAsShipping) {
- clearErrors([
- "billingEmail",
- "billingFirstName",
- "billingLastName",
- "billingCompanyName",
- "billingAddress",
- "billingCountry",
- "billingState",
- "billingCity",
- "billingPostcode",
- "billingPhoneNumber",
- ]);
- }
- }, [billingSameAsShipping, clearErrors]);
- /*
- const shippingFields = useWatch({
- control,
- name: [
- "shippingEmail",
- "shippingFirstName",
- "shippingLastName",
- "shippingCompanyName",
- "shippingAddress",
- "shippingCountry",
- "shippingState",
- "shippingCity",
- "shippingPostcode",
- "shippingPhoneNumber",
- ],
- });
- useEffect(() => {
- if (!billingSameAsShipping) return;
- const [
- shippingEmail,
- shippingFirstName,
- shippingLastName,
- shippingCompanyName,
- shippingAddress,
- shippingCountry,
- shippingState,
- shippingCity,
- shippingPostcode,
- shippingPhoneNumber,
- ] = shippingFields;
- setValue("billingEmail", shippingEmail);
- setValue("billingFirstName", shippingFirstName);
- setValue("billingLastName", shippingLastName);
- setValue("billingCompanyName", shippingCompanyName);
- setValue("billingAddress", shippingAddress);
- setValue("billingCountry", shippingCountry);
- setValue("billingCity", shippingCity);
- setValue("billingPostcode", shippingPostcode);
- setValue("billingPhoneNumber", shippingPhoneNumber);
- setValue("billingState", shippingState,{shouldValidate: true}); // 必须触发校验,因为使用浏览器自动填充的时候,可能导致billingState校验不通过,导致无法下单
- }, [billingSameAsShipping, shippingFields, setValue]);
- */
-
- const selectedCountry = useMemo(() => {
- return countries.find( (country) => country.code === selectedCountryCode)
- }, [countries, selectedCountryCode]);
- const { data: statesData, loading: statesLoading } = useQuery(GET_COUNTRY_STATES, {
- variables: {
- countryId: selectedCountry?._id
- },
- skip: !selectedCountryCode,
- });
- let statesOptions: { value: string; label: string, id: string }[] = [];
- if(statesData) {
- statesOptions = statesData.countryStates.map((state) => ({
- value: state.code,
- label: state.defaultName,
- id: String(state._id),
- }));
- }
- const [showDropDown, setShowDropDown] = useState(false);
- // 是否用户手动选择过区号
- const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] = useState(false);
- // 选择国家时同步电话区号
- useEffect(() => {
- const preBillingPhoneNum = getValues('billingPhoneNumber');
- const regPhoneCode = IS_VALID_PHONECODE;
-
- if(preBillingPhoneNum && !hasUserSelectedPhoneCode) {
- const newPhoneCode = getPhoneCodeBycountryCode(selectedCountryCode);
- const codePart = regPhoneCode.exec(preBillingPhoneNum);
- let phone = preBillingPhoneNum;
- if(codePart) {
- phone = preBillingPhoneNum.replace(codePart[0], '');
- }
- if(codePart && newPhoneCode !== codePart[0].trim()) {
- return;
- }
- setValue('billingPhoneNumber',formatFinalPhoneValue(newPhoneCode, phone))
- }
-
- },[selectedCountryCode,hasUserSelectedPhoneCode,getValues,setValue]);
- const showMyAddressList = () => {
- setShowDropDown(true);
- };
- const closeMyaddressList = useCallback(() => {
- setShowDropDown(false);
- },[]);
- // 从地址列表选择地址
- const selectAddress = (param: CustomerAddressItem | 'edit') => {
- if(param === 'edit') {
- if(!showFormField) onShowFormFieldChange(true);
- } else {
- setValue('billingFirstName',param.firstName);
- setValue('billingLastName',param.lastName);
- setValue('billingAddress',param.address);
- setValue('billingCountry',param.country);
- setValue('billingState',param.state);
- setValue('billingPostcode',param.postcode);
- setValue('billingCity',param.city);
- setValue('billingPhoneNumber',param.phone);
- }
- closeMyaddressList();
- };
-
- return (<>
- <section className="w-full mt-4 pb-8">
- <div className="box-border w-full p-3 bg-ly-lightgray">
- <div className="flex justify-between items-center">
- <span className="text-ly-12 flex-none">Billing Address</span>
- <label className="group relative w-9 h-5.5 flex-none">
- <input className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
- type="checkbox"
- {...register("billingSameAsShipping")}
- />
- <span className="transition-all box-border block px-0.5 w-full h-full rounded-full flex items-center bg-[#c2c2c2] group-has-[:checked]:bg-[#363636]">
- <span className="transition-all block w-4.5 h-4.5 rounded-full bg-white group-has-[:checked]:ml-3.5"></span>
- </span>
- </label>
-
- </div>
- <p className="text-ly-12 mt-1.25">Same As Shipping Address</p>
- </div>
- <div className={clsx("w-full mt-4",{
- "hidden": billingSameAsShipping,
- })}>
- {noAddress ?
- <div onClick={showMyAddressList}
- className="box-border w-full h-9 justify-between flex items-center px-4 relative bg-[url(/image/address-bg.webp)] bg-position-[0_-70%] bg-size-[100%_auto]"
- >
- <span className="text-ly-12">+New Shipping Address</span>
- {!isGuest &&
- <button className="flex-none" title="change address" type="button">
- <svg className="w-4 h-4" 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="16" y="0" width="16" height="16" transform="rotate(90 16 0)" fill="#FFFFFF" fillOpacity="0"></rect><path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square" d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
- </button>
- }
- </div>
- :
- <div onClick={showMyAddressList}
- className="w-full box-border px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]"
- >
- <div className="border-b-1 flex h-11.25 items-center justify-between">
- <p className="text-ly-13 font-medium">
- {firstName} {lastName}
- <span className="border-r border-ly-gray h-3.5 mx-2"></span>
- {phoneNumber}
- </p>
- {!isGuest &&
- <svg className="w-4 h-4" 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="16" y="0" width="16" height="16" transform="rotate(90 16 0)" fill="#FFFFFF" fillOpacity="0"></rect><path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square" d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
- }
- </div>
- <div className="flex items-center h-10">
- <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
- <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
- <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" ></path>
- <path d="M11.9983 11.3653C13.1666 11.3653 14.1138 10.4181 14.1138 9.24979C14.1138 8.08144 13.1666 7.13428 11.9983 7.13428C10.83 7.13428 9.88281 8.08144 9.88281 9.24979C9.88281 10.4181 10.83 11.3653 11.9983 11.3653Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" ></path>
- </svg>
- <span className="text-ly-12 ml-2.5">
- {streetAddress},
- {city},
- {stateProvince},
- {postCode},
- {selectedCountryCode}
- </span>
- </div>
- </div>
- }
- <LyDropDown
- showDropDown={showDropDown}
- onClose={closeMyaddressList}
- >
- <div className="w-full p-3 text-ly-13 font-medium" key={'edit'} onClick={() => {selectAddress('edit')}}>
- Edit Address
- </div>
- {customerAddressList.map((item) => {
- return (
- <div className="w-full p-3" key={item.id} onClick={() => selectAddress(item)}>
-
- <p className="text-ly-13 leading-4">
- {item.firstName} {item.lastName},
- {item.address},
- {item.city},
- {item.state},
- {item.postcode},
- {item.country},
- {item.phone}
- </p>
- </div>
- );
- })}
-
- </LyDropDown>
-
- </div>
- <div className={clsx("box-border w-full mt-4",{
- "hidden": (!showFormField && !noAddress) || billingSameAsShipping
- })}
- >
- <div className={clsx("w-full",{
- "hidden": !isGuest
- })}>
- <label className="text-ly-12 block mb-4 font-semibold">
- Email *
- </label>
- <InputText
- type="email"
- placeholder="Enter your email address"
- {...register("billingEmail", {
- required: !billingSameAsShipping ? "Email is required" : false,
- pattern: {
- value: EMAIL_REGEX,
- message: "Please enter a valid email address",
- }
- })}
- error={errors.billingEmail && errors.billingEmail.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">
- Name *
- </label>
- <div className="w-full flex justify-between">
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="First name"
- {...register("billingFirstName", {
- required: !billingSameAsShipping ? "First name is required" : false,
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid First Name",
- }
- })}
- error={errors.billingFirstName && errors.billingFirstName.message as string}
- />
- </div>
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="Last name"
- {...register("billingLastName", {
- required: !billingSameAsShipping ? "Last name is required" : false,
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid Last Name",
- }
- })}
- error={errors.billingLastName && errors.billingLastName.message as string}
- />
- </div>
- </div>
- </div>
-
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Address *</label>
- <InputText
- type="text"
- placeholder="Please Input Your Detailed Address With Apt. No"
- {...register("billingAddress", {
- required: !billingSameAsShipping ? "Address is required" : false,
- })}
- error={errors.billingAddress && errors.billingAddress.message as string}
- />
-
- </div>
-
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Country and State/Province *</label>
- <div className="w-full">
- <div className="w-full flex justify-between">
- <div className="w-41.25 flex-none">
- <Select placeholder="Country/Region"
- {...register("billingCountry", {
- required: !billingSameAsShipping ? "Country/Region field is required" : false,
- onChange: () => {
- setValue('billingState','',{
- shouldDirty: true,
- shouldValidate: true,
- });
- setValue('billingCity','',{
- shouldDirty: true,
- shouldValidate: true,
- });
- }
- })}
- options={countriesOptions}
- error={errors.billingCountry && errors.billingCountry.message as string}
- />
- </div>
- <div className="w-41.25 flex-none relative">
- {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
- {statesOptions.length > 0 ?
- (
- <Select placeholder="State/Province"
- {...register("billingState", {
- required: !billingSameAsShipping ? "State/Province field is required" : false
- })}
- options={statesOptions}
- error={errors.billingState && errors.billingState.message as string}
- />
- )
- : (
- <InputText
- type="text"
- placeholder="State/Province"
- {...register("billingState", {
- required: !billingSameAsShipping ? "State/Province field is required" : false
- })}
- error={errors.billingState && errors.billingState.message as string}
- />
- )}
-
- </div>
-
- </div>
- <div className="w-full flex justify-between mt-4">
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="Zip/Postal Code"
- {...register("billingPostcode", {
- required: !billingSameAsShipping ? "Postcode field is required" : false,
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid Postcode",
- },
- })}
- error={errors.billingPostcode&& errors.billingPostcode.message as string}
- />
- </div>
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="City"
- {...register("billingCity", {
- required: !billingSameAsShipping ? "City field is required" : false,
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid City",
- },
- })}
- error={errors.billingCity && errors.billingCity.message as string}
- />
- </div>
- </div>
-
- </div>
- </div>
-
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Phone number *</label>
- <Controller
- name="billingPhoneNumber"
- control={control}
- rules={{
- required: !billingSameAsShipping ? "Phone Number is required" : false,
- pattern: {
- value: IS_VALID_FULL_PHONE,
- message: "Invalid Phone Number",
- }
- }}
- render={({ field, fieldState }) => {
- return (
- <PhoneNumberInput
- placeholder="Please Input Your Phone Number"
- value={field.value}
- onChange={field.onChange}
- onBlur={field.onBlur}
- name={field.name}
- ref={field.ref}
- error={fieldState.error?.message}
- countryCode={selectedCountryCode}
- hasUserSelectedPhoneCode={hasUserSelectedPhoneCode}
- onHasUserSelectedPhoneCode={() => setHasUserSelectedPhoneCode(true)}
- />
- );
- }}
- />
-
-
- </div>
-
- </div>
- </section>
- </>);
- }
|