diff --git a/.changeset/loose-breads-slide.md b/.changeset/loose-breads-slide.md new file mode 100644 index 00000000..73287a2e --- /dev/null +++ b/.changeset/loose-breads-slide.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/javascript': minor +'@asgardeo/react': minor +'@asgardeo/vue': minor +--- + +Rename flowId in flow execution to executionId diff --git a/packages/javascript/src/api/v2/executeEmbeddedSignInFlowV2.ts b/packages/javascript/src/api/v2/executeEmbeddedSignInFlowV2.ts index 470e03ac..186bc7da 100644 --- a/packages/javascript/src/api/v2/executeEmbeddedSignInFlowV2.ts +++ b/packages/javascript/src/api/v2/executeEmbeddedSignInFlowV2.ts @@ -51,7 +51,7 @@ const executeEmbeddedSignInFlowV2 = async ({ // `verbose: true` is required to get the `meta` field in the response that includes component details. // Add verbose:true if: // 1. payload contains only applicationId and flowType - // 2. payload contains only flowId + // 2. payload contains only executionId const hasOnlyAppIdAndFlowType: boolean = typeof cleanPayload === 'object' && cleanPayload !== null && @@ -61,7 +61,7 @@ const executeEmbeddedSignInFlowV2 = async ({ const hasOnlyFlowId: boolean = typeof cleanPayload === 'object' && cleanPayload !== null && - 'flowId' in cleanPayload && + 'executionId' in cleanPayload && Object.keys(cleanPayload).length === 1; const requestPayload: Record = diff --git a/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts b/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts index 360cc751..1aa1c7c6 100644 --- a/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts +++ b/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts @@ -51,7 +51,7 @@ const executeEmbeddedSignUpFlowV2 = async ({ // `verbose: true` is required to get the `meta` field in the response that includes component details. // Add verbose:true if: // 1. payload contains only applicationId and flowType - // 2. payload contains only flowId + // 2. payload contains only executionId const hasOnlyAppIdAndFlowType: boolean = typeof cleanPayload === 'object' && cleanPayload !== null && @@ -61,7 +61,7 @@ const executeEmbeddedSignUpFlowV2 = async ({ const hasOnlyFlowId: boolean = typeof cleanPayload === 'object' && cleanPayload !== null && - 'flowId' in cleanPayload && + 'executionId' in cleanPayload && Object.keys(cleanPayload).length === 1; const requestPayload: Record = diff --git a/packages/javascript/src/api/v2/executeEmbeddedUserOnboardingFlowV2.ts b/packages/javascript/src/api/v2/executeEmbeddedUserOnboardingFlowV2.ts index 949c3539..feb94333 100644 --- a/packages/javascript/src/api/v2/executeEmbeddedUserOnboardingFlowV2.ts +++ b/packages/javascript/src/api/v2/executeEmbeddedUserOnboardingFlowV2.ts @@ -40,14 +40,14 @@ export interface EmbeddedUserOnboardingFlowResponse { }; /** - * Reason for failure if flowStatus is ERROR. + * Unique identifier for the flow execution. */ - failureReason?: string; + executionId: string; /** - * Unique identifier for the flow execution. + * Reason for failure if flowStatus is ERROR. */ - flowId: string; + failureReason?: string; /** * Current status of the flow. @@ -88,7 +88,7 @@ export interface EmbeddedUserOnboardingFlowResponse { * const response = await executeEmbeddedUserOnboardingFlowV2({ * baseUrl: "https://api.thunder.io", * payload: { - * flowId: "flow-id-from-url", + * executionId: "flow-id-from-url", * inputs: { inviteToken: "token-from-url" } * } * }); @@ -128,10 +128,13 @@ const executeEmbeddedUserOnboardingFlowV2 = async ({ const hasOnlyFlowId: boolean = typeof cleanPayload === 'object' && cleanPayload !== null && - 'flowId' in cleanPayload && + 'executionId' in cleanPayload && Object.keys(cleanPayload).length === 1; const hasFlowIdWithInputs: boolean = - typeof cleanPayload === 'object' && cleanPayload !== null && 'flowId' in cleanPayload && 'inputs' in cleanPayload; + typeof cleanPayload === 'object' && + cleanPayload !== null && + 'executionId' in cleanPayload && + 'inputs' in cleanPayload; // Add verbose for initial requests and when continuing with inputs const requestPayload: Record = diff --git a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts index 37d73724..20f69338 100644 --- a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts @@ -145,7 +145,7 @@ export interface ExtendedEmbeddedSignInFlowResponse { * @example * ```typescript * const response: EmbeddedSignInFlowResponse = { - * flowId: "flow_12345", + * executionId: "flow_12345", * flowStatus: EmbeddedSignInFlowStatus.Incomplete, * type: EmbeddedSignInFlowType.View, * data: { @@ -197,16 +197,16 @@ export interface EmbeddedSignInFlowResponse extends ExtendedEmbeddedSignInFlowRe }; /** - * Optional reason for flow failure in case of an error. - * Provides additional context when flowStatus is set to ERROR. + * Unique identifier for this specific flow instance. + * Used to maintain state across multiple API calls during the authentication process. */ - failureReason?: string; + executionId: string; /** - * Unique identifier for this specific flow instance. - * Used to maintain state across multiple API calls during the authentication process. + * Optional reason for flow failure in case of an error. + * Provides additional context when flowStatus is set to ERROR. */ - flowId: string; + failureReason?: string; /** * Current status of the sign-in flow. @@ -292,14 +292,14 @@ export type EmbeddedSignInFlowInitiateRequest = { * Request payload for executing steps in Asgardeo embedded sign-in flows. * * This interface defines the structure for subsequent requests after flow initiation. - * It supports both continuing existing flows (with flowId) and submitting user + * It supports both continuing existing flows (with executionId) and submitting user * input data collected from the rendered components. * * @example * ```typescript * // Continue existing flow with user input * const stepRequest: EmbeddedSignInFlowRequest = { - * flowId: "flow_12345", + * executionId: "flow_12345", * action: "action_001", * inputs: { * username: "user@example.com", @@ -327,7 +327,7 @@ export interface EmbeddedSignInFlowRequest extends Partial { action?: string; - flowId?: string; + executionId?: string; inputs?: Record; } @@ -223,7 +223,7 @@ export interface EmbeddedSignUpFlowRequest extends Partial e typeof arg1 === 'object' && arg1 !== null && !isEmpty(arg1) && - ('flowId' in arg1 || 'applicationId' in arg1) + ('executionId' in arg1 || 'applicationId' in arg1) ) { const authIdFromUrl: string = new URL(window.location.href).searchParams.get('authId'); const authIdFromStorage: string = sessionStorage.getItem('asgardeo_auth_id'); diff --git a/packages/react/src/components/presentation/auth/AcceptInvite/v2/AcceptInvite.tsx b/packages/react/src/components/presentation/auth/AcceptInvite/v2/AcceptInvite.tsx index 918b0df5..d9bded4b 100644 --- a/packages/react/src/components/presentation/auth/AcceptInvite/v2/AcceptInvite.tsx +++ b/packages/react/src/components/presentation/auth/AcceptInvite/v2/AcceptInvite.tsx @@ -49,7 +49,7 @@ export interface AcceptInviteProps { * Flow ID from the invite link. * If not provided, will be extracted from URL query parameters. */ - flowId?: string; + executionId?: string; /** * Invite token from the invite link. @@ -101,14 +101,14 @@ export interface AcceptInviteProps { /** * Helper to extract query parameters from URL. */ -const getUrlParams = (): {flowId?: string; inviteToken?: string} => { +const getUrlParams = (): {executionId?: string; inviteToken?: string} => { if (typeof window === 'undefined') { return {}; } const params: any = new URLSearchParams(window.location.search); return { - flowId: params.get('flowId') || undefined, + executionId: params.get('executionId') || undefined, inviteToken: params.get('inviteToken') || undefined, }; }; @@ -118,7 +118,7 @@ const getUrlParams = (): {flowId?: string; inviteToken?: string} => { * * This component is designed for end users accessing the thunder-gate app via an invite link. * It automatically: - * 1. Extracts flowId and inviteToken from URL query parameters + * 1. Extracts executionId and inviteToken from URL query parameters * 2. Validates the invite token with the backend * 3. Displays the password form if token is valid * 4. Completes the accept invite when password is set @@ -127,7 +127,7 @@ const getUrlParams = (): {flowId?: string; inviteToken?: string} => { * ```tsx * import { AcceptInvite } from '@asgardeo/react'; * - * // URL: /invite?flowId=xxx&inviteToken=yyy + * // URL: /invite?executionId=xxx&inviteToken=yyy * * const AcceptInvitePage = () => { * return ( @@ -153,7 +153,7 @@ const getUrlParams = (): {flowId?: string; inviteToken?: string} => { */ const AcceptInvite: FC = ({ baseUrl, - flowId: flowIdProp, + executionId: executionIdProp, inviteToken: inviteTokenProp, onComplete, onError, @@ -167,9 +167,9 @@ const AcceptInvite: FC = ({ showSubtitle = true, }: AcceptInviteProps): ReactElement => { // Extract from URL if not provided as props - const {flowId: urlFlowId, inviteToken: urlInviteToken} = useMemo(() => getUrlParams(), []); + const {executionId: urlExecutionId, inviteToken: urlInviteToken} = useMemo(() => getUrlParams(), []); - const flowId: any = flowIdProp || urlFlowId; + const executionId: any = executionIdProp || urlExecutionId; const inviteToken: any = inviteTokenProp || urlInviteToken; // Determine base URL @@ -211,7 +211,7 @@ const AcceptInvite: FC = ({ return ( ; + executionId?: string; /** - * Current flow ID from URL. + * Field validation errors. */ - flowId?: string; + fieldErrors: Record; /** * Navigate to sign in page. @@ -170,7 +170,7 @@ export interface BaseAcceptInviteProps { /** * Flow ID from the invite link URL. */ - flowId?: string; + executionId?: string; /** * Invite token from the invite link URL. @@ -247,7 +247,7 @@ export interface BaseAcceptInviteProps { * 3. Flow completion */ const BaseAcceptInvite: FC = ({ - flowId, + executionId, inviteToken, onSubmit, onComplete, @@ -339,7 +339,7 @@ const BaseAcceptInvite: FC = ({ * This hook processes the authorization code and continues the flow. */ useOAuthCallback({ - currentFlowId: flowId ?? null, + currentExecutionId: executionId ?? null, isInitialized: true, onComplete: () => { setIsValidatingToken(false); @@ -457,7 +457,7 @@ const BaseAcceptInvite: FC = ({ const inputs: any = data || formValues; const payload: Record = { - flowId: currentFlow.flowId, + executionId: currentFlow.executionId, inputs, verbose: true, }; @@ -531,10 +531,10 @@ const BaseAcceptInvite: FC = ({ } // Validate required params for initial invite link - if (!flowId || !inviteToken) { + if (!executionId || !inviteToken) { setIsValidatingToken(false); setIsTokenInvalid(true); - handleError(new Error('Invalid invite link. Missing flowId or inviteToken.')); + handleError(new Error('Invalid invite link. Missing executionId or inviteToken.')); return; } @@ -545,14 +545,14 @@ const BaseAcceptInvite: FC = ({ setApiError(null); try { - // Store flowId in sessionStorage for OAuth callback - if (flowId) { - sessionStorage.setItem('asgardeo_flow_id', flowId); + // Store executionId in sessionStorage for OAuth callback + if (executionId) { + sessionStorage.setItem('asgardeo_execution_id', executionId); } // Send the invite token to validate and continue the flow const payload: any = { - flowId, + executionId, inputs: { inviteToken, }, @@ -579,7 +579,7 @@ const BaseAcceptInvite: FC = ({ setIsValidatingToken(false); } })(); - }, [flowId, inviteToken, onSubmit, onFlowChange, handleError, normalizeFlowResponseLocal]); + }, [executionId, inviteToken, onSubmit, onFlowChange, handleError, normalizeFlowResponseLocal]); /** * Extract title and subtitle from components. @@ -621,8 +621,8 @@ const BaseAcceptInvite: FC = ({ const renderProps: BaseAcceptInviteRenderProps = { components, error: apiError, + executionId, fieldErrors: formErrors, - flowId, goToSignIn: onGoToSignIn, handleInputBlur, handleInputChange, diff --git a/packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx b/packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx index 4626f69c..b0e9b74c 100644 --- a/packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx +++ b/packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx @@ -42,8 +42,8 @@ export interface InviteUserFlowResponse { components?: any[]; }; }; + executionId: string; failureReason?: string; - flowId: string; flowStatus: 'INCOMPLETE' | 'COMPLETE' | 'ERROR'; type?: 'VIEW' | 'REDIRECTION'; } @@ -67,15 +67,20 @@ export interface BaseInviteUserRenderProps { */ error?: Error | null; + /** + * Current flow execution ID. + */ + executionId?: string; + /** * Field validation errors. */ fieldErrors: Record; /** - * Current flow ID. + * Current flow execution ID. */ - flowId?: string; + flowExecId?: string; /** * Function to handle input blur. @@ -403,7 +408,7 @@ const BaseInviteUser: FC = ({ const inputs: any = data || formValues; const payload: Record = { - flowId: currentFlow.flowId, + executionId: currentFlow.executionId, inputs, verbose: true, }; @@ -579,8 +584,8 @@ const BaseInviteUser: FC = ({ additionalData: currentFlow?.data?.additionalData, components, error: apiError, + executionId: currentFlow?.executionId, fieldErrors: formErrors, - flowId: currentFlow?.flowId, handleInputBlur, handleInputChange, handleSubmit, diff --git a/packages/react/src/components/presentation/auth/InviteUser/v2/InviteUser.tsx b/packages/react/src/components/presentation/auth/InviteUser/v2/InviteUser.tsx index e40d9ec1..34425843 100644 --- a/packages/react/src/components/presentation/auth/InviteUser/v2/InviteUser.tsx +++ b/packages/react/src/components/presentation/auth/InviteUser/v2/InviteUser.tsx @@ -93,7 +93,7 @@ export interface InviteUserProps { * * return ( * setInviteLink(link)} + * onInviteLinkGenerated={(link, executionId) => setInviteLink(link)} * onError={(error) => console.error(error)} * > * {({ values, components, isLoading, handleInputChange, handleSubmit, inviteLink, isInviteGenerated }) => ( diff --git a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx index 3c26f576..274b0db7 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx @@ -142,7 +142,7 @@ interface PasskeyState { challenge: string | null; creationOptions: string | null; error: Error | null; - flowId: string | null; + executionId: string | null; isActive: boolean; } @@ -222,7 +222,7 @@ const SignIn: FC = ({ // State management for the flow const [components, setComponents] = useState([]); const [additionalData, setAdditionalData] = useState>({}); - const [currentFlowId, setCurrentFlowId] = useState(null); + const [currentExecutionId, setCurrentExecutionId] = useState(null); const [isFlowInitialized, setIsFlowInitialized] = useState(false); const [flowError, setFlowError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -232,22 +232,22 @@ const SignIn: FC = ({ challenge: null, creationOptions: null, error: null, - flowId: null, + executionId: null, isActive: false, }); const initializationAttemptedRef: any = useRef(false); const oauthCodeProcessedRef: any = useRef(false); const passkeyProcessedRef: any = useRef(false); /** - * Sets flowId between sessionStorage and state. + * Sets executionId between sessionStorage and state. * This ensures both are always in sync. */ - const setFlowId = (flowId: string | null): void => { - setCurrentFlowId(flowId); - if (flowId) { - sessionStorage.setItem('asgardeo_flow_id', flowId); + const setExecutionId = (executionId: string | null): void => { + setCurrentExecutionId(executionId); + if (executionId) { + sessionStorage.setItem('asgardeo_execution_id', executionId); } else { - sessionStorage.removeItem('asgardeo_flow_id'); + sessionStorage.removeItem('asgardeo_execution_id'); } }; @@ -255,7 +255,7 @@ const SignIn: FC = ({ * Clear all flow-related storage and state. */ const clearFlowState = (): void => { - setFlowId(null); + setExecutionId(null); setIsFlowInitialized(false); sessionStorage.removeItem('asgardeo_auth_id'); setIsTimeoutDisabled(false); @@ -275,7 +275,7 @@ const SignIn: FC = ({ code: urlParams.get('code'), error: urlParams.get('error'), errorDescription: urlParams.get('error_description'), - flowId: urlParams.get('flowId'), + executionId: urlParams.get('executionId'), nonce: urlParams.get('nonce'), state: urlParams.get('state'), }; @@ -307,13 +307,13 @@ const SignIn: FC = ({ }; /** - * Clean up flow-related URL parameters (flowId, authId) from the browser URL. - * Used after flowId is set in state to prevent using invalidated flowId from URL. + * Clean up flow-related URL parameters (executionId, authId) from the browser URL. + * Used after executionId is set in state to prevent using invalidated executionId from URL. */ const cleanupFlowUrlParams = (): void => { if (!window?.location?.href) return; const url: any = new URL(window.location.href); - url.searchParams.delete('flowId'); + url.searchParams.delete('executionId'); url.searchParams.delete('authId'); url.searchParams.delete('applicationId'); window?.history?.replaceState({}, '', url.toString()); @@ -349,8 +349,8 @@ const SignIn: FC = ({ const redirectURL: any = (response.data as any)?.redirectURL || (response as any)?.redirectURL; if (redirectURL && window?.location) { - if (response.flowId) { - setFlowId(response.flowId); + if (response.executionId) { + setExecutionId(response.executionId); } const urlParams: any = getUrlParams(); @@ -365,7 +365,7 @@ const SignIn: FC = ({ /** * Initialize the authentication flow. - * Priority: flowId > applicationId (from context) > applicationId (from URL) + * Priority: executionId > applicationId (from context) > applicationId (from URL) */ const initializeFlow = async (): Promise => { const urlParams: any = getUrlParams(); @@ -377,9 +377,9 @@ const SignIn: FC = ({ const effectiveApplicationId: any = applicationId || urlParams.applicationId; - if (!urlParams.flowId && !effectiveApplicationId) { + if (!urlParams.executionId && !effectiveApplicationId) { const error: any = new AsgardeoRuntimeError( - 'Either flowId or applicationId is required for authentication', + 'Either executionId or applicationId is required for authentication', 'SIGN_IN_ERROR', 'react', ); @@ -392,9 +392,9 @@ const SignIn: FC = ({ let response: EmbeddedSignInFlowResponseV2; - if (urlParams.flowId) { + if (urlParams.executionId) { response = (await signIn({ - flowId: urlParams.flowId, + executionId: urlParams.executionId, })) as EmbeddedSignInFlowResponseV2; } else { response = (await signIn({ @@ -408,7 +408,7 @@ const SignIn: FC = ({ } const { - flowId: normalizedFlowId, + executionId: normalizedExecutionId, components: normalizedComponents, additionalData: normalizedAdditionalData, } = normalizeFlowResponse( @@ -420,13 +420,13 @@ const SignIn: FC = ({ meta, ); - if (normalizedFlowId && normalizedComponents) { - setFlowId(normalizedFlowId); + if (normalizedExecutionId && normalizedComponents) { + setExecutionId(normalizedExecutionId); setComponents(normalizedComponents); setAdditionalData(normalizedAdditionalData ?? {}); setIsFlowInitialized(true); setIsTimeoutDisabled(false); - // Clean up flowId from URL after setting it in state + // Clean up executionId from URL after setting it in state cleanupFlowUrlParams(); } } catch (error) { @@ -468,7 +468,7 @@ const SignIn: FC = ({ !isLoading && !isFlowInitialized && !initializationAttemptedRef.current && - !currentFlowId && + !currentExecutionId && !currentUrlParams.code && !currentUrlParams.state && !isSubmitting && @@ -477,7 +477,7 @@ const SignIn: FC = ({ initializationAttemptedRef.current = true; initializeFlow(); } - }, [isInitialized, isLoading, isFlowInitialized, currentFlowId]); + }, [isInitialized, isLoading, isFlowInitialized, currentExecutionId]); /** * Handle step timeout if configured in additionalData. @@ -513,10 +513,10 @@ const SignIn: FC = ({ * Handle form submission from BaseSignIn or render props. */ const handleSubmit = async (payload: EmbeddedSignInFlowRequestV2): Promise => { - // Use flowId from payload if available, otherwise fall back to currentFlowId - const effectiveFlowId: any = payload.flowId || currentFlowId; + // Use executionId from payload if available, otherwise fall back to currentExecutionId + const effectiveExecutionId: any = payload.executionId || currentExecutionId; - if (!effectiveFlowId) { + if (!effectiveExecutionId) { throw new Error('No active flow ID'); } @@ -592,7 +592,7 @@ const SignIn: FC = ({ setFlowError(null); const response: EmbeddedSignInFlowResponseV2 = (await signIn({ - flowId: effectiveFlowId, + executionId: effectiveExecutionId, ...payload, inputs: processedInputs, })) as EmbeddedSignInFlowResponseV2; @@ -605,7 +605,7 @@ const SignIn: FC = ({ response.data?.additionalData?.['passkeyCreationOptions'] ) { const {passkeyChallenge, passkeyCreationOptions}: any = response.data.additionalData; - const effectiveFlowIdForPasskey: any = response.flowId || effectiveFlowId; + const effectiveExecutionIdForPasskey: any = response.executionId || effectiveExecutionId; // Reset passkey processed ref to allow processing passkeyProcessedRef.current = false; @@ -616,7 +616,7 @@ const SignIn: FC = ({ challenge: passkeyChallenge, creationOptions: passkeyCreationOptions, error: null, - flowId: effectiveFlowIdForPasskey, + executionId: effectiveExecutionIdForPasskey, isActive: true, }); setIsSubmitting(false); @@ -625,7 +625,7 @@ const SignIn: FC = ({ } const { - flowId: normalizedFlowId, + executionId: normalizedExecutionId, components: normalizedComponents, additionalData: normalizedAdditionalData, } = normalizeFlowResponse( @@ -659,9 +659,9 @@ const SignIn: FC = ({ setIsSubmitting(false); // Clear all OAuth-related storage on successful completion - setFlowId(null); + setExecutionId(null); setIsFlowInitialized(false); - sessionStorage.removeItem('asgardeo_flow_id'); + sessionStorage.removeItem('asgardeo_execution_id'); sessionStorage.removeItem('asgardeo_auth_id'); // Clean up OAuth URL params before redirect @@ -681,15 +681,15 @@ const SignIn: FC = ({ return; } - // Update flowId if response contains a new one - if (normalizedFlowId && normalizedComponents) { - setFlowId(normalizedFlowId); + // Update executionId if response contains a new one + if (normalizedExecutionId && normalizedComponents) { + setExecutionId(normalizedExecutionId); setComponents(normalizedComponents); setAdditionalData(normalizedAdditionalData ?? {}); setIsTimeoutDisabled(false); // Ensure flow is marked as initialized when we have components setIsFlowInitialized(true); - // Clean up flowId from URL after setting it in state + // Clean up executionId from URL after setting it in state cleanupFlowUrlParams(); // Display failure reason from INCOMPLETE response @@ -719,16 +719,16 @@ const SignIn: FC = ({ }; useOAuthCallback({ - currentFlowId, + currentExecutionId, isInitialized: isInitialized && !isLoading, isSubmitting, onError: (err: any) => { clearFlowState(); setError(err instanceof Error ? err : new Error(String(err))); }, - onSubmit: async (payload: any) => handleSubmit({flowId: payload.flowId, inputs: payload.inputs}), + onSubmit: async (payload: any) => handleSubmit({executionId: payload.executionId, inputs: payload.inputs}), processedRef: oauthCodeProcessedRef, - setFlowId, + setExecutionId, }); /** @@ -736,7 +736,11 @@ const SignIn: FC = ({ * This effect auto-triggers the browser passkey popup and submits the result. */ useEffect(() => { - if (!passkeyState.isActive || (!passkeyState.challenge && !passkeyState.creationOptions) || !passkeyState.flowId) { + if ( + !passkeyState.isActive || + (!passkeyState.challenge && !passkeyState.creationOptions) || + !passkeyState.executionId + ) { return; } @@ -774,7 +778,7 @@ const SignIn: FC = ({ } await handleSubmit({ - flowId: passkeyState.flowId!, + executionId: passkeyState.executionId!, inputs, }); }; @@ -786,7 +790,7 @@ const SignIn: FC = ({ challenge: null, creationOptions: null, error: null, - flowId: null, + executionId: null, isActive: false, }); }) @@ -795,7 +799,7 @@ const SignIn: FC = ({ setFlowError(error as Error); onError?.(error as Error); }); - }, [passkeyState.isActive, passkeyState.challenge, passkeyState.creationOptions, passkeyState.flowId]); + }, [passkeyState.isActive, passkeyState.challenge, passkeyState.creationOptions, passkeyState.executionId]); if (children) { const renderProps: SignInRenderProps = { diff --git a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx index d18ca062..9fc76e4c 100644 --- a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx @@ -59,7 +59,7 @@ interface PasskeyState { actionId: string | null; creationOptions: string | null; error: Error | null; - flowId: string | null; + executionId: string | null; isActive: boolean; } @@ -285,7 +285,7 @@ const BaseSignUpContent: FC = ({ actionId: null, creationOptions: null, error: null, - flowId: null, + executionId: null, isActive: false, }); @@ -522,7 +522,7 @@ const BaseSignUpContent: FC = ({ if (code && state) { const payload: EmbeddedFlowExecuteRequestPayload = { - ...(currentFlow.flowId && {flowId: currentFlow.flowId}), + ...((currentFlow as any).executionId && {executionId: (currentFlow as any).executionId}), action: '', flowType: (currentFlow as any).flowType || 'REGISTRATION', inputs: { @@ -593,7 +593,7 @@ const BaseSignUpContent: FC = ({ if (code && state) { const payload: EmbeddedFlowExecuteRequestPayload = { - ...(currentFlow.flowId && {flowId: currentFlow.flowId}), + ...((currentFlow as any).executionId && {executionId: (currentFlow as any).executionId}), action: '', flowType: (currentFlow as any).flowType || 'REGISTRATION', inputs: { @@ -672,7 +672,7 @@ const BaseSignUpContent: FC = ({ } const payload: EmbeddedFlowExecuteRequestPayload = { - ...(currentFlow.flowId && {flowId: currentFlow.flowId}), + ...((currentFlow as any).executionId && {executionId: (currentFlow as any).executionId}), flowType: (currentFlow as any).flowType || 'REGISTRATION', ...(component.id && {action: component.id}), inputs: filteredInputs, @@ -694,7 +694,7 @@ const BaseSignUpContent: FC = ({ if (response.data?.additionalData?.['passkeyCreationOptions']) { const {passkeyCreationOptions}: any = response.data.additionalData; - const effectiveFlowIdForPasskey: any = response.flowId || currentFlow?.flowId; + const effectiveExecutionIdForPasskey: any = response.executionId || (currentFlow as any)?.executionId; // Reset passkey processed ref to allow processing passkeyProcessedRef.current = false; @@ -704,7 +704,7 @@ const BaseSignUpContent: FC = ({ actionId: component.id || 'submit', creationOptions: passkeyCreationOptions, error: null, - flowId: effectiveFlowIdForPasskey, + executionId: effectiveExecutionIdForPasskey, isActive: true, }); setIsLoading(false); @@ -726,7 +726,7 @@ const BaseSignUpContent: FC = ({ * This effect auto-triggers the browser passkey popup and submits the result. */ useEffect(() => { - if (!passkeyState.isActive || !passkeyState.creationOptions || !passkeyState.flowId) { + if (!passkeyState.isActive || !passkeyState.creationOptions || !passkeyState.executionId) { return; } @@ -749,7 +749,7 @@ const BaseSignUpContent: FC = ({ // After successful registration, submit the result to the server const payload: EmbeddedFlowExecuteRequestPayload = { actionId: passkeyState.actionId || 'submit', - flowId: passkeyState.flowId as string, + executionId: passkeyState.executionId as string, flowType: (currentFlow as any)?.flowType || 'REGISTRATION', inputs, } as any; @@ -768,14 +768,14 @@ const BaseSignUpContent: FC = ({ performPasskeyRegistration() .then(() => { - setPasskeyState({actionId: null, creationOptions: null, error: null, flowId: null, isActive: false}); + setPasskeyState({actionId: null, creationOptions: null, error: null, executionId: null, isActive: false}); }) .catch((error: any) => { setPasskeyState((prev: any) => ({...prev, error: error as Error, isActive: false})); handleError(error); onError?.(error as Error); }); - }, [passkeyState.isActive, passkeyState.creationOptions, passkeyState.flowId]); + }, [passkeyState.isActive, passkeyState.creationOptions, passkeyState.executionId]); const containerClasses: any = cx( [ diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 5a5d08e7..20e2fda4 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -221,7 +221,7 @@ const AsgardeoProvider: FC> = ({ config.platform === Platform.AsgardeoV2 && typeof arg1 === 'object' && arg1 !== null && - ('flowId' in arg1 || 'applicationId' in arg1); + ('executionId' in arg1 || 'applicationId' in arg1); try { if (!isV2FlowRequest) { @@ -316,11 +316,11 @@ const AsgardeoProvider: FC> = ({ // For V2 platform, check if this is an embedded flow or traditional OAuth const urlParams: URLSearchParams = currentUrl.searchParams; const code: string | null = urlParams.get('code'); - const flowIdFromUrl: string | null = urlParams.get('flowId'); - const storedFlowId: string | null = sessionStorage.getItem('asgardeo_flow_id'); + const executionIdFromUrl: string | null = urlParams.get('executionId'); + const storedExecutionId: string | null = sessionStorage.getItem('asgardeo_execution_id'); - // If there's a code and no flowId, exchange OAuth code for tokens - if (code && !flowIdFromUrl && !storedFlowId) { + // If there's a code and no executionId, exchange OAuth code for tokens + if (code && !executionIdFromUrl && !storedExecutionId) { await signIn(); } } else { diff --git a/packages/react/src/hooks/v2/useOAuthCallback.ts b/packages/react/src/hooks/v2/useOAuthCallback.ts index 61803aa0..e7f72c26 100644 --- a/packages/react/src/hooks/v2/useOAuthCallback.ts +++ b/packages/react/src/hooks/v2/useOAuthCallback.ts @@ -20,14 +20,14 @@ import {useEffect, useRef, type RefObject} from 'react'; export interface UseOAuthCallbackOptions { /** - * Current flowId from component state + * Current executionId from component state */ - currentFlowId: string | null; + currentExecutionId: string | null; /** - * SessionStorage key for flowId (defaults to 'asgardeo_flow_id') + * SessionStorage key for executionId (defaults to 'asgardeo_execution_id') */ - flowIdStorageKey?: string; + executionIdStorageKey?: string; /** * Whether the component is initialized and ready to process OAuth callback @@ -71,9 +71,9 @@ export interface UseOAuthCallbackOptions { processedRef?: RefObject; /** - * Additional handler for setting state (e.g., setFlowId) + * Additional handler for setting state (e.g., setExecutionId) */ - setFlowId?: (flowId: string) => void; + setExecutionId?: (executionId: string) => void; /** * Ref to mark that token validation was attempted (prevents duplicate validation) @@ -83,7 +83,7 @@ export interface UseOAuthCallbackOptions { } export interface OAuthCallbackPayload { - flowId: string; + executionId: string; inputs: { code: string; nonce?: string; @@ -104,12 +104,12 @@ function cleanupUrlParams(): void { } /** - * Processes OAuth callbacks by detecting auth code in URL, resolving flowId, and submitting to server. + * Processes OAuth callbacks by detecting auth code in URL, resolving executionId, and submitting to server. * Used by SignIn, SignUp, and AcceptInvite components. */ export function useOAuthCallback({ - currentFlowId, - flowIdStorageKey = 'asgardeo_flow_id', + currentExecutionId, + executionIdStorageKey = 'asgardeo_execution_id', isInitialized, isSubmitting = false, onComplete, @@ -118,7 +118,7 @@ export function useOAuthCallback({ onProcessingStart, onSubmit, processedRef, - setFlowId, + setExecutionId: setExecExecutionId, tokenValidationAttemptedRef, }: UseOAuthCallbackOptions): void { const internalRef: any = useRef(false); @@ -133,7 +133,7 @@ export function useOAuthCallback({ const code: string | null = urlParams.get('code'); const nonce: string | null = urlParams.get('nonce'); const state: string | null = urlParams.get('state'); - const flowIdFromUrl: string | null = urlParams.get('flowId'); + const executionIdFromUrl: string | null = urlParams.get('executionId'); const error: string | null = urlParams.get('error'); const errorDescription: string | null = urlParams.get('error_description'); @@ -156,12 +156,13 @@ export function useOAuthCallback({ return; } - const storedFlowId: string | null = sessionStorage.getItem(flowIdStorageKey); - const flowIdToUse: string | null = currentFlowId || storedFlowId || flowIdFromUrl || state || null; + const storedExecutionId: string | null = sessionStorage.getItem(executionIdStorageKey); + const executionIdToUse: string | null = + currentExecutionId || storedExecutionId || executionIdFromUrl || state || null; - if (!flowIdToUse) { + if (!executionIdToUse) { oauthCodeProcessedRef.current = true; - onError?.(new Error('Invalid flow. Missing flowId.')); + onError?.(new Error('Invalid flow. Missing executionId.')); cleanupUrlParams(); return; } @@ -175,14 +176,14 @@ export function useOAuthCallback({ onProcessingStart?.(); - if (!currentFlowId && setFlowId) { - setFlowId(flowIdToUse); + if (!currentExecutionId && setExecExecutionId) { + setExecExecutionId(executionIdToUse); } (async (): Promise => { try { const payload: OAuthCallbackPayload = { - flowId: flowIdToUse, + executionId: executionIdToUse, inputs: { code, ...(nonce && {nonce}), @@ -209,13 +210,13 @@ export function useOAuthCallback({ })(); }, [ isInitialized, - currentFlowId, + currentExecutionId, isSubmitting, onSubmit, onComplete, onError, onFlowChange, - setFlowId, - flowIdStorageKey, + setExecExecutionId, + executionIdStorageKey, ]); } diff --git a/packages/react/src/utils/v2/flowTransformer.ts b/packages/react/src/utils/v2/flowTransformer.ts index a22ab4e6..50d61be7 100644 --- a/packages/react/src/utils/v2/flowTransformer.ts +++ b/packages/react/src/utils/v2/flowTransformer.ts @@ -33,7 +33,7 @@ * ```typescript * import { normalizeFlowResponse } from '../../../utils/v2/flowTransformer'; * - * const { flowId, components } = normalizeFlowResponse(apiResponse, t, { + * const { executionId, components } = normalizeFlowResponse(apiResponse, t, { * defaultErrorKey: 'components.signIn.errors.generic' * }); * ``` @@ -50,8 +50,8 @@ import {UseTranslation} from '../../hooks/useTranslation'; * Generic flow error response interface that covers common error structure */ export interface FlowErrorResponse { + executionId: string; failureReason?: string; - flowId: string; flowStatus: 'ERROR'; } @@ -286,7 +286,7 @@ export const checkForErrorResponse = ( * @param response - The raw flow response from the API * @param t - Translation function from useTranslation hook * @param options - Configuration options for transformation behavior - * @returns Normalized flow response with flowId and transformed components + * @returns Normalized flow response with executionId and transformed components * @throws {any} The original response if it's an error and throwOnError is true */ export const normalizeFlowResponse = ( @@ -297,7 +297,7 @@ export const normalizeFlowResponse = ( ): { additionalData: Record; components: EmbeddedFlowComponent[]; - flowId: string; + executionId: string; } => { const {throwOnError = true, defaultErrorKey = 'errors.flow.generic', resolveTranslations = true} = options; @@ -325,6 +325,6 @@ export const normalizeFlowResponse = ( return { additionalData, components: transformComponents(response, t, resolveTranslations, meta), - flowId: response.flowId, + executionId: response.executionId, }; }; diff --git a/packages/vue/src/AsgardeoVueClient.ts b/packages/vue/src/AsgardeoVueClient.ts index 976649e1..82af859d 100644 --- a/packages/vue/src/AsgardeoVueClient.ts +++ b/packages/vue/src/AsgardeoVueClient.ts @@ -351,7 +351,7 @@ class AsgardeoVueClient extends typeof arg1 === 'object' && arg1 !== null && !isEmpty(arg1) && - ('flowId' in arg1 || 'applicationId' in arg1) + ('executionId' in arg1 || 'applicationId' in arg1) ) { const authIdFromUrl: string = new URL(window.location.href).searchParams.get('authId'); const authIdFromStorage: string = sessionStorage.getItem('asgardeo_auth_id'); diff --git a/packages/vue/src/components/presentation/sign-in/v2/SignIn.ts b/packages/vue/src/components/presentation/sign-in/v2/SignIn.ts index 4275d851..782e6b82 100644 --- a/packages/vue/src/components/presentation/sign-in/v2/SignIn.ts +++ b/packages/vue/src/components/presentation/sign-in/v2/SignIn.ts @@ -43,12 +43,12 @@ import BaseSignIn from './BaseSignIn'; import useAsgardeo from '../../../../composables/useAsgardeo'; import useFlowMeta from '../../../../composables/useFlowMeta'; import useI18n from '../../../../composables/useI18n'; -import {useOAuthCallback} from '../../../../composables/useOAuthCallback'; +import {useOAuthCallback} from '../../../../composables/v2/useOAuthCallback'; import {initiateOAuthRedirect} from '../../../../utils/oauth'; import {normalizeFlowResponse} from '../../../../utils/v2/flowTransformer'; import {handlePasskeyAuthentication, handlePasskeyRegistration} from '../../../../utils/v2/passkey'; -const FLOW_ID_STORAGE_KEY: string = 'asgardeo_flow_id'; +const EXECUTION_ID_STORAGE_KEY: string = 'asgardeo_execution_id'; const AUTH_ID_STORAGE_KEY: string = 'asgardeo_auth_id'; interface PasskeyState { @@ -56,7 +56,7 @@ interface PasskeyState { challenge: string | null; creationOptions: string | null; error: Error | null; - flowId: string | null; + executionId: string | null; isActive: boolean; } @@ -120,7 +120,7 @@ const SignIn: Component = defineComponent({ // Flow state const components: Ref = ref([]); const additionalData: Ref> = ref({}); - const currentFlowId: Ref = ref(null); + const currentExecutionId: Ref = ref(null); const isFlowInitialized: Ref = ref(false); const flowError: Ref = ref(null); const isSubmitting: Ref = ref(false); @@ -130,7 +130,7 @@ const SignIn: Component = defineComponent({ challenge: null, creationOptions: null, error: null, - flowId: null, + executionId: null, isActive: false, }); @@ -141,17 +141,17 @@ const SignIn: Component = defineComponent({ // ── Helpers ────────────────────────────────────────────────────────── - const persistFlowId = (flowId: string | null): void => { - currentFlowId.value = flowId; - if (flowId) { - sessionStorage.setItem(FLOW_ID_STORAGE_KEY, flowId); + const persistExecutionId = (executionId: string | null): void => { + currentExecutionId.value = executionId; + if (executionId) { + sessionStorage.setItem(EXECUTION_ID_STORAGE_KEY, executionId); } else { - sessionStorage.removeItem(FLOW_ID_STORAGE_KEY); + sessionStorage.removeItem(EXECUTION_ID_STORAGE_KEY); } }; const clearFlowState = (): void => { - persistFlowId(null); + persistExecutionId(null); isFlowInitialized.value = false; sessionStorage.removeItem(AUTH_ID_STORAGE_KEY); isTimeoutDisabled.value = false; @@ -164,7 +164,7 @@ const SignIn: Component = defineComponent({ code: string | null; error: string | null; errorDescription: string | null; - flowId: string | null; + executionId: string | null; nonce: string | null; state: string | null; } @@ -177,7 +177,7 @@ const SignIn: Component = defineComponent({ code: params.get('code'), error: params.get('error'), errorDescription: params.get('error_description'), - flowId: params.get('flowId'), + executionId: params.get('executionId'), nonce: params.get('nonce'), state: params.get('state'), }; @@ -193,7 +193,7 @@ const SignIn: Component = defineComponent({ const cleanupFlowUrlParams = (): void => { if (!window?.location?.href) return; const url: URL = new URL(window.location.href); - ['flowId', 'authId', 'applicationId'].forEach((p: string) => url.searchParams.delete(p)); + ['executionId', 'authId', 'applicationId'].forEach((p: string) => url.searchParams.delete(p)); window.history.replaceState({}, '', url.toString()); }; @@ -217,9 +217,9 @@ const SignIn: Component = defineComponent({ const effectiveApplicationId: string | null | undefined = (applicationId as string | undefined) || urlParams.applicationId; - if (!urlParams.flowId && !effectiveApplicationId) { + if (!urlParams.executionId && !effectiveApplicationId) { const err: AsgardeoRuntimeError = new AsgardeoRuntimeError( - 'Either flowId or applicationId is required for authentication', + 'Either executionId or applicationId is required for authentication', 'SIGN_IN_ERROR', 'vue', ); @@ -232,8 +232,8 @@ const SignIn: Component = defineComponent({ let response: EmbeddedSignInFlowResponseV2; - if (urlParams.flowId) { - response = (await signIn({flowId: urlParams.flowId})) as EmbeddedSignInFlowResponseV2; + if (urlParams.executionId) { + response = (await signIn({executionId: urlParams.executionId})) as EmbeddedSignInFlowResponseV2; } else { response = (await signIn({ applicationId: effectiveApplicationId, @@ -245,7 +245,7 @@ const SignIn: Component = defineComponent({ if (response.type === EmbeddedSignInFlowTypeV2.Redirection) { const redirectURL: string | undefined = (response.data as any)?.redirectURL || (response as any)?.redirectURL; if (redirectURL && window?.location) { - if (response.flowId) persistFlowId(response.flowId); + if (response.executionId) persistExecutionId(response.executionId); if (urlParams.authId) sessionStorage.setItem(AUTH_ID_STORAGE_KEY, urlParams.authId); initiateOAuthRedirect(redirectURL); return; @@ -253,13 +253,13 @@ const SignIn: Component = defineComponent({ } const { - flowId: normalizedFlowId, + executionId: normalizedExecutionId, components: normalizedComponents, additionalData: normalizedAdditionalData, } = normalizeFlowResponse(response, t, {resolveTranslations: false}, flowMeta.value); - if (normalizedFlowId && normalizedComponents) { - persistFlowId(normalizedFlowId); + if (normalizedExecutionId && normalizedComponents) { + persistExecutionId(normalizedExecutionId); components.value = normalizedComponents; additionalData.value = normalizedAdditionalData ?? {}; isFlowInitialized.value = true; @@ -278,9 +278,9 @@ const SignIn: Component = defineComponent({ // ── Submit handler ──────────────────────────────────────────────────── const handleSubmit = async (payload: EmbeddedSignInFlowRequestV2): Promise => { - const effectiveFlowId: string | null = payload.flowId || currentFlowId.value; + const effectiveExecutionId: string | null = payload.executionId || currentExecutionId.value; - if (!effectiveFlowId) { + if (!effectiveExecutionId) { throw new Error('No active flow ID'); } @@ -335,7 +335,7 @@ const SignIn: Component = defineComponent({ flowError.value = null; const response: EmbeddedSignInFlowResponseV2 = (await signIn({ - flowId: effectiveFlowId, + executionId: effectiveExecutionId, ...payload, inputs: processedInputs, })) as EmbeddedSignInFlowResponseV2; @@ -344,7 +344,7 @@ const SignIn: Component = defineComponent({ if (response.type === EmbeddedSignInFlowTypeV2.Redirection) { const redirectURL: string | undefined = (response.data as any)?.redirectURL || (response as any)?.redirectURL; if (redirectURL && window?.location) { - if (response.flowId) persistFlowId(response.flowId); + if (response.executionId) persistExecutionId(response.executionId); const urlParams: UrlParams = getUrlParams(); if (urlParams.authId) sessionStorage.setItem(AUTH_ID_STORAGE_KEY, urlParams.authId); initiateOAuthRedirect(redirectURL); @@ -364,7 +364,7 @@ const SignIn: Component = defineComponent({ challenge: passkeyChallenge || null, creationOptions: passkeyCreationOptions || null, error: null, - flowId: response.flowId || effectiveFlowId, + executionId: response.executionId || effectiveExecutionId, isActive: true, }; isSubmitting.value = false; @@ -372,7 +372,7 @@ const SignIn: Component = defineComponent({ } const { - flowId: normalizedFlowId, + executionId: normalizedExecutionId, components: normalizedComponents, additionalData: normalizedAdditionalData, } = normalizeFlowResponse(response, t, {resolveTranslations: false}, flowMeta.value); @@ -394,7 +394,7 @@ const SignIn: Component = defineComponent({ const finalRedirectUrl: string | undefined = redirectUrl || afterSignInUrl; isSubmitting.value = false; - persistFlowId(null); + persistExecutionId(null); isFlowInitialized.value = false; sessionStorage.removeItem(AUTH_ID_STORAGE_KEY); cleanupOAuthUrlParams(); @@ -411,8 +411,8 @@ const SignIn: Component = defineComponent({ } // Update flow state for next step - if (normalizedFlowId && normalizedComponents) { - persistFlowId(normalizedFlowId); + if (normalizedExecutionId && normalizedComponents) { + persistExecutionId(normalizedExecutionId); components.value = normalizedComponents; additionalData.value = normalizedAdditionalData ?? {}; isTimeoutDisabled.value = false; @@ -475,7 +475,7 @@ const SignIn: Component = defineComponent({ watch( () => passkeyState.value, async (state: PasskeyState) => { - if (!state.isActive || (!state.challenge && !state.creationOptions) || !state.flowId) return; + if (!state.isActive || (!state.challenge && !state.creationOptions) || !state.executionId) return; if (passkeyProcessed) return; passkeyProcessed = true; @@ -504,14 +504,14 @@ const SignIn: Component = defineComponent({ throw new Error('No passkey challenge or creation options available'); } - await handleSubmit({flowId: state.flowId!, inputs}); + await handleSubmit({executionId: state.executionId!, inputs}); passkeyState.value = { actionId: null, challenge: null, creationOptions: null, error: null, - flowId: null, + executionId: null, isActive: false, }; } catch (error: unknown) { @@ -527,8 +527,8 @@ const SignIn: Component = defineComponent({ // ── OAuth callback (via composable) ───────────────────────────────── useOAuthCallback({ - currentFlowId, - flowIdStorageKey: FLOW_ID_STORAGE_KEY, + currentExecutionId, + executionIdStorageKey: EXECUTION_ID_STORAGE_KEY, isInitialized, isSubmitting, onError: (err: any) => { @@ -539,9 +539,9 @@ const SignIn: Component = defineComponent({ } }, onSubmit: (payload: EmbeddedSignInFlowRequestV2) => - handleSubmit({flowId: payload.flowId, inputs: payload.inputs}), + handleSubmit({executionId: payload.executionId, inputs: payload.inputs}), processedFlag: oauthCodeProcessedFlag, - setFlowId: persistFlowId, + setExecutionId: persistExecutionId, }); // ── Lifecycle ───────────────────────────────────────────────────────── @@ -557,14 +557,20 @@ const SignIn: Component = defineComponent({ // Initialize flow when SDK is ready (OAuth callback is handled by useOAuthCallback) watch( () => - [isInitialized.value, sdkLoading.value, isFlowInitialized.value, currentFlowId.value, isSubmitting.value] as [ - boolean, - boolean, - boolean, - string | null, - boolean, - ], - ([initialized, loading, flowInit, flowId, submitting]: [boolean, boolean, boolean, string | null, boolean]) => { + [ + isInitialized.value, + sdkLoading.value, + isFlowInitialized.value, + currentExecutionId.value, + isSubmitting.value, + ] as [boolean, boolean, boolean, string | null, boolean], + ([initialized, loading, flowInit, executionId, submitting]: [ + boolean, + boolean, + boolean, + string | null, + boolean, + ]) => { const urlParams: UrlParams = getUrlParams(); const hasOAuthCode: boolean = !!urlParams.code; const hasOAuthState: boolean = !!urlParams.state; @@ -575,7 +581,7 @@ const SignIn: Component = defineComponent({ !loading && !flowInit && !initializationAttempted && - !flowId && + !executionId && !hasOAuthCode && !hasOAuthState && !submitting && diff --git a/packages/vue/src/composables/v2/useOAuthCallback.ts b/packages/vue/src/composables/v2/useOAuthCallback.ts new file mode 100644 index 00000000..dcb02cdc --- /dev/null +++ b/packages/vue/src/composables/v2/useOAuthCallback.ts @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {watch, type Ref} from 'vue'; + +export interface UseOAuthCallbackOptions { + /** Current executionId from component state */ + currentExecutionId: Ref; + + /** SessionStorage key for executionId (defaults to 'asgardeo_execution_id') */ + executionIdStorageKey?: string; + + /** Whether the component is initialized and ready to process OAuth callback */ + isInitialized: Ref; + + /** Whether a submission is currently in progress */ + isSubmitting?: Ref; + + /** Callback when OAuth flow completes successfully */ + onComplete?: () => void; + + /** Callback when OAuth flow encounters an error */ + onError?: (error: any) => void; + + /** Callback to handle flow response after submission */ + onFlowChange?: (response: any) => void; + + /** Callback to set loading state at the start of OAuth processing */ + onProcessingStart?: () => void; + + /** Function to submit OAuth code to the server */ + onSubmit: (payload: OAuthCallbackPayload) => Promise; + + /** Mutable flag to track whether OAuth has already been processed */ + processedFlag?: {value: boolean}; + + /** Additional handler for setting state (e.g., setExecutionId) */ + setExecutionId?: (executionId: string) => void; + + /** + * Mutable flag for token validation tracking. + * Used in AcceptInvite to coordinate between OAuth callback and token validation. + */ + tokenValidationAttemptedFlag?: {value: boolean}; +} + +export interface OAuthCallbackPayload { + /** The execution ID of the active flow step */ + executionId: string; + + /** OAuth callback inputs extracted from the redirect URL */ + inputs: { + /** The authorization code returned by the OAuth provider */ + code: string; + + /** Optional nonce for OIDC replay protection */ + nonce?: string; + }; +} + +/** + * Removes OAuth-related query parameters from the current URL without triggering a navigation. + * This prevents re-processing the callback on subsequent renders or page interactions. + */ +function cleanupUrlParams(): void { + if (typeof window === 'undefined') return; + + const url: URL = new URL(window.location.href); + url.searchParams.delete('code'); + url.searchParams.delete('nonce'); + url.searchParams.delete('state'); + url.searchParams.delete('error'); + url.searchParams.delete('error_description'); + + window.history.replaceState({}, '', url.toString()); +} + +/** + * Processes OAuth callbacks by detecting auth code in URL, resolving executionId, and submitting to server. + * Used by SignIn, SignUp, and AcceptInvite components. + * + * Vue composable equivalent of React's useOAuthCallback hook. + */ +export function useOAuthCallback({ + currentExecutionId, + executionIdStorageKey = 'asgardeo_execution_id', + isInitialized, + isSubmitting, + onComplete, + onError, + onFlowChange, + onProcessingStart, + onSubmit, + processedFlag, + setExecutionId, + tokenValidationAttemptedFlag, +}: UseOAuthCallbackOptions): void { + /** Fallback mutable flag used when no external processedFlag is provided */ + const internalFlag: {value: boolean} = {value: false}; + + /** Ensures OAuth code is submitted only once, even across reactive re-evaluations */ + const oauthCodeProcessedFlag: {value: boolean} = processedFlag ?? internalFlag; + + /** Tracks whether token validation has been attempted; used to coordinate with AcceptInvite */ + const tokenValidationFlag: {value: boolean} | undefined = tokenValidationAttemptedFlag; + + // Re-run whenever initialization state, executionId, or submission state changes. + // `immediate: true` ensures the callback runs on mount to catch OAuth redirects on first load. + watch( + () => [isInitialized.value, currentExecutionId.value, isSubmitting?.value] as const, + ([initialized, , submitting]: readonly [boolean, string | null, boolean | undefined]) => { + // Wait until the component is ready and any in-flight submission has settled. + if (!initialized || submitting) { + return; + } + + // Extract all OAuth-related parameters from the redirect URL. + const urlParams: URLSearchParams = new URLSearchParams(window.location.search); + const code: string | null = urlParams.get('code'); + const nonce: string | null = urlParams.get('nonce'); + const state: string | null = urlParams.get('state'); + const executionIdFromUrl: string | null = urlParams.get('executionId'); + const error: string | null = urlParams.get('error'); + const errorDescription: string | null = urlParams.get('error_description'); + + // Handle OAuth provider errors (e.g., user denied consent) before processing the code. + if (error) { + oauthCodeProcessedFlag.value = true; + if (tokenValidationFlag) { + tokenValidationFlag.value = true; + } + onError?.(new Error(errorDescription || error || 'OAuth authentication failed')); + cleanupUrlParams(); + return; + } + + // Skip if there is no authorization code or if it has already been submitted. + if (!code || oauthCodeProcessedFlag.value) { + return; + } + + // In AcceptInvite flows, token validation runs concurrently. If it has already + // started, the OAuth callback should not interfere. + if (tokenValidationFlag?.value) { + return; + } + + // Resolve executionId using the most specific available source: + // component state > sessionStorage > URL param > OAuth state param. + const storedExecutionId: string | null = sessionStorage.getItem(executionIdStorageKey); + const executionIdToUse: string | null = + currentExecutionId.value || storedExecutionId || executionIdFromUrl || state || null; + + // Cannot proceed without an executionId — the flow context is missing. + if (!executionIdToUse) { + oauthCodeProcessedFlag.value = true; + onError?.(new Error('Invalid flow. Missing executionId.')); + cleanupUrlParams(); + return; + } + + // Mark as processed synchronously before the async submission to prevent + // duplicate submissions if the watcher fires again during the await. + oauthCodeProcessedFlag.value = true; + + if (tokenValidationFlag) { + tokenValidationFlag.value = true; + } + + // Signal the component to enter a loading state before the async work begins. + onProcessingStart?.(); + + // Sync the resolved executionId back into component state if it was sourced + // from sessionStorage or the URL rather than reactive state. + if (!currentExecutionId.value && setExecutionId) { + setExecutionId(executionIdToUse); + } + + // Submit the OAuth code in an IIFE to allow async/await inside a synchronous watcher callback. + (async (): Promise => { + try { + const payload: OAuthCallbackPayload = { + executionId: executionIdToUse, + inputs: { + code, + ...(nonce && {nonce}), + }, + }; + + const response: any = await onSubmit(payload); + + // Notify the component so it can update its flow state (e.g., move to the next step). + onFlowChange?.(response); + + if (response?.flowStatus === 'COMPLETE' || response?.status === 'COMPLETE') { + onComplete?.(); + } + + if (response?.flowStatus === 'ERROR' || response?.status === 'ERROR') { + onError?.(response); + } + + cleanupUrlParams(); + } catch (err) { + onError?.(err); + cleanupUrlParams(); + } + })(); + }, + {immediate: true}, + ); +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index ea7925d5..9f3f025a 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -42,6 +42,11 @@ export {default as useTheme} from './composables/useTheme'; export {default as useUser} from './composables/useUser'; export {useOAuthCallback} from './composables/useOAuthCallback'; export type {UseOAuthCallbackOptions, OAuthCallbackPayload} from './composables/useOAuthCallback'; +export {useOAuthCallback as useOAuthCallbackV2} from './composables/v2/useOAuthCallback'; +export type { + UseOAuthCallbackOptions as UseOAuthCallbackOptionsV2, + OAuthCallbackPayload as OAuthCallbackPayloadV2, +} from './composables/v2/useOAuthCallback'; // ── Client ── export {default as AsgardeoVueClient} from './AsgardeoVueClient'; diff --git a/packages/vue/src/providers/AsgardeoProvider.ts b/packages/vue/src/providers/AsgardeoProvider.ts index 665597f6..eb04a53a 100644 --- a/packages/vue/src/providers/AsgardeoProvider.ts +++ b/packages/vue/src/providers/AsgardeoProvider.ts @@ -293,7 +293,7 @@ const AsgardeoProvider: Component = defineComponent({ config.platform === Platform.AsgardeoV2 && typeof arg1 === 'object' && arg1 !== null && - ('flowId' in arg1 || 'applicationId' in arg1); + ('executionId' in arg1 || 'applicationId' in arg1); try { if (!isV2FlowRequest) { @@ -470,10 +470,10 @@ const AsgardeoProvider: Component = defineComponent({ if (isV2Platform) { const urlParams: URLSearchParams = currentUrl.searchParams; const code: string | null = urlParams.get('code'); - const flowIdFromUrl: string | null = urlParams.get('flowId'); - const storedFlowId: string | null = sessionStorage.getItem('asgardeo_flow_id'); + const executionIdFromUrl: string | null = urlParams.get('executionId'); + const storedExecutionId: string | null = sessionStorage.getItem('asgardeo_execution_id'); - if (code && !flowIdFromUrl && !storedFlowId) { + if (code && !executionIdFromUrl && !storedExecutionId) { await signIn(); } } else { diff --git a/packages/vue/src/utils/v2/flowTransformer.ts b/packages/vue/src/utils/v2/flowTransformer.ts index 71a50a97..e20bd852 100644 --- a/packages/vue/src/utils/v2/flowTransformer.ts +++ b/packages/vue/src/utils/v2/flowTransformer.ts @@ -33,7 +33,7 @@ * ```typescript * import { normalizeFlowResponse } from '../../../utils/v2/flowTransformer'; * - * const { flowId, components } = normalizeFlowResponse(apiResponse, t, { + * const { executionId, components } = normalizeFlowResponse(apiResponse, t, { * defaultErrorKey: 'components.signIn.errors.generic' * }); * ``` @@ -51,8 +51,8 @@ type TranslationFn = (key: string, params?: Record) => * Generic flow error response interface that covers common error structure */ export interface FlowErrorResponse { + executionId: string; failureReason?: string; - flowId: string; flowStatus: 'ERROR'; } @@ -239,7 +239,7 @@ export const normalizeFlowResponse = ( ): { additionalData: Record; components: EmbeddedFlowComponent[]; - flowId: string; + executionId: string; } => { const {throwOnError = true, defaultErrorKey = 'errors.flow.generic', resolveTranslations = true} = options; @@ -263,6 +263,6 @@ export const normalizeFlowResponse = ( return { additionalData, components: transformComponents(response, t, resolveTranslations, meta), - flowId: response.flowId, + executionId: response.executionId, }; };