Skip to content

Commit 1d96bcf

Browse files
author
Sajid Mannikeri
committed
Add validation on change
Signed-off-by: Sajid Mannikeri <sajid.mannikeri@ad.infosys.com>
1 parent a02382f commit 1d96bcf

5 files changed

Lines changed: 156 additions & 67 deletions

File tree

packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ export interface BaseRecoveryProps {
9595
* Component-level preferences to override global i18n and theme settings.
9696
*/
9797
preferences?: Preferences;
98+
/**
99+
* When a field has been blurred at least once, re-run validation on every subsequent
100+
* keystroke so a rendered error clears the moment the value becomes valid. Doesn't
101+
* affect fields that have never been blurred — the user isn't shown errors while
102+
* initially typing. Default `false` preserves prior behavior.
103+
*/
104+
revalidateOnChangeAfterBlur?: boolean;
98105
showLogo?: boolean;
99106
showSubtitle?: boolean;
100107
showTitle?: boolean;
@@ -125,6 +132,7 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
125132
children,
126133
showTitle = true,
127134
showSubtitle = true,
135+
revalidateOnChangeAfterBlur = false,
128136
}: BaseRecoveryProps): ReactElement => {
129137
const {theme, colorScheme} = useTheme();
130138
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
@@ -236,6 +244,7 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
236244
fields: formFields,
237245
initialValues: {},
238246
requiredMessage: t('validations.required.field.error'),
247+
revalidateOnChangeAfterBlur,
239248
validateOnBlur: true,
240249
validateOnChange: false,
241250
});

packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* under the License.
1717
*/
1818

