Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/trac-291-state-optional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Fix Account Registration validating State/Province as required for countries without any states (e.g., Algeria). The register page now queries per-country state data from BigCommerce and hides the State/Province field entirely when the selected country has no states.
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ function parseRegisterCustomerInput(
}

export async function registerCustomer<F extends Field>(
{ fields, passwordComplexity }: DynamicFormActionArgs<F>,
{ fields, passwordComplexity, countriesWithoutStates }: DynamicFormActionArgs<F>,
_prevState: {
lastResult: SubmissionResult | null;
},
Expand All @@ -350,8 +350,11 @@ export async function registerCustomer<F extends Field>(
const locale = await getLocale();
const cartId = await getCartId();

const countryCode = formData.get('countryCode');
const currentCountry = typeof countryCode === 'string' ? countryCode : undefined;

const submission = parseWithZod(formData, {
schema: schema(fields, passwordComplexity),
schema: schema(fields, passwordComplexity, undefined, countriesWithoutStates, currentCountry),
});

if (submission.status !== 'success') {
Expand Down
3 changes: 3 additions & 0 deletions core/app/[locale]/(default)/(auth)/register/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const RegisterCustomerQuery = graphql(
countries {
code
name
statesOrProvinces {
entityId
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions core/app/[locale]/(default)/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export default async function Register({ params }: Props) {
const { addressFields, customerFields, countries, passwordComplexitySettings } =
registerCustomerData;

const countriesWithoutStates = (countries ?? [])
.filter((country) => country.statesOrProvinces.length === 0)
.map((country) => country.code);

const recaptchaSiteKey = await getRecaptchaSiteKey();

const fields = transformFieldsToLayout(
Expand Down Expand Up @@ -112,6 +116,7 @@ export default async function Register({ params }: Props) {
return (
<DynamicFormSection
action={registerCustomer}
countriesWithoutStates={countriesWithoutStates}
errorTranslations={{
firstName: {
invalid_type: t('FieldErrors.firstNameRequired'),
Expand Down
37 changes: 35 additions & 2 deletions core/vibes/soul/form/dynamic-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
startTransition,
useActionState,
useEffect,
useState,
} from 'react';
import { useFormStatus } from 'react-dom';
import RecaptchaWidget from 'react-google-recaptcha';
Expand Down Expand Up @@ -50,6 +51,7 @@ import { removeOptionsFromFields } from './utils';
export interface DynamicFormActionArgs<F extends Field> {
fields: Array<F | FieldGroup<F>>;
passwordComplexity?: PasswordComplexitySettings | null;
countriesWithoutStates?: string[];
}

type Action<F extends Field, S, P> = (
Expand Down Expand Up @@ -79,6 +81,7 @@ export interface DynamicFormProps<F extends Field> {
passwordComplexity?: PasswordComplexitySettings | null;
errorTranslations?: FormErrorTranslationMap;
recaptchaSiteKey?: string;
countriesWithoutStates?: string[];
}

export function DynamicForm<F extends Field>({
Expand All @@ -95,19 +98,32 @@ export function DynamicForm<F extends Field>({
passwordComplexity,
errorTranslations,
recaptchaSiteKey,
countriesWithoutStates,
}: DynamicFormProps<F>) {
const t = useTranslations('Form');
// Remove options from fields before passing to action to reduce payload size
// Options are only needed for rendering, not for processing form submissions
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const fieldsWithoutOptions = removeOptionsFromFields(fields) as Array<F | FieldGroup<F>>;
const actionWithFields = action.bind(null, { fields: fieldsWithoutOptions, passwordComplexity });
const actionWithFields = action.bind(null, {
fields: fieldsWithoutOptions,
passwordComplexity,
countriesWithoutStates,
});

const [{ lastResult, successMessage }, formAction] = useActionState(actionWithFields, {
lastResult: null,
});

const dynamicSchema = schema(fields, passwordComplexity, errorTranslations);
const [currentCountry, setCurrentCountry] = useState<string | undefined>(undefined);

const dynamicSchema = schema(
fields,
passwordComplexity,
errorTranslations,
countriesWithoutStates,
currentCountry,
);
const defaultValue = fields
.flatMap((f) => (Array.isArray(f) ? f : [f]))
.reduce<z.infer<typeof dynamicSchema>>(
Expand Down Expand Up @@ -163,6 +179,19 @@ export function DynamicForm<F extends Field>({
}
}, [lastResult, successMessage, onSuccess]);

const countryValue = formFields.countryCode?.value;

useEffect(() => {
setCurrentCountry(typeof countryValue === 'string' ? countryValue : undefined);
}, [countryValue]);

const isCurrentCountryStateless =
countriesWithoutStates != null &&
typeof countryValue === 'string' &&
countriesWithoutStates.includes(countryValue);
const shouldHideStateField = (fieldName: string) =>
fieldName === 'stateOrProvince' && isCurrentCountryStateless;

return (
<FormProvider context={form.context}>
<form {...getFormProps(form)} action={formAction} onChange={onChange}>
Expand All @@ -172,6 +201,8 @@ export function DynamicForm<F extends Field>({
return (
<div className="flex flex-col gap-4 @sm:flex-row" key={index}>
{field.map((f) => {
if (shouldHideStateField(f.name)) return null;

const groupFormField = formFields[f.name];

if (!groupFormField) return null;
Expand All @@ -188,6 +219,8 @@ export function DynamicForm<F extends Field>({
);
}

if (shouldHideStateField(field.name)) return null;

const formField = formFields[field.name];

if (formField == null) return null;
Expand Down
15 changes: 15 additions & 0 deletions core/vibes/soul/form/dynamic-form/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,26 @@ export function schema(
fields: Array<Field | FieldGroup<Field>>,
passwordComplexity?: PasswordComplexitySettings | null,
errorTranslations?: FormErrorTranslationMap,
countriesWithoutStates?: string[],
currentCountry?: string,
) {
const shape = getFieldsShape(fields, passwordComplexity, errorTranslations);
const flatFields = fields.flatMap((field) => (Array.isArray(field) ? field : [field]));
const passwordFieldName = flatFields.find((f) => f.type === 'password')?.name;
const confirmPasswordFieldName = flatFields.find((f) => f.type === 'confirm-password')?.name;
const stateFieldName = flatFields.find((f) => f.name === 'stateOrProvince')?.name;
const countryIsStateless =
countriesWithoutStates != null &&
currentCountry != null &&
countriesWithoutStates.includes(currentCountry);

if (countryIsStateless && stateFieldName != null) {
const existing = shape[stateFieldName];

if (existing instanceof z.ZodString) {
shape[stateFieldName] = existing.optional();
}
}

return z.object(shape).superRefine((data, ctx) => {
if (
Expand Down
3 changes: 3 additions & 0 deletions core/vibes/soul/sections/dynamic-form-section/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface Props<F extends Field> {
passwordComplexity?: PasswordComplexitySettings | null;
errorTranslations?: FormErrorTranslationMap;
recaptchaSiteKey?: string;
countriesWithoutStates?: string[];
}

export function DynamicFormSection<F extends Field>({
Expand All @@ -31,6 +32,7 @@ export function DynamicFormSection<F extends Field>({
passwordComplexity,
errorTranslations,
recaptchaSiteKey,
countriesWithoutStates,
}: Props<F>) {
return (
<SectionLayout className={clsx('mx-auto w-full max-w-4xl', className)} containerSize="lg">
Expand All @@ -46,6 +48,7 @@ export function DynamicFormSection<F extends Field>({
)}
<DynamicForm
action={action}
countriesWithoutStates={countriesWithoutStates}
errorTranslations={errorTranslations}
fields={fields}
passwordComplexity={passwordComplexity}
Expand Down
Loading