import React from 'react'; import { isEmpty } from 'lodash'; import { Button, Flex, FlexItem, Modal } from '@wordpress/components'; import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { dispatch, useSelect } from '@wordpress/data'; import { Form } from '@woocommerce/components'; import { AddressFields } from './fields'; import { AddressSuggestion } from './suggestion'; import { AddressVerifiedIcon } from '../address-verified-icon'; import { createLocalErrors, isMailAndPhoneRequired, validateCountryAndState, validateDestinationPhone, validateEmail, validatePhone, validatePostalCode, validateRequiredFields, validateEmojiString, } from 'utils'; import { hasOnlyCheckboxChanges } from 'utils/address'; import { AddressContextProvider } from './context'; import { ADDRESS_TYPES } from 'data/constants'; import { AddressTypes, CamelCaseType, Destination, LocationResponse, OriginAddress, } from 'types'; import { addressStore } from 'data/address'; import { withBoundary } from 'components/HOC'; import { AddressDataForm } from './address_data_form'; interface AddressStepProps< T = Destination > { type: AddressTypes; address: T; onCompleteCallback: ( address: T ) => void; onCancelCallback?: () => void; orderId?: string; // order id is only needed when dealing with destination address isAdd: boolean; // if the form is used to add an address originCountry?: string; // origin country is only needed for destination address for validations nextDesign?: boolean; // whether to use the next design for the address step surfaceArea?: string; // telemetry surface area identifier for CIAB tracking clearStoreAddressDriftOnSave?: boolean; } export const AddressStep = withBoundary( < T extends CamelCaseType< LocationResponse > >( { type, address, onCompleteCallback, onCancelCallback, isAdd = false, orderId, originCountry, nextDesign = false, surfaceArea, clearStoreAddressDriftOnSave = false, }: AddressStepProps< T > ) => { const [ isSuggestionModalOpen, setIsSuggestionModalOpen ] = useState( false ); const [ isUpdating, setIsUpdating ] = useState( false ); const [ isConfirming, setIsConfirming ] = useState( false ); const [ isComplete, setIsComplete ] = useState( false ); const [ warningMessage, setWarningMessage ] = useState( __( 'Unvalidated address', 'woocommerce-shipping' ) ); const [ showBypassConfirmation, setShowBypassConfirmation ] = useState( false ); const [ pendingBypassValues, setPendingBypassValues ] = useState< T | null >( null ); // Assert that order id is provided for destination address if ( type === ADDRESS_TYPES.DESTINATION && ! orderId ) { throw new Error( 'Order id is required for destination address' ); } if ( isAdd && type === ADDRESS_TYPES.DESTINATION ) { throw new Error( 'Destination address cannot be added' ); } const validateAddressSection = ( values: T ) => { const validatables = [ { values, errors: createLocalErrors(), type, }, ] .map( ( validatable ) => validateRequiredFields( isMailAndPhoneRequired( { type, originCountry, destinationCountry: validatable.values.country, } ) )< T >( validatable ) ) .map( validateCountryAndState ) .map( validateEmail ) .map( validatePostalCode ) .map( validateEmojiString ) .map( ( validatable ) => { if ( type === ADDRESS_TYPES.DESTINATION && originCountry ) { return validateDestinationPhone( originCountry )( validatable ); } return validatePhone( validatable ); } ); return validatables[ 0 ].errors; }; const validationErrors = useSelect( ( select ) => select( addressStore ).getFormErrors( type ), [ type ] ); const submittedAddress = useSelect( ( select ) => select( addressStore ).getSubmittedAddress( type ), [ type ] ); const isVerified = useSelect( ( select ) => select( addressStore ).getIsAddressVerified( type ), [ type ] ); const normalizedAddress = useSelect( ( select ) => select( addressStore ).getNormalizedAddress( type ), [ type ] ); const needsConfirmation = useSelect( ( select ) => select( addressStore ).getAddressNeedsConfirmation( type ), [ type ] ); const warnings = useSelect( ( select ) => select( addressStore ).getAddressVerifcationWarnings( ADDRESS_TYPES.DESTINATION ), [] ); const normalizeAddress = async ( values: T ) => { setIsUpdating( true ); await dispatch( addressStore ).normalizeAddress( { address: { ...values, address1: values.address ?? '', address2: '', }, }, type ); setIsUpdating( false ); }; const toAddressShape = ( vals: T ) => ( { ...vals, address1: vals.address ?? '', address2: '', } ); const persistAddress = async ( vals: T, verified: boolean ) => { setIsUpdating( true ); try { if ( isAdd ) { await dispatch( addressStore ).addOriginAddress( toAddressShape( vals ) as OriginAddress ); } else { await dispatch( addressStore ).updateShipmentAddress( { orderId: orderId ?? '', address: toAddressShape( vals ), isVerified: verified, clearStoreAddressDrift: type === ADDRESS_TYPES.ORIGIN && clearStoreAddressDriftOnSave, }, type ); } setIsComplete( true ); } finally { setIsUpdating( false ); } }; const handleFormSubmit = async ( values: T ) => { // If only checkbox fields changed and address is already verified, // skip normalization and directly update the address. if ( hasOnlyCheckboxChanges( address, values ) && isVerified ) { await persistAddress( values, true ); return; } // For any actual address field changes, go through normalization. await normalizeAddress( values ); }; const handleSaveWithoutValidation = async ( values: T ) => { setPendingBypassValues( values ); setShowBypassConfirmation( true ); }; const confirmSaveWithoutValidation = async () => { if ( ! pendingBypassValues ) { return; } setShowBypassConfirmation( false ); await persistAddress( pendingBypassValues, true ); setPendingBypassValues( null ); }; const cancelSaveWithoutValidation = () => { setShowBypassConfirmation( false ); setPendingBypassValues( null ); }; const updateAddress = async ( isNormalizedAddress: boolean ) => { setIsUpdating( true ); if ( normalizedAddress && submittedAddress ) { normalizedAddress.email = submittedAddress.email; normalizedAddress.phone = submittedAddress.phone; if ( type === ADDRESS_TYPES.ORIGIN ) { const normalizedOriginAddress = normalizedAddress as OriginAddress; const submittedOriginAddress = submittedAddress as OriginAddress; normalizedOriginAddress.id = submittedOriginAddress.id; normalizedOriginAddress.name = submittedOriginAddress.name; normalizedOriginAddress.company = submittedOriginAddress.company; normalizedOriginAddress.firstName = submittedOriginAddress.firstName; normalizedOriginAddress.lastName = submittedOriginAddress.lastName; normalizedOriginAddress.defaultAddress = submittedOriginAddress.defaultAddress; normalizedOriginAddress.defaultReturnAddress = submittedOriginAddress.defaultReturnAddress; } } const selectedAddress = isNormalizedAddress ? normalizedAddress : submittedAddress; if ( ! selectedAddress ) { // eslint-disable-next-line no-console console.warn( `No address to update for ${ type }, address: ${ selectedAddress }` ); return; } if ( isAdd ) { await dispatch( addressStore ).addOriginAddress( selectedAddress as OriginAddress // Only origin address can be added ); } else { await dispatch( addressStore ).updateShipmentAddress( { orderId: orderId ?? '', address: selectedAddress, isVerified: true, // Either the address is verified or the normalized address is selected clearStoreAddressDrift: type === ADDRESS_TYPES.ORIGIN && clearStoreAddressDriftOnSave, }, type ); } setIsUpdating( false ); setIsSuggestionModalOpen( false ); setIsConfirming( false ); setIsComplete( true ); }; const returnFromSuggestion = () => { setIsSuggestionModalOpen( false ); setIsConfirming( false ); }; useEffect( () => { if ( needsConfirmation && ! isConfirming ) { setIsConfirming( true ); setIsSuggestionModalOpen( true ); dispatch( addressStore ).resetAddressNormalizationResponse( type ); } }, [ needsConfirmation, isConfirming, type ] ); useEffect( () => { if ( isComplete && isEmpty( validationErrors ) ) { setIsComplete( false ); onCompleteCallback( address ); } if ( ! isEmpty( validationErrors ) ) { setIsComplete( false ); } // eslint-disable-next-line react-hooks/exhaustive-deps -- deps omitted to avoid duplicate invocations from parent re-renders. }, [ isComplete, validationErrors ] ); const isSubmitButtonDisabled = ( { isDirty, isValidForm, }: { isValidForm: boolean; isDirty: boolean; } ): boolean => { /** * We should always allow unverified addresses to be submitted. * We allow it so the user can always get feedback about what they need to do next to complete address * verification. */ if ( ! isVerified ) { return false; } /** * Disallow incomplete forms from being submitted since our inline error messages will inform users about * what they need to do as next steps before sending a request. */ if ( ! isValidForm ) { return true; } /** * We should allow the form to be submitted if there are any changes to the form. */ return ! isDirty; }; return (
{ __( 'This address could not be validated. By proceeding, you confirm the address is correct and deliverable. Incorrect addresses may result in delivery issues and are not eligible for refunds.', 'woocommerce-shipping' ) }