19-
import {cx} from '@emotion/css';
19+
import { cx } from '@emotion/css';
2020
import {
2121
withVendorCSSClassPrefix,
2222
EmbeddedSignInFlowRequest,
@@ -26,7 +26,7 @@ import {
2626
Preferences,
2727
buildValidatorFromRules,
2828
} from '@thunderid/browser';
29-
import {FC, useEffect, useMemo, useState, useCallback, useContext, ReactElement, ReactNode} from 'react';
29+
import { FC, useEffect, useMemo, useState, useCallback, useContext, ReactElement, ReactNode } from 'react';
3030
import useStyles from './BaseSignIn.styles';
3131
import ComponentRendererContext, {
3232
ComponentRendererMap,
@@ -36,16 +36,16 @@ import useFlow from '../../../../contexts/Flow/useFlow';
3636
import ComponentPreferencesContext from '../../../../contexts/I18n/ComponentPreferencesContext';
3737
import useTheme from '../../../../contexts/Theme/useTheme';
3838
import useThunderID from '../../../../contexts/ThunderID/useThunderID';
39-
import {FormField, useForm} from '../../../../hooks/useForm';
39+
import { FormField, useForm } from '../../../../hooks/useForm';
4040
import useTranslation from '../../../../hooks/useTranslation';
4141
import composeAffixedInputs from '../../../../utils/composeAffixedInputs';
42-
import {extractErrorMessage} from '../../../../utils/flowTransformer';
42+
import { extractErrorMessage } from '../../../../utils/flowTransformer';
4343
import AlertPrimitive from '../../../primitives/Alert/Alert';
4444
// eslint-disable-next-line import/no-named-as-default
45-
import CardPrimitive, {CardProps} from '../../../primitives/Card/Card';
45+
import CardPrimitive, { CardProps } from '../../../primitives/Card/Card';
4646
import Spinner from '../../../primitives/Spinner/Spinner';
4747
import Typography from '../../../primitives/Typography/Typography';
48-
import {renderSignInComponents} from '../AuthOptionFactory';
48+
import { renderSignInComponents } from '../AuthOptionFactory';
4949

5050
/**
5151
* Render props for custom UI rendering
@@ -94,7 +94,7 @@ export interface BaseSignInRenderProps {
9494
/**
9595
* Flow messages
9696
*/
97-
messages: {message: string; type: string}[];
97+
messages: { message: string; type: string }[];
9898

9999
/**
100100
* Flow metadata returned by the platform (v2 only). `null` while loading or unavailable.
@@ -119,7 +119,7 @@ export interface BaseSignInRenderProps {
119119
/**
120120
* Function to validate the form
121121
*/
122-
validateForm: () => {fieldErrors: Record<string, string>; isValid: boolean};
122+
validateForm: () => { fieldErrors: Record<string, string>; isValid: boolean };
123123

124124
/**
125125
* Form values
@@ -226,6 +226,14 @@ export interface BaseSignInProps {
226226
*/
227227
serverFieldErrors?: FieldError[] | null;
228228

229+
/**
230+
* When a field has been blurred at least once, re-run validation on every subsequent
231+
* keystroke so a rendered error clears the moment the value becomes valid. Doesn't
232+
* affect fields that have never been blurred — the user isn't shown errors while
233+
* initially typing. Default `false` preserves prior behavior.
234+
*/
235+
revalidateOnChangeAfterBlur?: boolean;
236+
229237
/**
230238
* Size variant for the component.
231239
*/
@@ -257,19 +265,34 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
257265
additionalData = {},
258266
isTimeoutDisabled = false,
259267
serverFieldErrors = null,
268+
revalidateOnChangeAfterBlur = false,
260269
}: BaseSignInProps): ReactElement => {
261-
const {meta, vendor} = useThunderID();
262-
const {theme} = useTheme();
270+
const { meta, vendor } = useThunderID();
271+
const { theme } = useTheme();
263272
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
264-
const {t} = useTranslation();
265-
const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow();
273+
const { t } = useTranslation();
274+
const { subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages } = useFlow();
266275
const styles: any = useStyles(theme, theme.vars.colors.text.primary);
267276

268277
const [isSubmitting, setIsSubmitting] = useState(false);
269278
const [apiError, setApiError] = useState<Error | null>(null);
270279

271280
const isLoading: boolean = externalIsLoading || isSubmitting;
272281

282+
/**
283+
* Component type for which forms validation will be applicable
284+
*/
285+
const acceptableType = [
286+
'TEXT_INPUT',
287+
'PASSWORD_INPUT',
288+
'EMAIL_INPUT',
289+
'PHONE_INPUT',
290+
'OTP_INPUT',
291+
'SELECT',
292+
'DATE_INPUT',
293+
'CUSTOM',
294+
];
295+
273296
/**
274297
* Handle error responses and extract meaningful error messages
275298
* Uses the transformer's extractErrorMessage function for consistency
@@ -300,15 +323,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
300323

301324
const processComponents = (comps: EmbeddedFlowComponent[]): any => {
302325
comps.forEach((component: any) => {
303-
if (
304-
component.type === 'TEXT_INPUT' ||
305-
component.type === 'PASSWORD_INPUT' ||
306-
component.type === 'EMAIL_INPUT' ||
307-
component.type === 'PHONE_INPUT' ||
308-
component.type === 'OTP_INPUT' ||
309-
component.type === 'SELECT' ||
310-
component.type === 'DATE_INPUT'
311-
) {
326+
if (acceptableType.includes(component.type)) {
312327
const identifier: string = component.ref;
313328
const ruleValidator = buildValidatorFromRules(component.validation);
314329
fields.push({
@@ -360,6 +375,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
360375
fields: formFields,
361376
initialValues: {},
362377
requiredMessage: t('validations.required.field.error'),
378+
revalidateOnChangeAfterBlur,
363379
validateOnBlur: true,
364380
validateOnChange: false,
365381
});
@@ -460,7 +476,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
460476
// For V2, we always send inputs and action
461477
payload = {
462478
...payload,
463-
...(component.id && {action: component.id}),
479+
...(component.id && { action: component.id }),
464480
inputs: filteredInputs,
465481
};
466482

@@ -573,7 +589,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
573589
touched: touchedFields,
574590
validateForm: () => {
575591
const result: any = validateForm();
576-
return {fieldErrors: result.errors, isValid: result.isValid};
592+
return { fieldErrors: result.errors, isValid: result.isValid };
577593
},
578594
values: formValues,
579595
};
@@ -595,7 +611,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
595611
variant={variant}
596612
>
597613
<CardPrimitive.Content>
598-
<div style={{display: 'flex', justifyContent: 'center', padding: '2rem'}}>
614+
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
599615
<Spinner />
600616
</div>
601617
</CardPrimitive.Content>
@@ -703,7 +719,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
703719
* </BaseSignIn>
704720
* ```
705721
*/
706-
const BaseSignIn: FC<BaseSignInProps> = ({preferences, ...rest}: BaseSignInProps): ReactElement => {
722+
const BaseSignIn: FC<BaseSignInProps> = ({ preferences, ...rest }: BaseSignInProps): ReactElement => {
707723
const content: ReactElement = (
708724
<FlowProvider>
709725
<BaseSignInContent {...rest} />

packages/react/src/components/presentation/auth/SignIn/SignIn.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ export interface SignInProps {
134134
*/
135135
preferences?: Preferences;
136136

137+
/**
138+
* When a field has been blurred at least once, re-run validation on every subsequent
139+
* keystroke so a rendered error clears the moment the value becomes valid. Doesn't
140+
* affect fields that have never been blurred — the user isn't shown errors while
141+
* initially typing. Default `false` preserves prior behavior.
142+
*/
143+
revalidateOnChangeAfterBlur?: boolean;
144+
137145
/**
138146
* Size variant for the component.
139147
*/
@@ -226,6 +234,7 @@ const SignIn: FC<SignInProps> = ({
226234
onError,
227235
variant,
228236
children,
237+
revalidateOnChangeAfterBlur,
229238
}: SignInProps): ReactElement => {
230239
const {applicationId, afterSignInUrl, signIn, isInitialized, isLoading, meta, getStorageManager, scopes, vendor} =
231240
useThunderID();
@@ -435,19 +444,25 @@ const SignIn: FC<SignInProps> = ({
435444
};
436445

437446
/**
438-
* Handle terminal flow responses (Error and Complete) shared by initializeFlow and handleSubmit.
439-
* Throws on an Error status so the caller's catch block can propagate it to BaseSignIn.
440-
* Returns true when a Complete status was handled (caller should return), false otherwise.
447+
* Handle ERROR and COMPLETE responses. Returns true if fully handled.
448+
* ERROR + executionId: recoverable — session preserved for retry.
449+
* ERROR + no executionId: terminal — clear state and surface the error.
441450
*/
442451
const handleTerminalResponse = async (response: EmbeddedSignInFlowResponse): Promise<boolean> => {
443-
// Handle Error flow status - flow has failed and is invalidated
444452
if (response.flowStatus === EmbeddedSignInFlowStatus.Error) {
453+
if (response.executionId) {
454+
// Recoverable: session still alive. Show inline error without firing onError.
455+
setExecutionId(response.executionId);
456+
await setChallengeToken(response.challengeToken ?? null);
457+
setIsFlowInitialized(true);
458+
setFlowError(new Error(extractErrorMessage(response, t)));
459+
return true;
460+
}
461+
// Terminal: backend invalidated the session — clear all state.
445462
await clearFlowState();
446-
const err: any = new Error(extractErrorMessage(response, t));
447-
setError(err);
463+
setError(new Error(extractErrorMessage(response, t)));
448464
cleanupFlowUrlParams();
449-
// Throw the error so it's caught by the catch block and propagated to BaseSignIn
450-
throw err;
465+
return true;
451466
}
452467

453468
if (response.flowStatus === EmbeddedSignInFlowStatus.Complete) {
@@ -588,6 +603,8 @@ const SignIn: FC<SignInProps> = ({
588603
// session lets the backend return COMPLETE immediately with no UI components. Without
589604
// this the UI would fall through with no components and spin forever.
590605
if (await handleTerminalResponse(response)) {
606+
// Reset the init gate so a terminal error doesn't leave the user stuck without retry.
607+
initializationAttemptedRef.current = false;
591608
return;
592609
}
593610

@@ -814,6 +831,11 @@ const SignIn: FC<SignInProps> = ({
814831
return;
815832
}
816833

834+
// Handle terminal flow statuses before normalization.
835+
if (await handleTerminalResponse(response)) {
836+
return;
837+
}
838+
817839
const {
818840
executionId: normalizedExecutionId,
819841
components: normalizedComponents,
@@ -827,11 +849,6 @@ const SignIn: FC<SignInProps> = ({
827849
meta,
828850
);
829851

830-
// Handle terminal flow statuses (Error throws, Complete redirects and returns true).
831-
if (await handleTerminalResponse(response)) {
832-
return;
833-
}
834-
835852
// Always update challenge token on any INCOMPLETE response — token rotates every step.
836853
await setChallengeToken(response.challengeToken ?? null);
837854

@@ -1004,6 +1021,7 @@ const SignIn: FC<SignInProps> = ({
10041021
size={size}
10051022
variant={variant}
10061023
preferences={preferences}
1024+
revalidateOnChangeAfterBlur={revalidateOnChangeAfterBlur}
10071025
serverFieldErrors={serverFieldErrors}
10081026
/>
10091027
);

packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ export interface BaseSignUpProps {
228228
*/
229229
preferences?: Preferences;
230230

231+
/**
232+
* When a field has been blurred at least once, re-run validation on every subsequent
233+
* keystroke so a rendered error clears the moment the value becomes valid. Doesn't
234+
* affect fields that have never been blurred — the user isn't shown errors while
235+
* initially typing. Default `false` preserves prior behavior.
236+
*/
237+
revalidateOnChangeAfterBlur?: boolean;
238+
231239
/**
232240
* Whether to redirect after sign-up.
233241
*/
@@ -282,6 +290,7 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
282290
children,
283291
showTitle = true,
284292
showSubtitle = true,
293+
revalidateOnChangeAfterBlur = false,
285294
}: BaseSignUpProps): ReactElement => {
286295
const {theme, colorScheme} = useTheme();
287296
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
@@ -471,6 +480,7 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
471480
fields: formFields,
472481
initialValues: {},
473482
requiredMessage: t('validations.required.field.error'),
483+
revalidateOnChangeAfterBlur,
474484
validateOnBlur: true,
475485
validateOnChange: false,
476486
});

0 commit comments

Comments
 (0)