diff --git a/packages/core/src/app/address/AddressForm.tsx b/packages/core/src/app/address/AddressForm.tsx index 4df1e4e1c1..c4ad5d8251 100644 --- a/packages/core/src/app/address/AddressForm.tsx +++ b/packages/core/src/app/address/AddressForm.tsx @@ -93,13 +93,16 @@ const AddressForm: React.FC = ({ } setFieldValue(fieldName, value as string); - onChange(fieldName, value as string); + // onChange(fieldName, value as string); }); const address1 = address.address1 ? address.address1 : autocompleteValue; - - if (address1) { - syncNonFormikValue(AUTOCOMPLETE_FIELD_NAME, address1); + + // if (address1) { + // syncNonFormikValue(AUTOCOMPLETE_FIELD_NAME, address1); + // } + if (address1 && fieldName === AUTOCOMPLETE_FIELD_NAME) { + setFieldValue(fieldName, address1); } }, [countries, setFieldValue, onChange, syncNonFormikValue]); diff --git a/packages/core/src/app/shipping/ShippingForm.tsx b/packages/core/src/app/shipping/ShippingForm.tsx index 6854126934..0b76e33cae 100644 --- a/packages/core/src/app/shipping/ShippingForm.tsx +++ b/packages/core/src/app/shipping/ShippingForm.tsx @@ -49,7 +49,6 @@ const ShippingForm = ({ methodId, shouldShowOrderComments, shippingAddress, - signOut, updateShippingAddress: updateAddress } = useShipping(); const { extensionState: { shippingFormRenderTimestamp } } = useExtensions(); @@ -94,7 +93,6 @@ const ShippingForm = ({ isBillingSameAsShipping={isBillingSameAsShipping} isInitialValueLoaded={isInitialValueLoaded} isLoading={isLoading} - isMultiShippingMode={isMultiShippingMode} isShippingStepPending={isShippingStepPending} methodId={methodId} onSubmit={onSingleShippingSubmit} @@ -102,7 +100,6 @@ const ShippingForm = ({ shippingAddress={shippingAddress} shippingFormRenderTimestamp={shippingFormRenderTimestamp} shouldShowOrderComments={shouldShowOrderComments} - signOut={signOut} updateAddress={updateAddress} /> ); diff --git a/packages/core/src/app/shipping/SingleShippingForm.tsx b/packages/core/src/app/shipping/SingleShippingForm.tsx index 70ab2abe4d..d65209f241 100644 --- a/packages/core/src/app/shipping/SingleShippingForm.tsx +++ b/packages/core/src/app/shipping/SingleShippingForm.tsx @@ -3,7 +3,6 @@ import { type CheckoutParams, type CheckoutSelectors, type Consignment, - type CustomerRequestOptions, type FormField, type RequestOptions, type ShippingInitializeOptions, @@ -11,11 +10,10 @@ import { } from '@bigcommerce/checkout-sdk'; import { type FormikProps } from 'formik'; import { debounce, isEqual, noop } from 'lodash'; -import React, { PureComponent, type ReactNode } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { lazy, object } from 'yup'; import { withLanguage, type WithLanguageProps } from '@bigcommerce/checkout/locale'; -import { FormContext } from '@bigcommerce/checkout/ui'; import { type AddressFormValues, @@ -44,7 +42,6 @@ export interface SingleShippingFormProps { customerMessage: string; isLoading: boolean; isShippingStepPending: boolean; - isMultiShippingMode: boolean; methodId?: string; shippingAddress?: Address; shippingAutosaveDelay?: number; @@ -57,7 +54,6 @@ export interface SingleShippingFormProps { initialize(options: ShippingInitializeOptions): Promise; onSubmit(values: SingleShippingFormValues): void; onUnhandledError?(error: Error): void; - signOut(options?: CustomerRequestOptions): void; updateAddress( address: Partial
, options?: RequestOptions, @@ -70,12 +66,6 @@ export interface SingleShippingFormValues { orderComment: string; } -interface SingleShippingFormState { - isResettingAddress: boolean; - isUpdatingShippingData: boolean; - hasRequestedShippingOptions: boolean; -} - function shouldHaveCustomValidation(methodId?: string): boolean { const methodIdsWithoutCustomValidation: string[] = [ PaymentMethodId.BraintreeAcceleratedCheckout, @@ -87,27 +77,47 @@ function shouldHaveCustomValidation(methodId?: string): boolean { export const SHIPPING_AUTOSAVE_DELAY = 1700; -class SingleShippingForm extends PureComponent< +const SingleShippingForm: React.FC< SingleShippingFormProps & WithLanguageProps & FormikProps -> { - static contextType = FormContext; - - state: SingleShippingFormState = { - isResettingAddress: false, - isUpdatingShippingData: false, - hasRequestedShippingOptions: false, - }; - - private debouncedUpdateAddress: any; - - constructor( - props: SingleShippingFormProps & WithLanguageProps & FormikProps, - ) { - super(props); - - const { updateAddress } = this.props; - - this.debouncedUpdateAddress = debounce( +> = ({ + cartHasChanged, + consignments, + customerMessage, + deinitialize, + deleteConsignments, + getFields, + initialize, + isBillingSameAsShipping, + isInitialValueLoaded, + isLoading, + isShippingStepPending, + isValid, + validateForm, + methodId, + onUnhandledError = noop, + setFieldValue, + setValues, + shippingAddress, + shippingAutosaveDelay = SHIPPING_AUTOSAVE_DELAY, + shippingFormRenderTimestamp, + shouldShowOrderComments, + updateAddress, + values, + }) => { + const [isResettingAddress, setIsResettingAddress] = useState(false); + const [isUpdatingShippingData, setIsUpdatingShippingData] = useState(false); + const [hasRequestedShippingOptions, setHasRequestedShippingOptions] = useState(false); + + const stateOrProvinceCodeFormField = useMemo(() => { + return getFields( + values.shippingAddress?.countryCode, + ).find(({ name }) => name === 'stateOrProvinceCode'); + }, [getFields, values.shippingAddress?.countryCode]); + + const debouncedUpdateAddressRef = useRef(); + + useEffect(() => { + debouncedUpdateAddressRef.current = debounce( async (address: Address, includeShippingOptions: boolean) => { try { await updateAddress(address, { @@ -119,32 +129,21 @@ class SingleShippingForm extends PureComponent< }); if (includeShippingOptions) { - this.setState({ hasRequestedShippingOptions: true }); + setHasRequestedShippingOptions(true); } } finally { - this.setState({ isUpdatingShippingData: false }); + setIsUpdatingShippingData(false); } }, - props.shippingAutosaveDelay ?? SHIPPING_AUTOSAVE_DELAY, - ); - } - - componentDidUpdate({ shippingFormRenderTimestamp }: SingleShippingFormProps) { - const { - shippingFormRenderTimestamp: newShippingFormRenderTimestamp, - setValues, - getFields, - shippingAddress, - isBillingSameAsShipping, - customerMessage, - values, - setFieldValue, - } = this.props; - - const stateOrProvinceCodeFormField = getFields(values && values.shippingAddress?.countryCode).find( - ({ name }) => name === 'stateOrProvinceCode', + shippingAutosaveDelay, ); + + return () => { + debouncedUpdateAddressRef.current?.cancel(); + }; + }, []); + useEffect(() => { // Workaround for a bug found during manual testing: // When the shipping step first loads, the `stateOrProvinceCode` field may not be there. // It later appears with an empty value if the selected country has states/provinces. @@ -156,194 +155,157 @@ class SingleShippingForm extends PureComponent< ) { setFieldValue('shippingAddress.stateOrProvinceCode', shippingAddress.stateOrProvinceCode); } + }, [ + stateOrProvinceCodeFormField, + shippingAddress?.stateOrProvinceCode, + values.shippingAddress?.stateOrProvinceCode, + ]); - // This is for executing extension command, `ReRenderShippingForm`. - if (newShippingFormRenderTimestamp !== shippingFormRenderTimestamp) { + + useEffect(() => { + if (shippingFormRenderTimestamp) { setValues({ billingSameAsShipping: isBillingSameAsShipping, orderComment: customerMessage, shippingAddress: mapAddressToFormValues( - getFields(shippingAddress && shippingAddress.countryCode), + getFields(shippingAddress?.countryCode), shippingAddress, ), }); } - } + }, [shippingFormRenderTimestamp]); - render(): ReactNode { - const { - cartHasChanged, - isInitialValueLoaded, - isLoading, - onUnhandledError, - methodId, - shippingAddress, - consignments, - shouldShowOrderComments, - initialize, - isValid, - deinitialize, - values: { shippingAddress: addressForm }, - isShippingStepPending, - shippingFormRenderTimestamp, - } = this.props; - - const { isResettingAddress, isUpdatingShippingData, hasRequestedShippingOptions } = - this.state; - - const PAYMENT_METHOD_VALID = ['amazonpay']; - const shouldShowBillingSameAsShipping = !PAYMENT_METHOD_VALID.some( - (method) => method === methodId, - ); + const updateAddressWithFormData = (includeShippingOptions: boolean) => { + const updatedShippingAddress = + values.shippingAddress && mapAddressFromFormValues(values.shippingAddress); - return ( -
-
- - {shouldShowBillingSameAsShipping && ( -
- -
- )} -
- - - - ); - } + let newIncludeShippingOptions = includeShippingOptions; - private shouldDisableSubmit: () => boolean = () => { - const { isLoading, consignments, isValid } = this.props; - - const { isUpdatingShippingData } = this.state; + if (Array.isArray(shippingAddress?.customFields)) { + newIncludeShippingOptions = + !isEqual(shippingAddress?.customFields, updatedShippingAddress?.customFields) || + includeShippingOptions; + } - if (!isValid) { - return false; + if (!updatedShippingAddress || isEqualAddress(updatedShippingAddress, shippingAddress)) { + return; } - return isLoading || isUpdatingShippingData || !hasSelectedShippingOptions(consignments) || !isSelectedShippingOptionValid(consignments); + setIsUpdatingShippingData(true); + debouncedUpdateAddressRef.current?.(updatedShippingAddress, newIncludeShippingOptions); }; - private handleFieldChange: (name: string) => void = async (name) => { - const { setFieldValue } = this.props; - + const handleFieldChange = async (name: string) => { if (name === 'countryCode') { setFieldValue('shippingAddress.stateOrProvince', ''); setFieldValue('shippingAddress.stateOrProvinceCode', ''); } - // Enqueue the following code to run after Formik has run validation await new Promise((resolve) => setTimeout(resolve)); - const isShippingField = SHIPPING_ADDRESS_FIELDS.includes(name); - - const { hasRequestedShippingOptions } = this.state; - - const { isValid } = this.props; + const errors = await validateForm(); + const addressErrors = errors.shippingAddress; - if (!isValid) { - return; - } - - this.updateAddressWithFormData(isShippingField || !hasRequestedShippingOptions); - }; - - private updateAddressWithFormData(includeShippingOptions: boolean) { - const { - shippingAddress, - values: { shippingAddress: addressForm }, - } = this.props; - - const updatedShippingAddress = addressForm && mapAddressFromFormValues(addressForm); - - if (Array.isArray(shippingAddress?.customFields)) { - includeShippingOptions = !isEqual( - shippingAddress?.customFields, - updatedShippingAddress?.customFields - ) || includeShippingOptions; - } - - if (!updatedShippingAddress || isEqualAddress(updatedShippingAddress, shippingAddress)) { + // Only update address if there are no address errors + if (addressErrors && Object.keys(addressErrors).length > 0) { return; } - this.setState({ isUpdatingShippingData: true }); - this.debouncedUpdateAddress(updatedShippingAddress, includeShippingOptions); - } + const isShippingField = SHIPPING_ADDRESS_FIELDS.includes(name); - private handleAddressSelect: (address: Address) => void = async (address) => { - const { updateAddress, onUnhandledError = noop, values, setValues } = this.props; + updateAddressWithFormData(isShippingField || !hasRequestedShippingOptions); + }; - this.setState({ isResettingAddress: true }); + const handleAddressSelect = async (address: Address) => { + setIsResettingAddress(true); try { await updateAddress(address); setValues({ ...values, - shippingAddress: mapAddressToFormValues( - this.getFields(address.countryCode), - address, - ), + shippingAddress: mapAddressToFormValues(getFields(address.countryCode), address), }); } catch (error) { onUnhandledError(error); } finally { - this.setState({ isResettingAddress: false }); + setIsResettingAddress(false); } - }; - - private onUseNewAddress: () => void = async () => { - const { deleteConsignments, onUnhandledError = noop, setValues, values } = this.props; + } - this.setState({ isResettingAddress: true }); + const handleUseNewAddress = async () => { + setIsResettingAddress(true); try { const address = await deleteConsignments(); setValues({ ...values, - shippingAddress: mapAddressToFormValues( - this.getFields(address && address.countryCode), - address, - ), + shippingAddress: mapAddressToFormValues(getFields(address?.countryCode), address), }); - } catch (e) { - onUnhandledError(e); + } catch (error) { + onUnhandledError(error); } finally { - this.setState({ isResettingAddress: false }); + setIsResettingAddress(false); } }; - private getFields(countryCode: string | undefined): FormField[] { - const { getFields } = this.props; + const shouldDisableSubmit = () => { + if (!isValid) { + return false; + } - return getFields(countryCode); - } -} + return ( + isLoading || + isUpdatingShippingData || + !hasSelectedShippingOptions(consignments) || + !isSelectedShippingOptionValid(consignments) + ); + }; + + const PAYMENT_METHOD_VALID = ['amazonpay']; + const shouldShowBillingSameAsShipping = !PAYMENT_METHOD_VALID.some( + (method) => method === methodId, + ); + + return ( +
+
+ + {shouldShowBillingSameAsShipping && ( +
+ +
+ )} +
+ + + + ); +}; export default withLanguage( withFormikExtended({ @@ -359,7 +321,7 @@ export default withLanguage( billingSameAsShipping: isBillingSameAsShipping, orderComment: customerMessage, shippingAddress: mapAddressToFormValues( - getFields(shippingAddress && shippingAddress.countryCode), + getFields(shippingAddress?.countryCode), shippingAddress, ), }), @@ -379,7 +341,7 @@ export default withLanguage( shippingAddress: lazy>((formValues) => getCustomFormFieldsValidationSchema({ translate: getTranslateAddressError(language), - formFields: getFields(formValues && formValues.countryCode), + formFields: getFields(formValues?.countryCode), }), ), }) @@ -387,7 +349,7 @@ export default withLanguage( shippingAddress: lazy>((formValues) => getAddressFormFieldsValidationSchema({ language, - formFields: getFields(formValues && formValues.countryCode), + formFields: getFields(formValues?.countryCode), }), ), }),