@thunderid/react 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/adapters/Consent.cjs +21 -26
- package/dist/cjs/components/adapters/Consent.cjs.map +1 -1
- package/dist/cjs/components/adapters/ConsentCheckboxList.cjs +9 -3
- package/dist/cjs/components/adapters/ConsentCheckboxList.cjs.map +1 -1
- package/dist/cjs/components/presentation/LanguageSwitcher/LanguageSwitcher.cjs +10 -2
- package/dist/cjs/components/presentation/LanguageSwitcher/LanguageSwitcher.cjs.map +1 -1
- package/dist/cjs/components/presentation/auth/AuthOptionFactory.cjs +1 -2
- package/dist/cjs/components/presentation/auth/AuthOptionFactory.cjs.map +1 -1
- package/dist/cjs/components/presentation/auth/Recovery/BaseRecovery.cjs +1 -1
- package/dist/cjs/components/presentation/auth/Recovery/BaseRecovery.cjs.map +1 -1
- package/dist/cjs/components/presentation/auth/SignIn/BaseSignIn.cjs +6 -3
- package/dist/cjs/components/presentation/auth/SignIn/BaseSignIn.cjs.map +1 -1
- package/dist/cjs/components/presentation/auth/SignUp/BaseSignUp.cjs +1 -1
- package/dist/cjs/components/presentation/auth/SignUp/BaseSignUp.cjs.map +1 -1
- package/dist/cjs/contexts/FlowMeta/FlowMetaProvider.cjs +21 -9
- package/dist/cjs/contexts/FlowMeta/FlowMetaProvider.cjs.map +1 -1
- package/dist/cjs/contexts/ThunderID/ThunderIDProvider.cjs +2 -1
- package/dist/cjs/contexts/ThunderID/ThunderIDProvider.cjs.map +1 -1
- package/dist/cjs/hooks/useForm.cjs +42 -5
- package/dist/cjs/hooks/useForm.cjs.map +1 -1
- package/dist/cjs/utils/resolveTranslationsInObject.cjs +3 -1
- package/dist/cjs/utils/resolveTranslationsInObject.cjs.map +1 -1
- package/dist/components/adapters/Consent.d.ts +1 -18
- package/dist/components/adapters/Consent.d.ts.map +1 -1
- package/dist/components/adapters/Consent.js +21 -25
- package/dist/components/adapters/Consent.js.map +1 -1
- package/dist/components/adapters/ConsentCheckboxList.d.ts +5 -0
- package/dist/components/adapters/ConsentCheckboxList.d.ts.map +1 -1
- package/dist/components/adapters/ConsentCheckboxList.js +9 -3
- package/dist/components/adapters/ConsentCheckboxList.js.map +1 -1
- package/dist/components/presentation/LanguageSwitcher/LanguageSwitcher.d.ts.map +1 -1
- package/dist/components/presentation/LanguageSwitcher/LanguageSwitcher.js +11 -3
- package/dist/components/presentation/LanguageSwitcher/LanguageSwitcher.js.map +1 -1
- package/dist/components/presentation/auth/AuthOptionFactory.d.ts.map +1 -1
- package/dist/components/presentation/auth/AuthOptionFactory.js +1 -2
- package/dist/components/presentation/auth/AuthOptionFactory.js.map +1 -1
- package/dist/components/presentation/auth/Recovery/BaseRecovery.d.ts.map +1 -1
- package/dist/components/presentation/auth/Recovery/BaseRecovery.js +1 -1
- package/dist/components/presentation/auth/Recovery/BaseRecovery.js.map +1 -1
- package/dist/components/presentation/auth/SignIn/BaseSignIn.d.ts.map +1 -1
- package/dist/components/presentation/auth/SignIn/BaseSignIn.js +6 -3
- package/dist/components/presentation/auth/SignIn/BaseSignIn.js.map +1 -1
- package/dist/components/presentation/auth/SignUp/BaseSignUp.js +1 -1
- package/dist/components/presentation/auth/SignUp/BaseSignUp.js.map +1 -1
- package/dist/contexts/FlowMeta/FlowMetaProvider.d.ts +15 -0
- package/dist/contexts/FlowMeta/FlowMetaProvider.d.ts.map +1 -1
- package/dist/contexts/FlowMeta/FlowMetaProvider.js +21 -9
- package/dist/contexts/FlowMeta/FlowMetaProvider.js.map +1 -1
- package/dist/contexts/ThunderID/ThunderIDProvider.d.ts.map +1 -1
- package/dist/contexts/ThunderID/ThunderIDProvider.js +2 -1
- package/dist/contexts/ThunderID/ThunderIDProvider.js.map +1 -1
- package/dist/hooks/useForm.d.ts +5 -0
- package/dist/hooks/useForm.d.ts.map +1 -1
- package/dist/hooks/useForm.js +43 -6
- package/dist/hooks/useForm.js.map +1 -1
- package/dist/models/config.d.ts +2 -0
- package/dist/models/config.d.ts.map +1 -1
- package/dist/utils/resolveTranslationsInObject.d.ts.map +1 -1
- package/dist/utils/resolveTranslationsInObject.js +3 -1
- package/dist/utils/resolveTranslationsInObject.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThunderIDProvider.js","names":["logger: ReturnType<typeof createPackageComponentLogger>","ThunderIDProvider: FC<PropsWithChildren<ThunderIDProviderProps>>","reRenderCheckRef: RefObject<boolean>","client: ThunderIDReactClient","ThunderIDReactClient","storageManagerRef: any","useBrowserUrl","pendingSessionUpdateRef: RefObject<Promise<void> | null>","resolvedBaseUrl: string","decodedToken: IdToken","shouldFetchProfile: boolean","claims: User","currentSignInStatus: boolean","getUsersMe","getUsersMeMeta","arg1: any","isV2FlowRequest: boolean","currentUrl: URL","urlParams: URLSearchParams","code: string | null","executionIdFromUrl: string | null","storedExecutionId: string | null","interval: NodeJS.Timeout","status: boolean","getDecodedIdToken: () => Promise<IdToken>","getIdToken: () => Promise<string>","getAccessToken: () => Promise<string>","getStorageManager: () => Promise<any>","storageManager: any","request: (...args: any[]) => Promise<any>","requestAll: (...args: any[]) => Promise<any>","exchangeToken: (exchangeConfig: any) => Promise<any>","signOut: (...args: any[]) => Promise<any>","recover: (payload: any) => Promise<any>","signUp: (...args: any[]) => Promise<any>","clearSession: (...args: any[]) => Promise<any>","reInitialize: (reInitConfig: any) => Promise<any>","result: any","reinitializedConfig: ThunderIDReactConfig","value: any","ThunderIDContext","I18nProvider","FlowMetaProvider","ThemeProvider","FlowProvider","UserProvider","ComponentRendererProvider"],"sources":["../../../src/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025-2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n ThunderIDRuntimeError,\n generateFlattenedUserProfile,\n OIDCDiscoveryApiResponse,\n SignInOptions,\n User,\n UserProfile,\n IdToken,\n extractUserClaimsFromIdToken,\n EmbeddedSignInFlowResponse,\n createPackageComponentLogger,\n getVendorPrefix,\n resolveResourceEndpoint,\n AttributeSchema,\n} from '@thunderid/browser';\nimport {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, useCallback} from 'react';\nimport ThunderIDContext from './ThunderIDContext';\nimport useBrowserUrl from '../../hooks/useBrowserUrl';\nimport {ThunderIDReactConfig} from '../../models/config';\nimport {configureEmotionNonce} from '../../styles/emotion';\nimport ThunderIDReactClient from '../../ThunderIDReactClient';\nimport ComponentRendererProvider from '../ComponentRenderer/ComponentRendererProvider';\nimport FlowProvider from '../Flow/FlowProvider';\nimport FlowMetaProvider from '../FlowMeta/FlowMetaProvider';\nimport I18nProvider from '../I18n/I18nProvider';\nimport ThemeProvider from '../Theme/ThemeProvider';\nimport UserProvider from '../User/UserProvider';\nimport getUsersMe from '../../api/getUsersMe';\nimport getUsersMeMeta from '../../api/getUsersMeMeta';\n\nconst logger: ReturnType<typeof createPackageComponentLogger> = createPackageComponentLogger(\n '@thunderid/react',\n 'ThunderIDProvider',\n);\n\n/**\n * Props interface of {@link ThunderIDProvider}\n */\nexport type ThunderIDProviderProps = ThunderIDReactConfig;\n\nconst ThunderIDProvider: FC<PropsWithChildren<ThunderIDProviderProps>> = ({\n afterSignInUrl,\n afterSignOutUrl,\n baseUrl: initialBaseUrl,\n clientId,\n children,\n extensions,\n scopes,\n preferences,\n signInUrl,\n signUpUrl,\n organizationHandle,\n applicationId,\n signInOptions,\n tokenRequest,\n syncSession,\n instanceId = 0,\n organizationChain,\n cspNonce,\n vendor,\n ...rest\n}: PropsWithChildren<ThunderIDProviderProps>): ReactElement => {\n // Must run synchronously here, in the render body, before any descendant's css()/cx()/\n // keyframes() calls execute during this same render pass - see the doc comment on\n // `configureEmotionNonce` for why a `useEffect` would be too late.\n configureEmotionNonce(cspNonce, vendor);\n\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const client: ThunderIDReactClient = useMemo(() => new ThunderIDReactClient(instanceId), [instanceId]);\n const storageManagerRef: any = useRef<any>(null);\n const {hasAuthParams, hasCalledForThisInstance} = useBrowserUrl();\n const [user, setUser] = useState<any | null>(null);\n const [isSignedInSync, setIsSignedInSync] = useState<boolean>(false);\n const [isInitializedSync, setIsInitializedSync] = useState<boolean>(false);\n const [isLoadingSync, setIsLoadingSync] = useState<boolean>(true);\n\n const [userProfile, setUserProfile] = useState<UserProfile | null>(null);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(null);\n const [baseUrl, setBaseUrl] = useState<string>(initialBaseUrl ?? '');\n const [config, setConfig] = useState<ThunderIDReactConfig>({\n afterSignInUrl: afterSignInUrl ?? window.location.origin,\n afterSignOutUrl: afterSignOutUrl ?? window.location.origin,\n applicationId,\n baseUrl,\n clientId,\n organizationChain,\n organizationHandle,\n scopes,\n signInOptions,\n tokenRequest,\n signInUrl,\n signUpUrl,\n syncSession,\n vendor,\n ...rest,\n });\n\n const [isUpdatingSession, setIsUpdatingSession] = useState<boolean>(false);\n const [wellKnown, setWellKnown] = useState<OIDCDiscoveryApiResponse | null>(null);\n\n // Holds the in-flight `updateSession()` promise so concurrent callers coalesce onto a single\n // `/users/me` request instead of issuing one each.\n const pendingSessionUpdateRef: RefObject<Promise<void> | null> = useRef<Promise<void> | null>(null);\n\n useEffect(() => {\n setBaseUrl(initialBaseUrl ?? '');\n }, [initialBaseUrl]);\n\n useEffect(() => {\n (async (): Promise<void> => {\n await client.initialize(config);\n const initializedConfig: ThunderIDReactConfig = await client.getConfiguration();\n setConfig(initializedConfig);\n setWellKnown(await client.getDiscoveryResponse());\n })();\n }, []);\n\n async function performSessionUpdate(): Promise<void> {\n try {\n // Set flag to prevent loading state tracking from interfering\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n let resolvedBaseUrl: string = baseUrl;\n\n const decodedToken: IdToken = await client.getDecodedIdToken();\n\n // If there's a `user_org` claim in the ID token,\n // Treat this login as a organization login.\n if (decodedToken?.['user_org']) {\n resolvedBaseUrl = `${(await client.getConfiguration()).baseUrl}/o`;\n setBaseUrl(resolvedBaseUrl);\n }\n\n const shouldFetchProfile: boolean = preferences?.user?.fetchUserProfile !== false;\n const claims: User = extractUserClaimsFromIdToken(decodedToken) as User;\n let profileData = claims;\n const currentSignInStatus: boolean = await client.isSignedIn();\n\n if (currentSignInStatus && shouldFetchProfile) {\n try {\n const fetchedProfile = await getUsersMe({\n baseUrl: resolvedBaseUrl,\n url: resolveResourceEndpoint('usersMe', config),\n instanceId,\n });\n profileData = {...claims, ...fetchedProfile};\n } catch (err) {\n logger.warn('Failed to fetch user profile from /users/me:', err);\n }\n\n try {\n const metaRes = await getUsersMeMeta({\n baseUrl: resolvedBaseUrl,\n url: resolveResourceEndpoint('usersMeMeta', config),\n instanceId,\n });\n if (metaRes?.schema) {\n setUserSchema(metaRes.schema);\n } else {\n setUserSchema(null);\n }\n } catch (err) {\n setUserSchema(null);\n logger.warn('Failed to fetch user schema metadata from /users/me/meta:', err);\n }\n } else {\n setUserSchema(null);\n }\n\n setUser(profileData);\n setUserProfile({\n flattenedProfile: generateFlattenedUserProfile(profileData, []),\n profile: profileData,\n });\n\n // CRITICAL: Update sign-in status BEFORE setting loading to false\n // This prevents the race condition where ProtectedRoute sees isLoading=false but isSignedIn=false\n setIsSignedInSync(currentSignInStatus);\n } catch (error) {\n // TODO: Add an error log.\n } finally {\n // Clear the flag and set final loading state\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n }\n\n /**\n * Single-flight wrapper around {@link performSessionUpdate}.\n *\n * Concurrent callers share the in-flight request instead of each issuing their own `/users/me`.\n * This is not a cache: once the request settles the ref is cleared, so a later caller (e.g.\n * revalidation after a profile update) fetches again.\n */\n async function updateSession(): Promise<void> {\n if (pendingSessionUpdateRef.current) {\n return pendingSessionUpdateRef.current;\n }\n\n pendingSessionUpdateRef.current = performSessionUpdate().finally(() => {\n pendingSessionUpdateRef.current = null;\n });\n\n return pendingSessionUpdateRef.current;\n }\n\n async function signIn(...args: any): Promise<User | EmbeddedSignInFlowResponse> {\n // Check if this is a V2 embedded flow request BEFORE calling signIn\n // This allows us to skip session checks entirely for V2 flows\n const arg1: any = args[0];\n const isV2FlowRequest: boolean =\n typeof arg1 === 'object' && arg1 !== null && ('executionId' in arg1 || 'applicationId' in arg1);\n\n try {\n if (!isV2FlowRequest) {\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n }\n\n return await client.signIn(...args);\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Sign in failed: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signIn-Error',\n 'react',\n 'An error occurred while trying to sign in.',\n );\n } finally {\n if (!isV2FlowRequest) {\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n }\n }\n\n /**\n * Try signing in when the component is mounted.\n */\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n (async (): Promise<void> => {\n // Sync session state whenever sign-in completes (both redirect and embedded V2 flows).\n // Pass the user returned by the SDK's sign-in flow so users/me result is not discarded.\n await client.on('sign-in', async () => {\n await updateSession();\n });\n\n // Start auto-refresh with a soft failure. This also covers the case where the access token\n // expired while the refresh token is still valid — startAutoRefreshToken() calls\n // refreshAccessToken() immediately when timeUntilRefresh <= 0 — so the sign-in check below\n // sees the refreshed state.\n try {\n await client.startAutoRefreshToken();\n } catch (error) {\n logger.warn('Failed to schedule automatic token refresh.', error);\n }\n\n // User is already authenticated. Restore the session and skip the sign-in path.\n if (await client.isSignedIn()) {\n await updateSession();\n\n return;\n }\n\n const currentUrl: URL = new URL(window.location.href);\n const hasAuthParamsResult: boolean =\n hasAuthParams(currentUrl, config.afterSignInUrl!) && hasCalledForThisInstance(currentUrl, instanceId ?? 0);\n\n if (hasAuthParamsResult) {\n try {\n const urlParams: URLSearchParams = currentUrl.searchParams;\n const code: string | null = urlParams.get('code');\n const executionIdFromUrl: string | null = urlParams.get('executionId');\n const storedExecutionId: string | null = sessionStorage.getItem(\n `${getVendorPrefix(config.vendor)}_execution_id`,\n );\n\n if (code && !executionIdFromUrl && !storedExecutionId) {\n await signIn();\n }\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Sign in failed: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signIn-Error',\n 'react',\n 'An error occurred while trying to sign in.',\n );\n }\n }\n })();\n }, []);\n\n /**\n * Check if the user is signed in and update the state accordingly.\n * This will also set an interval to check for the sign-in status every second\n * until the user is signed in.\n */\n useEffect(() => {\n let interval: NodeJS.Timeout;\n\n (async (): Promise<void> => {\n try {\n const status: boolean = await client.isSignedIn();\n\n setIsSignedInSync(status);\n\n if (!status) {\n interval = setInterval(async () => {\n const newStatus: boolean = await client.isSignedIn();\n\n if (newStatus) {\n setIsSignedInSync(true);\n clearInterval(interval);\n }\n }, 1000);\n } else {\n // TODO: Add a debug log to indicate that the user is already signed in.\n }\n } catch (error) {\n setIsSignedInSync(false);\n }\n })();\n\n return (): void => {\n if (interval) {\n clearInterval(interval);\n }\n };\n }, [client]);\n\n useEffect(() => {\n (async (): Promise<void> => {\n try {\n const status: boolean = await client.isInitialized();\n\n setIsInitializedSync(status);\n } catch (error) {\n setIsInitializedSync(false);\n }\n })();\n }, [client]);\n\n /**\n * Track loading state changes from the ThunderID client\n */\n useEffect(() => {\n const checkLoadingState = (): void => {\n // Don't override loading state during critical session updates\n if (isUpdatingSession) {\n return;\n }\n\n // Don't set loading=false while auth params are in the URL and user isn't signed in yet.\n // This prevents ProtectedRoute from redirecting before the sign-in effect processes the auth code.\n const currentUrl: URL = new URL(window.location.href);\n if (!isSignedInSync && hasAuthParams(currentUrl, config.afterSignInUrl!)) {\n return;\n }\n\n setIsLoadingSync(client.isLoading());\n };\n\n // Initial check\n checkLoadingState();\n\n // Set up an interval to check for loading state changes\n const interval: NodeJS.Timeout = setInterval(checkLoadingState, 100);\n\n return (): void => {\n clearInterval(interval);\n };\n }, [client, isLoadingSync, isSignedInSync, isUpdatingSession]);\n\n const signInSilently = async (options?: SignInOptions): Promise<User | boolean> => {\n try {\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n return await client.signInSilently(options);\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Error while signing in silently: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signInSilently-Error',\n 'react',\n 'An error occurred while trying to sign in silently.',\n );\n } finally {\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n };\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile | null) => ({\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n const getDecodedIdToken: () => Promise<IdToken> = useCallback(\n async (): Promise<IdToken> => client.getDecodedIdToken(),\n [client],\n );\n\n const getIdToken: () => Promise<string> = useCallback(async (): Promise<string> => client.getIdToken(), [client]);\n\n const getAccessToken: () => Promise<string> = useCallback(\n async (): Promise<string> => client.getAccessToken(),\n [client],\n );\n\n const getStorageManager: () => Promise<any> = useCallback(async (): Promise<any> => {\n const storageManager: any = storageManagerRef.current ?? (await client.getStorageManager());\n if (storageManager) {\n storageManagerRef.current = storageManager;\n }\n return storageManager;\n }, [client]);\n\n const request: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.request(...args),\n [client],\n );\n\n const requestAll: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.requestAll(...args),\n [client],\n );\n\n const exchangeToken: (exchangeConfig: any) => Promise<any> = useCallback(\n async (exchangeConfig: any): Promise<any> => client.exchangeToken(exchangeConfig),\n [client],\n );\n\n const signOut: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.signOut(...args),\n [client],\n );\n\n const recover: (payload: any) => Promise<any> = useCallback(\n async (payload: any): Promise<any> => client.recover(payload),\n [client],\n );\n\n const signUp: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.signUp(...args),\n [client],\n );\n\n const clearSession: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.clearSession(...args),\n [client],\n );\n\n const reInitialize: (reInitConfig: any) => Promise<any> = useCallback(\n async (reInitConfig: any): Promise<any> => {\n const result: any = await client.reInitialize(reInitConfig);\n\n // Refresh React state from the reinitialized client so the context (config.endpoints,\n // baseUrl, discovery) reflects the new configuration instead of the pre-reinitialization state.\n const reinitializedConfig: ThunderIDReactConfig = await client.getConfiguration();\n setConfig(reinitializedConfig);\n setBaseUrl(reinitializedConfig.baseUrl ?? '');\n setWellKnown(await client.getDiscoveryResponse());\n\n return result;\n },\n [client],\n );\n\n const value: any = useMemo(\n () => ({\n afterSignInUrl: config.afterSignInUrl,\n applicationId: config.applicationId,\n baseUrl,\n endpoints: config.endpoints,\n scopes: config.scopes,\n clearSession,\n clientId,\n discovery: {\n wellKnown,\n },\n exchangeToken,\n getAccessToken,\n getDecodedIdToken,\n getIdToken,\n getStorageManager,\n http: {\n request,\n requestAll,\n },\n instanceId,\n isInitialized: isInitializedSync,\n isLoading: isLoadingSync,\n isSignedIn: isSignedInSync,\n organizationChain,\n organizationHandle: config?.organizationHandle,\n preferences,\n reInitialize,\n recover,\n signIn,\n signInOptions,\n tokenRequest,\n signInSilently,\n signInUrl,\n signOut,\n signUp,\n signUpUrl,\n syncSession,\n user,\n userSchema,\n vendor: getVendorPrefix(config.vendor),\n }),\n [\n applicationId,\n config?.organizationHandle,\n config.vendor,\n config.afterSignInUrl,\n config.endpoints,\n config.scopes,\n signInUrl,\n signUpUrl,\n baseUrl,\n clientId,\n wellKnown,\n isInitializedSync,\n isLoadingSync,\n isSignedInSync,\n signIn,\n signInSilently,\n user,\n userSchema,\n client,\n signInOptions,\n tokenRequest,\n syncSession,\n getDecodedIdToken,\n clearSession,\n exchangeToken,\n getAccessToken,\n getStorageManager,\n instanceId,\n organizationChain,\n preferences,\n recover,\n reInitialize,\n request,\n requestAll,\n signOut,\n signUp,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={value}>\n <I18nProvider preferences={preferences?.i18n} vendor={getVendorPrefix(config.vendor)}>\n <FlowMetaProvider enabled={preferences?.resolveFromMeta !== false}>\n <ThemeProvider\n theme={{\n ...preferences?.theme?.overrides,\n direction: preferences?.theme?.direction,\n }}\n >\n <FlowProvider>\n <UserProvider\n profile={userProfile as UserProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n >\n <ComponentRendererProvider renderers={(extensions?.components?.renderers ?? {}) as any}>\n {children}\n </ComponentRendererProvider>\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDProvider;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ThunderIDProvider.js","names":["logger: ReturnType<typeof createPackageComponentLogger>","ThunderIDProvider: FC<PropsWithChildren<ThunderIDProviderProps>>","reRenderCheckRef: RefObject<boolean>","client: ThunderIDReactClient","ThunderIDReactClient","storageManagerRef: any","useBrowserUrl","pendingSessionUpdateRef: RefObject<Promise<void> | null>","resolvedBaseUrl: string","decodedToken: IdToken","shouldFetchProfile: boolean","claims: User","currentSignInStatus: boolean","getUsersMe","getUsersMeMeta","arg1: any","isV2FlowRequest: boolean","currentUrl: URL","urlParams: URLSearchParams","code: string | null","executionIdFromUrl: string | null","storedExecutionId: string | null","interval: NodeJS.Timeout","status: boolean","getDecodedIdToken: () => Promise<IdToken>","getIdToken: () => Promise<string>","getAccessToken: () => Promise<string>","getStorageManager: () => Promise<any>","storageManager: any","request: (...args: any[]) => Promise<any>","requestAll: (...args: any[]) => Promise<any>","exchangeToken: (exchangeConfig: any) => Promise<any>","signOut: (...args: any[]) => Promise<any>","recover: (payload: any) => Promise<any>","signUp: (...args: any[]) => Promise<any>","clearSession: (...args: any[]) => Promise<any>","reInitialize: (reInitConfig: any) => Promise<any>","result: any","reinitializedConfig: ThunderIDReactConfig","value: any","ThunderIDContext","I18nProvider","FlowMetaProvider","ThemeProvider","FlowProvider","UserProvider","ComponentRendererProvider"],"sources":["../../../src/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025-2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n ThunderIDRuntimeError,\n generateFlattenedUserProfile,\n OIDCDiscoveryApiResponse,\n SignInOptions,\n User,\n UserProfile,\n IdToken,\n extractUserClaimsFromIdToken,\n EmbeddedSignInFlowResponse,\n createPackageComponentLogger,\n getVendorPrefix,\n resolveResourceEndpoint,\n AttributeSchema,\n} from '@thunderid/browser';\nimport {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, useCallback} from 'react';\nimport ThunderIDContext from './ThunderIDContext';\nimport useBrowserUrl from '../../hooks/useBrowserUrl';\nimport {ThunderIDReactConfig} from '../../models/config';\nimport {configureEmotionNonce} from '../../styles/emotion';\nimport ThunderIDReactClient from '../../ThunderIDReactClient';\nimport ComponentRendererProvider from '../ComponentRenderer/ComponentRendererProvider';\nimport FlowProvider from '../Flow/FlowProvider';\nimport FlowMetaProvider from '../FlowMeta/FlowMetaProvider';\nimport I18nProvider from '../I18n/I18nProvider';\nimport ThemeProvider from '../Theme/ThemeProvider';\nimport UserProvider from '../User/UserProvider';\nimport getUsersMe from '../../api/getUsersMe';\nimport getUsersMeMeta from '../../api/getUsersMeMeta';\n\nconst logger: ReturnType<typeof createPackageComponentLogger> = createPackageComponentLogger(\n '@thunderid/react',\n 'ThunderIDProvider',\n);\n\n/**\n * Props interface of {@link ThunderIDProvider}\n */\nexport type ThunderIDProviderProps = ThunderIDReactConfig;\n\nconst ThunderIDProvider: FC<PropsWithChildren<ThunderIDProviderProps>> = ({\n afterSignInUrl,\n afterSignOutUrl,\n baseUrl: initialBaseUrl,\n clientId,\n children,\n extensions,\n scopes,\n preferences,\n signInUrl,\n signUpUrl,\n organizationHandle,\n applicationId,\n signInOptions,\n tokenRequest,\n syncSession,\n instanceId = 0,\n organizationChain,\n cspNonce,\n vendor,\n namespace,\n ...rest\n}: PropsWithChildren<ThunderIDProviderProps>): ReactElement => {\n // Must run synchronously here, in the render body, before any descendant's css()/cx()/\n // keyframes() calls execute during this same render pass - see the doc comment on\n // `configureEmotionNonce` for why a `useEffect` would be too late.\n configureEmotionNonce(cspNonce, vendor);\n\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const client: ThunderIDReactClient = useMemo(() => new ThunderIDReactClient(instanceId), [instanceId]);\n const storageManagerRef: any = useRef<any>(null);\n const {hasAuthParams, hasCalledForThisInstance} = useBrowserUrl();\n const [user, setUser] = useState<any | null>(null);\n const [isSignedInSync, setIsSignedInSync] = useState<boolean>(false);\n const [isInitializedSync, setIsInitializedSync] = useState<boolean>(false);\n const [isLoadingSync, setIsLoadingSync] = useState<boolean>(true);\n\n const [userProfile, setUserProfile] = useState<UserProfile | null>(null);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(null);\n const [baseUrl, setBaseUrl] = useState<string>(initialBaseUrl ?? '');\n const [config, setConfig] = useState<ThunderIDReactConfig>({\n afterSignInUrl: afterSignInUrl ?? window.location.origin,\n afterSignOutUrl: afterSignOutUrl ?? window.location.origin,\n applicationId,\n baseUrl,\n clientId,\n organizationChain,\n organizationHandle,\n scopes,\n signInOptions,\n tokenRequest,\n signInUrl,\n signUpUrl,\n syncSession,\n vendor,\n ...rest,\n });\n\n const [isUpdatingSession, setIsUpdatingSession] = useState<boolean>(false);\n const [wellKnown, setWellKnown] = useState<OIDCDiscoveryApiResponse | null>(null);\n\n // Holds the in-flight `updateSession()` promise so concurrent callers coalesce onto a single\n // `/users/me` request instead of issuing one each.\n const pendingSessionUpdateRef: RefObject<Promise<void> | null> = useRef<Promise<void> | null>(null);\n\n useEffect(() => {\n setBaseUrl(initialBaseUrl ?? '');\n }, [initialBaseUrl]);\n\n useEffect(() => {\n (async (): Promise<void> => {\n await client.initialize(config);\n const initializedConfig: ThunderIDReactConfig = await client.getConfiguration();\n setConfig(initializedConfig);\n setWellKnown(await client.getDiscoveryResponse());\n })();\n }, []);\n\n async function performSessionUpdate(): Promise<void> {\n try {\n // Set flag to prevent loading state tracking from interfering\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n let resolvedBaseUrl: string = baseUrl;\n\n const decodedToken: IdToken = await client.getDecodedIdToken();\n\n // If there's a `user_org` claim in the ID token,\n // Treat this login as a organization login.\n if (decodedToken?.['user_org']) {\n resolvedBaseUrl = `${(await client.getConfiguration()).baseUrl}/o`;\n setBaseUrl(resolvedBaseUrl);\n }\n\n const shouldFetchProfile: boolean = preferences?.user?.fetchUserProfile !== false;\n const claims: User = extractUserClaimsFromIdToken(decodedToken) as User;\n let profileData = claims;\n const currentSignInStatus: boolean = await client.isSignedIn();\n\n if (currentSignInStatus && shouldFetchProfile) {\n try {\n const fetchedProfile = await getUsersMe({\n baseUrl: resolvedBaseUrl,\n url: resolveResourceEndpoint('usersMe', config),\n instanceId,\n });\n profileData = {...claims, ...fetchedProfile};\n } catch (err) {\n logger.warn('Failed to fetch user profile from /users/me:', err);\n }\n\n try {\n const metaRes = await getUsersMeMeta({\n baseUrl: resolvedBaseUrl,\n url: resolveResourceEndpoint('usersMeMeta', config),\n instanceId,\n });\n if (metaRes?.schema) {\n setUserSchema(metaRes.schema);\n } else {\n setUserSchema(null);\n }\n } catch (err) {\n setUserSchema(null);\n logger.warn('Failed to fetch user schema metadata from /users/me/meta:', err);\n }\n } else {\n setUserSchema(null);\n }\n\n setUser(profileData);\n setUserProfile({\n flattenedProfile: generateFlattenedUserProfile(profileData, []),\n profile: profileData,\n });\n\n // CRITICAL: Update sign-in status BEFORE setting loading to false\n // This prevents the race condition where ProtectedRoute sees isLoading=false but isSignedIn=false\n setIsSignedInSync(currentSignInStatus);\n } catch (error) {\n // TODO: Add an error log.\n } finally {\n // Clear the flag and set final loading state\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n }\n\n /**\n * Single-flight wrapper around {@link performSessionUpdate}.\n *\n * Concurrent callers share the in-flight request instead of each issuing their own `/users/me`.\n * This is not a cache: once the request settles the ref is cleared, so a later caller (e.g.\n * revalidation after a profile update) fetches again.\n */\n async function updateSession(): Promise<void> {\n if (pendingSessionUpdateRef.current) {\n return pendingSessionUpdateRef.current;\n }\n\n pendingSessionUpdateRef.current = performSessionUpdate().finally(() => {\n pendingSessionUpdateRef.current = null;\n });\n\n return pendingSessionUpdateRef.current;\n }\n\n async function signIn(...args: any): Promise<User | EmbeddedSignInFlowResponse> {\n // Check if this is a V2 embedded flow request BEFORE calling signIn\n // This allows us to skip session checks entirely for V2 flows\n const arg1: any = args[0];\n const isV2FlowRequest: boolean =\n typeof arg1 === 'object' && arg1 !== null && ('executionId' in arg1 || 'applicationId' in arg1);\n\n try {\n if (!isV2FlowRequest) {\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n }\n\n return await client.signIn(...args);\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Sign in failed: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signIn-Error',\n 'react',\n 'An error occurred while trying to sign in.',\n );\n } finally {\n if (!isV2FlowRequest) {\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n }\n }\n\n /**\n * Try signing in when the component is mounted.\n */\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n (async (): Promise<void> => {\n // Sync session state whenever sign-in completes (both redirect and embedded V2 flows).\n // Pass the user returned by the SDK's sign-in flow so users/me result is not discarded.\n await client.on('sign-in', async () => {\n await updateSession();\n });\n\n // Start auto-refresh with a soft failure. This also covers the case where the access token\n // expired while the refresh token is still valid — startAutoRefreshToken() calls\n // refreshAccessToken() immediately when timeUntilRefresh <= 0 — so the sign-in check below\n // sees the refreshed state.\n try {\n await client.startAutoRefreshToken();\n } catch (error) {\n logger.warn('Failed to schedule automatic token refresh.', error);\n }\n\n // User is already authenticated. Restore the session and skip the sign-in path.\n if (await client.isSignedIn()) {\n await updateSession();\n\n return;\n }\n\n const currentUrl: URL = new URL(window.location.href);\n const hasAuthParamsResult: boolean =\n hasAuthParams(currentUrl, config.afterSignInUrl!) && hasCalledForThisInstance(currentUrl, instanceId ?? 0);\n\n if (hasAuthParamsResult) {\n try {\n const urlParams: URLSearchParams = currentUrl.searchParams;\n const code: string | null = urlParams.get('code');\n const executionIdFromUrl: string | null = urlParams.get('executionId');\n const storedExecutionId: string | null = sessionStorage.getItem(\n `${getVendorPrefix(config.vendor)}_execution_id`,\n );\n\n if (code && !executionIdFromUrl && !storedExecutionId) {\n await signIn();\n }\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Sign in failed: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signIn-Error',\n 'react',\n 'An error occurred while trying to sign in.',\n );\n }\n }\n })();\n }, []);\n\n /**\n * Check if the user is signed in and update the state accordingly.\n * This will also set an interval to check for the sign-in status every second\n * until the user is signed in.\n */\n useEffect(() => {\n let interval: NodeJS.Timeout;\n\n (async (): Promise<void> => {\n try {\n const status: boolean = await client.isSignedIn();\n\n setIsSignedInSync(status);\n\n if (!status) {\n interval = setInterval(async () => {\n const newStatus: boolean = await client.isSignedIn();\n\n if (newStatus) {\n setIsSignedInSync(true);\n clearInterval(interval);\n }\n }, 1000);\n } else {\n // TODO: Add a debug log to indicate that the user is already signed in.\n }\n } catch (error) {\n setIsSignedInSync(false);\n }\n })();\n\n return (): void => {\n if (interval) {\n clearInterval(interval);\n }\n };\n }, [client]);\n\n useEffect(() => {\n (async (): Promise<void> => {\n try {\n const status: boolean = await client.isInitialized();\n\n setIsInitializedSync(status);\n } catch (error) {\n setIsInitializedSync(false);\n }\n })();\n }, [client]);\n\n /**\n * Track loading state changes from the ThunderID client\n */\n useEffect(() => {\n const checkLoadingState = (): void => {\n // Don't override loading state during critical session updates\n if (isUpdatingSession) {\n return;\n }\n\n // Don't set loading=false while auth params are in the URL and user isn't signed in yet.\n // This prevents ProtectedRoute from redirecting before the sign-in effect processes the auth code.\n const currentUrl: URL = new URL(window.location.href);\n if (!isSignedInSync && hasAuthParams(currentUrl, config.afterSignInUrl!)) {\n return;\n }\n\n setIsLoadingSync(client.isLoading());\n };\n\n // Initial check\n checkLoadingState();\n\n // Set up an interval to check for loading state changes\n const interval: NodeJS.Timeout = setInterval(checkLoadingState, 100);\n\n return (): void => {\n clearInterval(interval);\n };\n }, [client, isLoadingSync, isSignedInSync, isUpdatingSession]);\n\n const signInSilently = async (options?: SignInOptions): Promise<User | boolean> => {\n try {\n setIsUpdatingSession(true);\n setIsLoadingSync(true);\n return await client.signInSilently(options);\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Error while signing in silently: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`,\n 'thunderid-signInSilently-Error',\n 'react',\n 'An error occurred while trying to sign in silently.',\n );\n } finally {\n setIsUpdatingSession(false);\n setIsLoadingSync(client.isLoading());\n }\n };\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile | null) => ({\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n const getDecodedIdToken: () => Promise<IdToken> = useCallback(\n async (): Promise<IdToken> => client.getDecodedIdToken(),\n [client],\n );\n\n const getIdToken: () => Promise<string> = useCallback(async (): Promise<string> => client.getIdToken(), [client]);\n\n const getAccessToken: () => Promise<string> = useCallback(\n async (): Promise<string> => client.getAccessToken(),\n [client],\n );\n\n const getStorageManager: () => Promise<any> = useCallback(async (): Promise<any> => {\n const storageManager: any = storageManagerRef.current ?? (await client.getStorageManager());\n if (storageManager) {\n storageManagerRef.current = storageManager;\n }\n return storageManager;\n }, [client]);\n\n const request: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.request(...args),\n [client],\n );\n\n const requestAll: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.requestAll(...args),\n [client],\n );\n\n const exchangeToken: (exchangeConfig: any) => Promise<any> = useCallback(\n async (exchangeConfig: any): Promise<any> => client.exchangeToken(exchangeConfig),\n [client],\n );\n\n const signOut: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.signOut(...args),\n [client],\n );\n\n const recover: (payload: any) => Promise<any> = useCallback(\n async (payload: any): Promise<any> => client.recover(payload),\n [client],\n );\n\n const signUp: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.signUp(...args),\n [client],\n );\n\n const clearSession: (...args: any[]) => Promise<any> = useCallback(\n async (...args: any[]): Promise<any> => client.clearSession(...args),\n [client],\n );\n\n const reInitialize: (reInitConfig: any) => Promise<any> = useCallback(\n async (reInitConfig: any): Promise<any> => {\n const result: any = await client.reInitialize(reInitConfig);\n\n // Refresh React state from the reinitialized client so the context (config.endpoints,\n // baseUrl, discovery) reflects the new configuration instead of the pre-reinitialization state.\n const reinitializedConfig: ThunderIDReactConfig = await client.getConfiguration();\n setConfig(reinitializedConfig);\n setBaseUrl(reinitializedConfig.baseUrl ?? '');\n setWellKnown(await client.getDiscoveryResponse());\n\n return result;\n },\n [client],\n );\n\n const value: any = useMemo(\n () => ({\n afterSignInUrl: config.afterSignInUrl,\n applicationId: config.applicationId,\n baseUrl,\n endpoints: config.endpoints,\n scopes: config.scopes,\n clearSession,\n clientId,\n discovery: {\n wellKnown,\n },\n exchangeToken,\n getAccessToken,\n getDecodedIdToken,\n getIdToken,\n getStorageManager,\n http: {\n request,\n requestAll,\n },\n instanceId,\n isInitialized: isInitializedSync,\n isLoading: isLoadingSync,\n isSignedIn: isSignedInSync,\n organizationChain,\n organizationHandle: config?.organizationHandle,\n preferences,\n reInitialize,\n recover,\n signIn,\n signInOptions,\n tokenRequest,\n signInSilently,\n signInUrl,\n signOut,\n signUp,\n signUpUrl,\n syncSession,\n user,\n userSchema,\n vendor: getVendorPrefix(config.vendor),\n }),\n [\n applicationId,\n config?.organizationHandle,\n config.vendor,\n config.afterSignInUrl,\n config.endpoints,\n config.scopes,\n signInUrl,\n signUpUrl,\n baseUrl,\n clientId,\n wellKnown,\n isInitializedSync,\n isLoadingSync,\n isSignedInSync,\n signIn,\n signInSilently,\n user,\n userSchema,\n client,\n signInOptions,\n tokenRequest,\n syncSession,\n getDecodedIdToken,\n clearSession,\n exchangeToken,\n getAccessToken,\n getStorageManager,\n instanceId,\n organizationChain,\n preferences,\n recover,\n reInitialize,\n request,\n requestAll,\n signOut,\n signUp,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={value}>\n <I18nProvider preferences={preferences?.i18n} vendor={getVendorPrefix(config.vendor)}>\n <FlowMetaProvider enabled={preferences?.resolveFromMeta !== false} namespace={namespace}>\n <ThemeProvider\n theme={{\n ...preferences?.theme?.overrides,\n direction: preferences?.theme?.direction,\n }}\n >\n <FlowProvider>\n <UserProvider\n profile={userProfile as UserProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n >\n <ComponentRendererProvider renderers={(extensions?.components?.renderers ?? {}) as any}>\n {children}\n </ComponentRendererProvider>\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDProvider;\n"],"mappings":"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"}
|
package/dist/hooks/useForm.d.ts
CHANGED
|
@@ -136,6 +136,11 @@ export interface UseFormReturn<T extends Record<string, string>> {
|
|
|
136
136
|
* Validate all fields
|
|
137
137
|
*/
|
|
138
138
|
validateForm: () => ValidationResult;
|
|
139
|
+
/**
|
|
140
|
+
* Re-run validation for all touched fields that have a client-side config,
|
|
141
|
+
* refreshing stored error strings to the current language.
|
|
142
|
+
*/
|
|
143
|
+
revalidateTouchedFields: () => void;
|
|
139
144
|
/**
|
|
140
145
|
* Current form values
|
|
141
146
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/useForm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/useForm.ts
|
|
4
4
|
/**
|
|
@@ -63,6 +63,7 @@ const useForm = (config = {}) => {
|
|
|
63
63
|
const [touched, setFormTouched] = useState({});
|
|
64
64
|
const [errors, setFormErrors] = useState({});
|
|
65
65
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
66
|
+
const clientErrorFieldRef = useRef(/* @__PURE__ */ new Set());
|
|
66
67
|
const getFieldConfig = useCallback((name) => fields.find((field) => field.name === name), [fields]);
|
|
67
68
|
const validateField = useCallback((name) => {
|
|
68
69
|
return computeFieldError(values[name] || "", getFieldConfig(name), requiredMessage);
|
|
@@ -71,6 +72,27 @@ const useForm = (config = {}) => {
|
|
|
71
72
|
getFieldConfig,
|
|
72
73
|
requiredMessage
|
|
73
74
|
]);
|
|
75
|
+
const validateFieldRef = useRef(validateField);
|
|
76
|
+
validateFieldRef.current = validateField;
|
|
77
|
+
const revalidateTouchedFields = useCallback(() => {
|
|
78
|
+
setFormErrors((prevErrors) => {
|
|
79
|
+
if (Object.keys(prevErrors).length === 0) return prevErrors;
|
|
80
|
+
const newErrors = { ...prevErrors };
|
|
81
|
+
let changed = false;
|
|
82
|
+
Object.keys(prevErrors).forEach((name) => {
|
|
83
|
+
if (!clientErrorFieldRef.current.has(name)) return;
|
|
84
|
+
const freshError = validateFieldRef.current(name);
|
|
85
|
+
if (freshError === prevErrors[name]) return;
|
|
86
|
+
changed = true;
|
|
87
|
+
if (freshError) newErrors[name] = freshError;
|
|
88
|
+
else {
|
|
89
|
+
delete newErrors[name];
|
|
90
|
+
clientErrorFieldRef.current.delete(name);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return changed ? newErrors : prevErrors;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
74
96
|
const validateForm = useCallback(() => {
|
|
75
97
|
const newErrors = {};
|
|
76
98
|
fields.forEach((field) => {
|
|
@@ -103,8 +125,13 @@ const useForm = (config = {}) => {
|
|
|
103
125
|
const error = computeFieldError(value, getFieldConfig(name), requiredMessage);
|
|
104
126
|
setFormErrors((prev) => {
|
|
105
127
|
const newErrors = { ...prev };
|
|
106
|
-
if (error)
|
|
107
|
-
|
|
128
|
+
if (error) {
|
|
129
|
+
newErrors[name] = error;
|
|
130
|
+
clientErrorFieldRef.current.add(name);
|
|
131
|
+
} else {
|
|
132
|
+
delete newErrors[name];
|
|
133
|
+
clientErrorFieldRef.current.delete(name);
|
|
134
|
+
}
|
|
108
135
|
return newErrors;
|
|
109
136
|
});
|
|
110
137
|
}, [
|
|
@@ -129,8 +156,13 @@ const useForm = (config = {}) => {
|
|
|
129
156
|
const error = validateField(name);
|
|
130
157
|
setFormErrors((prev) => {
|
|
131
158
|
const newErrors = { ...prev };
|
|
132
|
-
if (error)
|
|
133
|
-
|
|
159
|
+
if (error) {
|
|
160
|
+
newErrors[name] = error;
|
|
161
|
+
clientErrorFieldRef.current.add(name);
|
|
162
|
+
} else {
|
|
163
|
+
delete newErrors[name];
|
|
164
|
+
clientErrorFieldRef.current.delete(name);
|
|
165
|
+
}
|
|
134
166
|
return newErrors;
|
|
135
167
|
});
|
|
136
168
|
}
|
|
@@ -146,7 +178,9 @@ const useForm = (config = {}) => {
|
|
|
146
178
|
acc[field.name] = true;
|
|
147
179
|
return acc;
|
|
148
180
|
}, {}));
|
|
149
|
-
|
|
181
|
+
const validation = validateForm();
|
|
182
|
+
setFormErrors(validation.errors);
|
|
183
|
+
clientErrorFieldRef.current = new Set(Object.keys(validation.errors));
|
|
150
184
|
}, [fields, validateForm]);
|
|
151
185
|
const setError = useCallback((name, error) => {
|
|
152
186
|
setFormErrors((prev) => ({
|
|
@@ -162,12 +196,14 @@ const useForm = (config = {}) => {
|
|
|
162
196
|
}, []);
|
|
163
197
|
const clearErrors = useCallback(() => {
|
|
164
198
|
setFormErrors({});
|
|
199
|
+
clientErrorFieldRef.current.clear();
|
|
165
200
|
}, []);
|
|
166
201
|
const reset = useCallback(() => {
|
|
167
202
|
setFormValues({ ...initialValues });
|
|
168
203
|
setFormTouched({});
|
|
169
204
|
setFormErrors({});
|
|
170
205
|
setIsSubmitted(false);
|
|
206
|
+
clientErrorFieldRef.current.clear();
|
|
171
207
|
}, [initialValues]);
|
|
172
208
|
const handleSubmit = useCallback((onSubmit) => async (e) => {
|
|
173
209
|
if (e) e.preventDefault();
|
|
@@ -205,6 +241,7 @@ const useForm = (config = {}) => {
|
|
|
205
241
|
isSubmitted,
|
|
206
242
|
isValid,
|
|
207
243
|
reset,
|
|
244
|
+
revalidateTouchedFields,
|
|
208
245
|
setError,
|
|
209
246
|
setErrors,
|
|
210
247
|
setTouched,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.js","names":["fieldError: string | null","getFieldConfig: (name: keyof T) => FormField | undefined","validateField: (name: keyof T) => string | null","validateForm: () => ValidationResult","newErrors: Record<keyof T, string>","error: string | null","globalErrors: Record<string, string>","isValid: boolean","setValue: (name: keyof T, value: string) => void","setValues: (newValues: Partial<T>) => void","setTouched: (name: keyof T, isTouched?: boolean) => void","setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void","touchAllFields: () => void","setError: (name: keyof T, error: string) => void","setErrors: (newErrors: Partial<Record<keyof T, string>>) => void","clearErrors: () => void","reset: () => void","handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>","fieldConfig: FormField | undefined"],"sources":["../../src/hooks/useForm.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {useState, useCallback, FormEvent} from 'react';\n\n/**\n * Generic form field configuration\n */\nexport interface FormField {\n initialValue?: string;\n name: string;\n required?: boolean;\n validator?: (value: string) => string | null;\n}\n\n/**\n * Form validation result\n */\nexport interface ValidationResult {\n errors: Record<string, string>;\n isValid: boolean;\n}\n\n/**\n * Configuration for the useForm hook\n */\nexport interface UseFormConfig<T extends Record<string, string>> {\n /**\n * Form field definitions\n */\n fields?: FormField[];\n /**\n * Initial form values\n */\n initialValues?: Partial<T>;\n /**\n * Custom required field validation message\n */\n requiredMessage?: string;\n /**\n * When a field has been touched (blurred at least once), re-run validation on every\n * subsequent change so a rendered error clears the moment the value becomes valid.\n *\n * Independent of `validateOnChange`: this only affects fields that have already been\n * blurred, so users don't see errors while initially typing. Default `false` to\n * preserve prior behavior; enable per-form for a \"correct-as-you-type\" UX.\n */\n revalidateOnChangeAfterBlur?: boolean;\n /**\n * Whether to validate on blur (default: true)\n */\n validateOnBlur?: boolean;\n /**\n * Whether to validate on change (default: false). When true, every keystroke\n * triggers validation — including the first, which surfaces errors before the user\n * has had a chance to finish typing. Prefer `revalidateOnChangeAfterBlur` for the\n * common \"clear the error as the user corrects\" UX.\n */\n validateOnChange?: boolean;\n /**\n * Global form validator function\n */\n validator?: (values: T) => Record<string, string>;\n}\n\n/**\n * Pure per-field validator. Kept outside `useForm` so `setValue` can validate against\n * the value it just wrote — inline validation via the closure-based `validateField`\n * inside the hook would read stale `values` (state is committed asynchronously).\n */\nconst computeFieldError = (\n value: string,\n fieldConfig: FormField | undefined,\n requiredMessage: string,\n): string | null => {\n if (fieldConfig?.required && (!value || value.trim() === '')) {\n return requiredMessage;\n }\n if (fieldConfig?.validator) {\n const fieldError: string | null = fieldConfig.validator(value);\n if (fieldError) return fieldError;\n }\n return null;\n};\n\n/**\n * Return type for the useForm hook\n */\nexport interface UseFormReturn<T extends Record<string, string>> {\n /**\n * Clear all errors\n */\n clearErrors: () => void;\n /**\n * Current validation errors\n */\n errors: Record<keyof T, string>;\n /**\n * Get field props for easy integration with form components\n */\n getFieldProps: (name: keyof T) => {\n error: string | undefined;\n name: keyof T;\n onBlur: () => void;\n onChange: (value: string) => void;\n required: boolean;\n touched: boolean;\n value: string;\n };\n /**\n * Handle form submission\n */\n handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>;\n /**\n * Whether the form has been submitted\n */\n isSubmitted: boolean;\n /**\n * Whether the form is currently valid\n */\n isValid: boolean;\n /**\n * Reset the form to initial values\n */\n reset: () => void;\n /**\n * Set a field error\n */\n setError: (name: keyof T, error: string) => void;\n /**\n * Set multiple field errors\n */\n setErrors: (errors: Partial<Record<keyof T, string>>) => void;\n /**\n * Mark a field as touched\n */\n setTouched: (name: keyof T, touched?: boolean) => void;\n /**\n * Mark multiple fields as touched\n */\n setTouchedFields: (touched: Partial<Record<keyof T, boolean>>) => void;\n /**\n * Set a single field value\n */\n setValue: (name: keyof T, value: string) => void;\n /**\n * Set multiple field values\n */\n setValues: (values: Partial<T>) => void;\n /**\n * Mark all fields as touched\n */\n touchAllFields: () => void;\n /**\n * Fields that have been touched by the user\n */\n touched: Record<keyof T, boolean>;\n /**\n * Validate a single field\n */\n validateField: (name: keyof T) => string | null;\n /**\n * Validate all fields\n */\n validateForm: () => ValidationResult;\n /**\n * Current form values\n */\n values: T;\n}\n\n/**\n * Generic form hook that provides comprehensive form state management and validation.\n *\n * @template T - The type of form values (must extend Record<string, string>)\n * @param config - Configuration options for the form\n * @returns Form state and methods\n *\n * @example\n * ```tsx\n * interface LoginForm {\n * username: string;\n * password: string;\n * }\n *\n * const {\n * values,\n * touched,\n * errors,\n * isValid,\n * setValue,\n * handleSubmit,\n * getFieldProps\n * } = useForm<LoginForm>({\n * initialValues: { username: '', password: '' },\n * fields: [\n * { name: 'username', required: true },\n * { name: 'password', required: true }\n * ]\n * });\n *\n * const onSubmit = handleSubmit((values) => {\n * console.log('Form submitted:', values);\n * });\n *\n * return (\n * <form onSubmit={onSubmit}>\n * <input {...getFieldProps('username')} />\n * <input {...getFieldProps('password')} type=\"password\" />\n * <button type=\"submit\" disabled={!isValid}>Submit</button>\n * </form>\n * );\n * ```\n */\nexport const useForm = <T extends Record<string, string>>(config: UseFormConfig<T> = {}): UseFormReturn<T> => {\n const {\n initialValues = {} as T,\n fields = [],\n validator,\n validateOnChange = false,\n validateOnBlur = true,\n revalidateOnChangeAfterBlur = false,\n requiredMessage = 'This field is required',\n } = config;\n\n // Initialize form state\n const [values, setFormValues] = useState<T>({...initialValues} as T);\n const [touched, setFormTouched] = useState<Record<keyof T, boolean>>({} as Record<keyof T, boolean>);\n const [errors, setFormErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);\n const [isSubmitted, setIsSubmitted] = useState(false);\n\n // Get field configuration by name\n const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback(\n (name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name),\n [fields],\n );\n\n // Validate a single field against currently-committed state. Used by the blur path\n // (where `values` is already up-to-date) and by `validateForm`.\n const validateField: (name: keyof T) => string | null = useCallback(\n (name: keyof T): string | null => {\n const value: string = values[name] || '';\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n return computeFieldError(value, fieldConfig, requiredMessage);\n },\n [values, getFieldConfig, requiredMessage],\n );\n\n // Validate the entire form\n const validateForm: () => ValidationResult = useCallback((): ValidationResult => {\n const newErrors: Record<keyof T, string> = {} as Record<keyof T, string>;\n\n // Validate individual fields\n fields.forEach((field: FormField) => {\n const error: string | null = validateField(field.name as keyof T);\n if (error) {\n newErrors[field.name as keyof T] = error;\n }\n });\n\n // Run global validator if provided\n if (validator) {\n const globalErrors: Record<string, string> = validator(values);\n Object.keys(globalErrors).forEach((key: string) => {\n if (globalErrors[key]) {\n newErrors[key as keyof T] = globalErrors[key];\n }\n });\n }\n\n return {\n errors: newErrors,\n isValid: Object.keys(newErrors).length === 0,\n };\n }, [fields, validateField, validator, values]);\n\n // Check if form is currently valid\n const isValid: boolean = Object.keys(errors).length === 0;\n\n // Set a single field value.\n //\n // Validation policy:\n // - `validateOnChange: true` → validate on every keystroke.\n // - `revalidateOnChangeAfterBlur: true` → validate only if the field has\n // already been touched (blurred once),\n // so errors clear as the user corrects\n // without appearing while first typing.\n // - both false → no on-change validation.\n //\n // Validation runs against the NEXT value (the string being written) rather than\n // going through `validateField` which reads the stale closed-over `values`. This\n // prevents the one-keystroke-lag bug where the error clears one character late.\n const setValue: (name: keyof T, value: string) => void = useCallback(\n (name: keyof T, value: string): void => {\n setFormValues((prev: T) => ({\n ...prev,\n [name]: value,\n }));\n\n const shouldValidate: boolean = validateOnChange || (revalidateOnChangeAfterBlur && touched[name] === true);\n if (!shouldValidate) {\n return;\n }\n\n const error: string | null = computeFieldError(value, getFieldConfig(name), requiredMessage);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n } else {\n delete newErrors[name];\n }\n return newErrors;\n });\n },\n [validateOnChange, revalidateOnChangeAfterBlur, touched, getFieldConfig, requiredMessage],\n );\n\n // Set multiple field values\n const setValues: (newValues: Partial<T>) => void = useCallback((newValues: Partial<T>): void => {\n setFormValues((prev: T) => ({\n ...prev,\n ...newValues,\n }));\n }, []);\n\n // Mark a field as touched\n const setTouched: (name: keyof T, isTouched?: boolean) => void = useCallback(\n (name: keyof T, isTouched = true): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n [name]: isTouched,\n }));\n\n // Validate on blur if enabled and field is touched\n if (validateOnBlur && isTouched) {\n const error: string | null = validateField(name);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n } else {\n delete newErrors[name];\n }\n return newErrors;\n });\n }\n },\n [validateField, validateOnBlur],\n );\n\n // Set multiple touched fields\n const setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void = useCallback(\n (touchedFields: Partial<Record<keyof T, boolean>>): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n ...touchedFields,\n }));\n },\n [],\n );\n\n // Mark all fields as touched\n const touchAllFields: () => void = useCallback((): void => {\n const allTouched: Record<keyof T, boolean> = fields.reduce(\n (acc: Record<keyof T, boolean>, field: FormField) => {\n acc[field.name as keyof T] = true;\n return acc;\n },\n {} as Record<keyof T, boolean>,\n );\n\n setFormTouched(allTouched);\n\n // Validate all fields\n const validation: ValidationResult = validateForm();\n setFormErrors(validation.errors as Record<keyof T, string>);\n }, [fields, validateForm]);\n\n // Set a field error\n const setError: (name: keyof T, error: string) => void = useCallback((name: keyof T, error: string): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n [name]: error,\n }));\n }, []);\n\n // Set multiple field errors\n const setErrors: (newErrors: Partial<Record<keyof T, string>>) => void = useCallback(\n (newErrors: Partial<Record<keyof T, string>>): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n ...newErrors,\n }));\n },\n [],\n );\n\n // Clear all errors\n const clearErrors: () => void = useCallback((): void => {\n setFormErrors({} as Record<keyof T, string>);\n }, []);\n\n // Reset form to initial state\n const reset: () => void = useCallback((): void => {\n setFormValues({...initialValues} as T);\n setFormTouched({} as Record<keyof T, boolean>);\n setFormErrors({} as Record<keyof T, string>);\n setIsSubmitted(false);\n }, [initialValues]);\n\n // Handle form submission\n const handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void> = useCallback(\n (onSubmit: (values: T) => void | Promise<void>) =>\n async (e?: FormEvent): Promise<void> => {\n if (e) {\n e.preventDefault();\n }\n\n setIsSubmitted(true);\n touchAllFields();\n\n const validation: ValidationResult = validateForm();\n\n if (validation.isValid) {\n await onSubmit(values);\n }\n },\n [values, touchAllFields, validateForm],\n );\n\n // Get field props for easy integration\n const getFieldProps: (name: keyof T) => any = useCallback(\n (name: keyof T) => {\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n\n return {\n error: touched[name] ? errors[name] : undefined,\n name,\n onBlur: (): void => setTouched(name, true),\n onChange: (value: string): void => setValue(name, value),\n required: fieldConfig?.required || false,\n touched: touched[name] || false,\n value: values[name] || '',\n };\n },\n [values, errors, touched, setValue, setTouched, getFieldConfig],\n );\n\n return {\n clearErrors,\n errors,\n getFieldProps,\n handleSubmit,\n isSubmitted,\n isValid,\n reset,\n setError,\n setErrors,\n setTouched,\n setTouchedFields,\n setValue,\n setValues,\n touchAllFields,\n touched,\n validateField,\n validateForm,\n values,\n };\n};\n\nexport default useForm;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useForm.js","names":["fieldError: string | null","getFieldConfig: (name: keyof T) => FormField | undefined","validateField: (name: keyof T) => string | null","revalidateTouchedFields: () => void","newErrors: Record<keyof T, string>","freshError: string | null","validateForm: () => ValidationResult","error: string | null","globalErrors: Record<string, string>","isValid: boolean","setValue: (name: keyof T, value: string) => void","setValues: (newValues: Partial<T>) => void","setTouched: (name: keyof T, isTouched?: boolean) => void","setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void","touchAllFields: () => void","validation: ValidationResult","setError: (name: keyof T, error: string) => void","setErrors: (newErrors: Partial<Record<keyof T, string>>) => void","clearErrors: () => void","reset: () => void","handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>","fieldConfig: FormField | undefined"],"sources":["../../src/hooks/useForm.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {useState, useCallback, useRef, FormEvent} from 'react';\n\n/**\n * Generic form field configuration\n */\nexport interface FormField {\n initialValue?: string;\n name: string;\n required?: boolean;\n validator?: (value: string) => string | null;\n}\n\n/**\n * Form validation result\n */\nexport interface ValidationResult {\n errors: Record<string, string>;\n isValid: boolean;\n}\n\n/**\n * Configuration for the useForm hook\n */\nexport interface UseFormConfig<T extends Record<string, string>> {\n /**\n * Form field definitions\n */\n fields?: FormField[];\n /**\n * Initial form values\n */\n initialValues?: Partial<T>;\n /**\n * Custom required field validation message\n */\n requiredMessage?: string;\n /**\n * When a field has been touched (blurred at least once), re-run validation on every\n * subsequent change so a rendered error clears the moment the value becomes valid.\n *\n * Independent of `validateOnChange`: this only affects fields that have already been\n * blurred, so users don't see errors while initially typing. Default `false` to\n * preserve prior behavior; enable per-form for a \"correct-as-you-type\" UX.\n */\n revalidateOnChangeAfterBlur?: boolean;\n /**\n * Whether to validate on blur (default: true)\n */\n validateOnBlur?: boolean;\n /**\n * Whether to validate on change (default: false). When true, every keystroke\n * triggers validation — including the first, which surfaces errors before the user\n * has had a chance to finish typing. Prefer `revalidateOnChangeAfterBlur` for the\n * common \"clear the error as the user corrects\" UX.\n */\n validateOnChange?: boolean;\n /**\n * Global form validator function\n */\n validator?: (values: T) => Record<string, string>;\n}\n\n/**\n * Pure per-field validator. Kept outside `useForm` so `setValue` can validate against\n * the value it just wrote — inline validation via the closure-based `validateField`\n * inside the hook would read stale `values` (state is committed asynchronously).\n */\nconst computeFieldError = (\n value: string,\n fieldConfig: FormField | undefined,\n requiredMessage: string,\n): string | null => {\n if (fieldConfig?.required && (!value || value.trim() === '')) {\n return requiredMessage;\n }\n if (fieldConfig?.validator) {\n const fieldError: string | null = fieldConfig.validator(value);\n if (fieldError) return fieldError;\n }\n return null;\n};\n\n/**\n * Return type for the useForm hook\n */\nexport interface UseFormReturn<T extends Record<string, string>> {\n /**\n * Clear all errors\n */\n clearErrors: () => void;\n /**\n * Current validation errors\n */\n errors: Record<keyof T, string>;\n /**\n * Get field props for easy integration with form components\n */\n getFieldProps: (name: keyof T) => {\n error: string | undefined;\n name: keyof T;\n onBlur: () => void;\n onChange: (value: string) => void;\n required: boolean;\n touched: boolean;\n value: string;\n };\n /**\n * Handle form submission\n */\n handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>;\n /**\n * Whether the form has been submitted\n */\n isSubmitted: boolean;\n /**\n * Whether the form is currently valid\n */\n isValid: boolean;\n /**\n * Reset the form to initial values\n */\n reset: () => void;\n /**\n * Set a field error\n */\n setError: (name: keyof T, error: string) => void;\n /**\n * Set multiple field errors\n */\n setErrors: (errors: Partial<Record<keyof T, string>>) => void;\n /**\n * Mark a field as touched\n */\n setTouched: (name: keyof T, touched?: boolean) => void;\n /**\n * Mark multiple fields as touched\n */\n setTouchedFields: (touched: Partial<Record<keyof T, boolean>>) => void;\n /**\n * Set a single field value\n */\n setValue: (name: keyof T, value: string) => void;\n /**\n * Set multiple field values\n */\n setValues: (values: Partial<T>) => void;\n /**\n * Mark all fields as touched\n */\n touchAllFields: () => void;\n /**\n * Fields that have been touched by the user\n */\n touched: Record<keyof T, boolean>;\n /**\n * Validate a single field\n */\n validateField: (name: keyof T) => string | null;\n /**\n * Validate all fields\n */\n validateForm: () => ValidationResult;\n /**\n * Re-run validation for all touched fields that have a client-side config,\n * refreshing stored error strings to the current language.\n */\n revalidateTouchedFields: () => void;\n /**\n * Current form values\n */\n values: T;\n}\n\n/**\n * Generic form hook that provides comprehensive form state management and validation.\n *\n * @template T - The type of form values (must extend Record<string, string>)\n * @param config - Configuration options for the form\n * @returns Form state and methods\n *\n * @example\n * ```tsx\n * interface LoginForm {\n * username: string;\n * password: string;\n * }\n *\n * const {\n * values,\n * touched,\n * errors,\n * isValid,\n * setValue,\n * handleSubmit,\n * getFieldProps\n * } = useForm<LoginForm>({\n * initialValues: { username: '', password: '' },\n * fields: [\n * { name: 'username', required: true },\n * { name: 'password', required: true }\n * ]\n * });\n *\n * const onSubmit = handleSubmit((values) => {\n * console.log('Form submitted:', values);\n * });\n *\n * return (\n * <form onSubmit={onSubmit}>\n * <input {...getFieldProps('username')} />\n * <input {...getFieldProps('password')} type=\"password\" />\n * <button type=\"submit\" disabled={!isValid}>Submit</button>\n * </form>\n * );\n * ```\n */\nexport const useForm = <T extends Record<string, string>>(config: UseFormConfig<T> = {}): UseFormReturn<T> => {\n const {\n initialValues = {} as T,\n fields = [],\n validator,\n validateOnChange = false,\n validateOnBlur = true,\n revalidateOnChangeAfterBlur = false,\n requiredMessage = 'This field is required',\n } = config;\n\n // Initialize form state\n const [values, setFormValues] = useState<T>({...initialValues} as T);\n const [touched, setFormTouched] = useState<Record<keyof T, boolean>>({} as Record<keyof T, boolean>);\n const [errors, setFormErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);\n const [isSubmitted, setIsSubmitted] = useState(false);\n\n // Ref to track which fields have client-side validation rules. Errors injected via\n // serErrors are not added here, so revalidateTouchedFields preserves server-side errors.\n const clientErrorFieldRef = useRef(new Set<keyof T>());\n // Get field configuration by name\n const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback(\n (name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name),\n [fields],\n );\n\n // Validate a single field against currently-committed state. Used by the blur path\n // (where `values` is already up-to-date) and by `validateForm`.\n const validateField: (name: keyof T) => string | null = useCallback(\n (name: keyof T): string | null => {\n const value: string = values[name] || '';\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n return computeFieldError(value, fieldConfig, requiredMessage);\n },\n [values, getFieldConfig, requiredMessage],\n );\n\n // \"Latest value\" ref so revalidateTouchedFields can read fresh validators without\n // appearing in any effect dep array (avoids exhaustive-deps violations at call sites).\n const validateFieldRef = useRef(validateField);\n validateFieldRef.current = validateField;\n\n // Re-translate all, client-validated error strings on language change.\n // Stable identity ([] deps): uses only refs and the stable state setter.\n const revalidateTouchedFields: () => void = useCallback((): void => {\n setFormErrors((prevErrors: Record<keyof T, string>) => {\n if (Object.keys(prevErrors).length === 0) return prevErrors;\n\n const newErrors: Record<keyof T, string> = {...prevErrors};\n let changed = false;\n\n (Object.keys(prevErrors) as Array<keyof T>).forEach((name: keyof T) => {\n // Skip errors that were not produced by client-side validation - preserve server messages.\n if (!clientErrorFieldRef.current.has(name)) return;\n\n const freshError: string | null = validateFieldRef.current(name);\n if (freshError === prevErrors[name]) return;\n\n changed = true;\n if (freshError) {\n newErrors[name] = freshError;\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n });\n\n return changed ? newErrors : prevErrors;\n });\n }, []);\n\n // Validate the entire form\n const validateForm: () => ValidationResult = useCallback((): ValidationResult => {\n const newErrors: Record<keyof T, string> = {} as Record<keyof T, string>;\n\n // Validate individual fields\n fields.forEach((field: FormField) => {\n const error: string | null = validateField(field.name as keyof T);\n if (error) {\n newErrors[field.name as keyof T] = error;\n }\n });\n\n // Run global validator if provided\n if (validator) {\n const globalErrors: Record<string, string> = validator(values);\n Object.keys(globalErrors).forEach((key: string) => {\n if (globalErrors[key]) {\n newErrors[key as keyof T] = globalErrors[key];\n }\n });\n }\n\n return {\n errors: newErrors,\n isValid: Object.keys(newErrors).length === 0,\n };\n }, [fields, validateField, validator, values]);\n\n // Check if form is currently valid\n const isValid: boolean = Object.keys(errors).length === 0;\n\n // Set a single field value.\n //\n // Validation policy:\n // - `validateOnChange: true` → validate on every keystroke.\n // - `revalidateOnChangeAfterBlur: true` → validate only if the field has\n // already been touched (blurred once),\n // so errors clear as the user corrects\n // without appearing while first typing.\n // - both false → no on-change validation.\n //\n // Validation runs against the NEXT value (the string being written) rather than\n // going through `validateField` which reads the stale closed-over `values`. This\n // prevents the one-keystroke-lag bug where the error clears one character late.\n const setValue: (name: keyof T, value: string) => void = useCallback(\n (name: keyof T, value: string): void => {\n setFormValues((prev: T) => ({\n ...prev,\n [name]: value,\n }));\n\n const shouldValidate: boolean = validateOnChange || (revalidateOnChangeAfterBlur && touched[name] === true);\n if (!shouldValidate) {\n return;\n }\n\n const error: string | null = computeFieldError(value, getFieldConfig(name), requiredMessage);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n clientErrorFieldRef.current.add(name);\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n return newErrors;\n });\n },\n [validateOnChange, revalidateOnChangeAfterBlur, touched, getFieldConfig, requiredMessage],\n );\n\n // Set multiple field values\n const setValues: (newValues: Partial<T>) => void = useCallback((newValues: Partial<T>): void => {\n setFormValues((prev: T) => ({\n ...prev,\n ...newValues,\n }));\n }, []);\n\n // Mark a field as touched\n const setTouched: (name: keyof T, isTouched?: boolean) => void = useCallback(\n (name: keyof T, isTouched = true): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n [name]: isTouched,\n }));\n\n // Validate on blur if enabled and field is touched\n if (validateOnBlur && isTouched) {\n const error: string | null = validateField(name);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n clientErrorFieldRef.current.add(name);\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n return newErrors;\n });\n }\n },\n [validateField, validateOnBlur],\n );\n\n // Set multiple touched fields\n const setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void = useCallback(\n (touchedFields: Partial<Record<keyof T, boolean>>): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n ...touchedFields,\n }));\n },\n [],\n );\n\n // Mark all fields as touched\n const touchAllFields: () => void = useCallback((): void => {\n const allTouched: Record<keyof T, boolean> = fields.reduce(\n (acc: Record<keyof T, boolean>, field: FormField) => {\n acc[field.name as keyof T] = true;\n return acc;\n },\n {} as Record<keyof T, boolean>,\n );\n\n setFormTouched(allTouched);\n\n // Validate all fields\n const validation: ValidationResult = validateForm();\n setFormErrors(validation.errors as Record<keyof T, string>);\n clientErrorFieldRef.current = new Set(Object.keys(validation.errors) as Array<keyof T>);\n }, [fields, validateForm]);\n\n // Set a field error\n const setError: (name: keyof T, error: string) => void = useCallback((name: keyof T, error: string): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n [name]: error,\n }));\n }, []);\n\n // Set multiple field errors\n const setErrors: (newErrors: Partial<Record<keyof T, string>>) => void = useCallback(\n (newErrors: Partial<Record<keyof T, string>>): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n ...newErrors,\n }));\n },\n [],\n );\n\n // Clear all errors\n const clearErrors: () => void = useCallback((): void => {\n setFormErrors({} as Record<keyof T, string>);\n clientErrorFieldRef.current.clear();\n }, []);\n\n // Reset form to initial state\n const reset: () => void = useCallback((): void => {\n setFormValues({...initialValues} as T);\n setFormTouched({} as Record<keyof T, boolean>);\n setFormErrors({} as Record<keyof T, string>);\n setIsSubmitted(false);\n clientErrorFieldRef.current.clear();\n }, [initialValues]);\n\n // Handle form submission\n const handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void> = useCallback(\n (onSubmit: (values: T) => void | Promise<void>) =>\n async (e?: FormEvent): Promise<void> => {\n if (e) {\n e.preventDefault();\n }\n\n setIsSubmitted(true);\n touchAllFields();\n\n const validation: ValidationResult = validateForm();\n\n if (validation.isValid) {\n await onSubmit(values);\n }\n },\n [values, touchAllFields, validateForm],\n );\n\n // Get field props for easy integration\n const getFieldProps: (name: keyof T) => any = useCallback(\n (name: keyof T) => {\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n\n return {\n error: touched[name] ? errors[name] : undefined,\n name,\n onBlur: (): void => setTouched(name, true),\n onChange: (value: string): void => setValue(name, value),\n required: fieldConfig?.required || false,\n touched: touched[name] || false,\n value: values[name] || '',\n };\n },\n [values, errors, touched, setValue, setTouched, getFieldConfig],\n );\n\n return {\n clearErrors,\n errors,\n getFieldProps,\n handleSubmit,\n isSubmitted,\n isValid,\n reset,\n revalidateTouchedFields,\n setError,\n setErrors,\n setTouched,\n setTouchedFields,\n setValue,\n setValues,\n touchAllFields,\n touched,\n validateField,\n validateForm,\n values,\n };\n};\n\nexport default useForm;\n"],"mappings":"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"}
|
package/dist/models/config.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export type ThunderIDReactConfig = ThunderIDBrowserConfig & {
|
|
|
8
8
|
* own CSP header/meta tag issues for the current request.
|
|
9
9
|
*/
|
|
10
10
|
cspNonce?: string;
|
|
11
|
+
/** Forwarded to `FlowMetaProvider`'s `namespace` prop. See its doc for details. */
|
|
12
|
+
namespace?: string;
|
|
11
13
|
};
|
|
12
14
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/models/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/models/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveTranslationsInObject.d.ts","sourceRoot":"","sources":["../../src/utils/resolveTranslationsInObject.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,oBAAoB,EAA8B,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAC,cAAc,EAAC,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACvE,KAAK,CAAC,EACN,GAAG,cAAc,CAAC,GAAG,CAAC,EACtB,aAAY,MAAM,
|
|
1
|
+
{"version":3,"file":"resolveTranslationsInObject.d.ts","sourceRoot":"","sources":["../../src/utils/resolveTranslationsInObject.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,oBAAoB,EAA8B,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAC,cAAc,EAAC,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACvE,KAAK,CAAC,EACN,GAAG,cAAc,CAAC,GAAG,CAAC,EACtB,aAAY,MAAM,EAAwE,EAC1F,OAAO,oBAAoB,GAAG,IAAI,KACjC,CAUF,CAAC;AAEF,eAAe,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveTranslationsInObject.js","names":["resolved: T"],"sources":["../../src/utils/resolveTranslationsInObject.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {FlowMetadataResponse, resolveFlowTemplateLiterals} from '@thunderid/browser';\nimport {UseTranslation} from '../hooks/useTranslation';\n\n/**\n * Resolves all {{ t() }} and {{ meta() }} template expressions in an object's string properties.\n * @param obj - The object to process\n * @param t - The translation function from useTranslation\n * @param properties - Array of property names to resolve (optional, defaults to common properties)\n * @param meta - Optional flow metadata for resolving meta() expressions\n * @returns A new object with resolved template strings\n */\nexport const resolveTranslationsInObject = <T extends Record<string, any>>(\n obj: T,\n t: UseTranslation['t'],\n properties: string[] = ['label', 'placeholder', 'text', 'title', 'subtitle'],\n meta?: FlowMetadataResponse | null,\n): T => {\n const resolved: T = {...obj};\n\n properties.forEach((prop: string) => {\n if (resolved[prop] && typeof resolved[prop] === 'string') {\n (resolved as any)[prop] = resolveFlowTemplateLiterals(resolved[prop], {meta, t});\n }\n });\n\n return resolved;\n};\n\nexport default resolveTranslationsInObject;\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,+BACX,KACA,GACA,aAAuB;CAAC;CAAS;CAAe;CAAQ;CAAS;
|
|
1
|
+
{"version":3,"file":"resolveTranslationsInObject.js","names":["resolved: T"],"sources":["../../src/utils/resolveTranslationsInObject.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {FlowMetadataResponse, resolveFlowTemplateLiterals} from '@thunderid/browser';\nimport {UseTranslation} from '../hooks/useTranslation';\n\n/**\n * Resolves all {{ t() }} and {{ meta() }} template expressions in an object's string properties.\n * @param obj - The object to process\n * @param t - The translation function from useTranslation\n * @param properties - Array of property names to resolve (optional, defaults to common properties)\n * @param meta - Optional flow metadata for resolving meta() expressions\n * @returns A new object with resolved template strings\n */\nexport const resolveTranslationsInObject = <T extends Record<string, any>>(\n obj: T,\n t: UseTranslation['t'],\n properties: string[] = ['label', 'placeholder', 'text', 'title', 'subtitle', 'alt', 'src'],\n meta?: FlowMetadataResponse | null,\n): T => {\n const resolved: T = {...obj};\n\n properties.forEach((prop: string) => {\n if (resolved[prop] && typeof resolved[prop] === 'string') {\n (resolved as any)[prop] = resolveFlowTemplateLiterals(resolved[prop], {meta, t});\n }\n });\n\n return resolved;\n};\n\nexport default resolveTranslationsInObject;\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,+BACX,KACA,GACA,aAAuB;CAAC;CAAS;CAAe;CAAQ;CAAS;CAAY;CAAO;CAAM,EAC1F,SACM;CACN,MAAMA,WAAc,EAAC,GAAG,KAAI;AAE5B,YAAW,SAAS,SAAiB;AACnC,MAAI,SAAS,SAAS,OAAO,SAAS,UAAU,SAC9C,CAAC,SAAiB,QAAQ,4BAA4B,SAAS,OAAO;GAAC;GAAM;GAAE,CAAC;GAElF;AAEF,QAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thunderid/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "React SDK for ThunderID",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"thunderid",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@floating-ui/react": "0.27.12",
|
|
63
63
|
"dompurify": "3.4.13",
|
|
64
64
|
"tslib": "2.8.1",
|
|
65
|
-
"@thunderid/browser": "^1.0.
|
|
65
|
+
"@thunderid/browser": "^1.0.6"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|