diff --git a/changelog/8282-fix-phone-validation-custom-fields.yaml b/changelog/8282-fix-phone-validation-custom-fields.yaml new file mode 100644 index 00000000000..19c167b267f --- /dev/null +++ b/changelog/8282-fix-phone-validation-custom-fields.yaml @@ -0,0 +1,4 @@ +type: Fixed +description: Fixed custom privacy request fields named "phone" or "email" incorrectly receiving E.164/email format validation when those identity inputs were not configured; also improved phone validation error message to specify expected format +pr: 8282 +labels: [] diff --git a/clients/admin-ui/src/features/privacy-requests/form/helpers.ts b/clients/admin-ui/src/features/privacy-requests/form/helpers.ts index 6cfee6b93b8..9da0ac34a1f 100644 --- a/clients/admin-ui/src/features/privacy-requests/form/helpers.ts +++ b/clients/admin-ui/src/features/privacy-requests/form/helpers.ts @@ -35,7 +35,7 @@ export const generateFormRulesFromAction = ( const phonePattern = /^\+?[1-9]\d{1,14}$/; const phoneMessage = - "Phone number must be formatted correctly (e.g. 15555555555)"; + "Phone must be in E.164 format (e.g. +15551234567 or 15551234567)"; if (action.identity_inputs?.phone === "required") { rules["identity.phone_number"] = [ diff --git a/clients/privacy-center/components/modals/consent-request-modal/useConsentRequestForm.ts b/clients/privacy-center/components/modals/consent-request-modal/useConsentRequestForm.ts index a66cc800a10..ac95923f5f8 100644 --- a/clients/privacy-center/components/modals/consent-request-modal/useConsentRequestForm.ts +++ b/clients/privacy-center/components/modals/consent-request-modal/useConsentRequestForm.ts @@ -64,33 +64,42 @@ const useConsentRequestForm = ({ const initialValues = useMemo(() => getInitialValues(), [getInitialValues]); - // Build the static portion of the validation schema (identity fields) - const identityValidationSchema = useMemo( - () => - Yup.object().shape({ - email: emailValidation(identityInputs?.email!).test( - "one of email or phone entered", - "You must enter an email", - (_value, context) => { - if (identityInputs?.email === "required") { - return Boolean(context.parent.email); - } - return true; - }, - ), - phone: phoneValidation(identityInputs?.phone!).test( - "one of email or phone entered", - "You must enter a phone number", - (_value, context) => { - if (identityInputs?.phone === "required") { - return Boolean(context.parent.phone); - } - return true; - }, - ), - }), - [identityInputs?.email, identityInputs?.phone], - ); + // Build the static portion of the validation schema (identity fields). + // Only include validation rules for identity fields that are actually configured, + // to avoid conflicts with custom_privacy_request_fields that may use the same keys. + const identityValidationSchema = useMemo(() => { + const schemaFields: Record = {}; + + // Only add email validation if email is configured in identity_inputs + if (identityInputs?.email) { + schemaFields.email = emailValidation(identityInputs.email).test( + "one of email or phone entered", + "You must enter an email", + (_value, context) => { + if (identityInputs.email === "required") { + return Boolean(context.parent.email); + } + return true; + }, + ); + } + + // Only add phone validation if phone is configured in identity_inputs + if (identityInputs?.phone) { + schemaFields.phone = phoneValidation(identityInputs.phone).test( + "one of email or phone entered", + "You must enter a phone number", + (_value, context) => { + if (identityInputs.phone === "required") { + return Boolean(context.parent.phone); + } + return true; + }, + ); + } + + return Yup.object().shape(schemaFields); + }, [identityInputs?.email, identityInputs?.phone]); const { validate, applicableFieldsRef, validationError } = useConditionalValidate({ diff --git a/clients/privacy-center/components/modals/privacy-request-modal/usePrivacyRequestForm.ts b/clients/privacy-center/components/modals/privacy-request-modal/usePrivacyRequestForm.ts index ea20b228fd8..fa4b56f2b99 100644 --- a/clients/privacy-center/components/modals/privacy-request-modal/usePrivacyRequestForm.ts +++ b/clients/privacy-center/components/modals/privacy-request-modal/usePrivacyRequestForm.ts @@ -108,54 +108,63 @@ const usePrivacyRequestForm = ({ const initialValues = useMemo(() => getInitialValues(), [getInitialValues]); - // Build the static portion of the validation schema (identity fields) - const identityValidationSchema = useMemo( - () => - Yup.object().shape({ - name: nameValidation(nameInput), - email: emailValidation(emailInput).test( - "one of email or phone entered", - "You must enter either email or phone", - (_value, context) => { - if (emailInput === "optional" && phoneInput === "optional") { - return Boolean(context.parent.phone || context.parent.email); - } - return true; - }, - ), - phone: phoneValidation(phoneInput).test( - "one of email or phone entered", - "You must enter either email or phone", - (_value, context) => { - if (emailInput === "optional" && phoneInput === "optional") { - return Boolean(context.parent.phone || context.parent.email); - } - return true; - }, - ), - ...Object.fromEntries( - Object.entries(customIdentityFields).flatMap(([key, value]) => { - if (!value) { - return []; - } - if (value.field_type === "date") { - return [ - [ - key, - dateFieldValidation( - value, - value.label, - value.required !== false, - ), - ], - ]; - } - return [[key, Yup.string().required(`${value.label} is required`)]]; - }), - ), - }), - [emailInput, phoneInput, nameInput, customIdentityFields], - ); + // Build the static portion of the validation schema (identity fields). + // Only include validation rules for identity fields that are actually configured, + // to avoid conflicts with custom_privacy_request_fields that may use the same keys. + const identityValidationSchema = useMemo(() => { + const schemaFields: Record = {}; + + // Only add name validation if name is configured in identity_inputs + if (nameInput) { + schemaFields.name = nameValidation(nameInput); + } + + // Only add email validation if email is configured in identity_inputs + if (emailInput) { + schemaFields.email = emailValidation(emailInput).test( + "one of email or phone entered", + "You must enter either email or phone", + (_value, context) => { + if (emailInput === "optional" && phoneInput === "optional") { + return Boolean(context.parent.phone || context.parent.email); + } + return true; + }, + ); + } + + // Only add phone validation if phone is configured in identity_inputs + if (phoneInput) { + schemaFields.phone = phoneValidation(phoneInput).test( + "one of email or phone entered", + "You must enter either email or phone", + (_value, context) => { + if (emailInput === "optional" && phoneInput === "optional") { + return Boolean(context.parent.phone || context.parent.email); + } + return true; + }, + ); + } + + // Add custom identity field validations + Object.entries(customIdentityFields).forEach(([key, value]) => { + if (!value) { + return; + } + if (value.field_type === "date") { + schemaFields[key] = dateFieldValidation( + value, + value.label, + value.required !== false, + ); + } else { + schemaFields[key] = Yup.string().required(`${value.label} is required`); + } + }); + + return Yup.object().shape(schemaFields); + }, [emailInput, phoneInput, nameInput, customIdentityFields]); const { validate, applicableFieldsRef, validationError } = useConditionalValidate({ diff --git a/clients/privacy-center/components/modals/validation.ts b/clients/privacy-center/components/modals/validation.ts index 69a7904a126..357947e63d0 100644 --- a/clients/privacy-center/components/modals/validation.ts +++ b/clients/privacy-center/components/modals/validation.ts @@ -53,7 +53,7 @@ export const emailValidation = (option?: string | null) => { export const phoneValidation = (option?: string | null) => { // E.164 international standard format let validation = Yup.string().matches(/^\+[1-9]\d{1,14}$/, { - message: "Phone is invalid", + message: "Phone must be in E.164 format (e.g. +15551234567)", excludeEmptyString: true, }); if (option === "required") {