Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
Preferences,
FlowMetadataResponse,
} from '@thunderid/browser';
import {FC, ReactElement, ReactNode, useContext, useEffect, useState, useCallback, useRef} from 'react';
import {FC, ReactElement, ReactNode, useContext, useEffect, useMemo, useState, useCallback, useRef} from 'react';
import ComponentRendererContext, {
ComponentRendererMap,
} from '../../../../contexts/ComponentRenderer/ComponentRendererContext';
Expand Down Expand Up @@ -226,7 +226,10 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
[t],
);

const formFields: any = currentFlow?.data?.components ? extractFormFields(currentFlow.data.components) : [];
const formFields: any = useMemo(
() => (currentFlow?.data?.components ? extractFormFields(currentFlow.data.components) : []),
[currentFlow, extractFormFields],
);

const form: any = useForm<Record<string, string>>({
fields: formFields,
Expand All @@ -243,32 +246,34 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
isValid: isFormValid,
setValue: setFormValue,
setTouched: setFormTouched,
setTouchedFields,
setErrors: setFormErrors,
clearErrors: clearFormErrors,
validateForm,
touchAllFields,
reset: resetForm,
} = form;

/**
* Project server-side validation errors from the most recent flow response into the
* form's `errors` state. See BaseSignIn for the same pattern.
*/
// Project server-side fieldErrors from the flow response into form state.
// `setTouchedFields` avoids re-running client-side validation that would wipe
// server errors when the client rules happen to pass.
useEffect(() => {
clearFormErrors();
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setTouchedFields(touched);
setFormErrors(errors);
Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
}, [currentFlow, setFormErrors, setFormTouched, clearFormErrors]);
}, [currentFlow, setFormErrors, setTouchedFields, clearFormErrors]);

const setupFormFields: any = useCallback(
(flowResponse: EmbeddedRecoveryFlowResponse) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import {
FieldError,
FlowMetadataResponse,
Preferences,
buildValidatorFromRules,
} from '@thunderid/browser';
import {FC, useState, useCallback, useContext, ReactElement, ReactNode} from 'react';
import {FC, useEffect, useMemo, useState, useCallback, useContext, ReactElement, ReactNode} from 'react';
import useStyles from './BaseSignIn.styles';
import ComponentRendererContext, {
ComponentRendererMap,
Expand Down Expand Up @@ -248,6 +249,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
children,
additionalData = {},
isTimeoutDisabled = false,
serverFieldErrors = null,
}: BaseSignInProps): ReactElement => {
const {meta} = useThunderID();
const {theme} = useTheme();
Expand Down Expand Up @@ -296,9 +298,12 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
component.type === 'PASSWORD_INPUT' ||
component.type === 'EMAIL_INPUT' ||
component.type === 'PHONE_INPUT' ||
component.type === 'OTP_INPUT'
component.type === 'OTP_INPUT' ||
component.type === 'SELECT' ||
component.type === 'DATE_INPUT'
) {
const identifier: string = component.ref;
const ruleValidator = buildValidatorFromRules(component.validation);
fields.push({
initialValue: '',
name: identifier,
Expand All @@ -315,6 +320,13 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
) {
return t('field.email.invalid');
}
// Run declarative rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}

return null;
},
Expand All @@ -332,7 +344,10 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
[t],
);

const formFields: FormField[] = components ? extractFormFields(components) : [];
const formFields: FormField[] = useMemo(
() => (components ? extractFormFields(components) : []),
[components, extractFormFields],
);

const form: ReturnType<typeof useForm> = useForm<Record<string, string>>({
fields: formFields,
Expand All @@ -349,10 +364,33 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
isValid: isFormValid,
setValue: setFormValue,
setTouched: setFormTouched,
setTouchedFields,
setErrors: setFormErrors,
clearErrors: clearFormErrors,
validateForm,
touchAllFields,
} = form;

// Project server-side fieldErrors into form state. `setTouchedFields` is used instead
// of `setTouched` so client-side `validateField` doesn't wipe server errors when the
// client rules happen to pass.
useEffect(() => {
clearFormErrors();
if (!serverFieldErrors || serverFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of serverFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setTouchedFields(touched);
setFormErrors(errors);
}, [serverFieldErrors, setFormErrors, setTouchedFields, clearFormErrors]);

/**
* Handle input value changes.
* Only updates the value without marking as touched.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,33 +475,34 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
isValid: isFormValid,
setValue: setFormValue,
setTouched: setFormTouched,
setTouchedFields,
setErrors: setFormErrors,
clearErrors: clearFormErrors,
validateForm,
touchAllFields,
reset: resetForm,
} = form;

/**
* Project server-side validation errors from the most recent flow response into the
* form's `errors` state. See BaseSignIn for the same pattern: first error per field
* wins, and the affected fields are marked touched so the error renders immediately.
*/
// Project server-side fieldErrors from the flow response into form state.
// `setTouchedFields` avoids re-running client-side validation that would wipe
// server errors when the client rules happen to pass.
useEffect(() => {
clearFormErrors();
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setTouchedFields(touched);
setFormErrors(errors);
Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
}, [currentFlow, setFormErrors, setFormTouched, clearFormErrors]);
}, [currentFlow, setFormErrors, setTouchedFields, clearFormErrors]);

/**
* Setup form fields based on the current flow.
Expand Down
Loading