diff --git a/packages/browser/src/__legacy__/client.ts b/packages/browser/src/__legacy__/client.ts index 2e4c4061..05f04a27 100755 --- a/packages/browser/src/__legacy__/client.ts +++ b/packages/browser/src/__legacy__/client.ts @@ -61,6 +61,8 @@ const DefaultConfig: Partial> = { /** * This class provides the necessary methods to implement authentication in a Single Page Application. + * Implements a Multiton pattern to support multi-tenancy scenarios where multiple authentication + * contexts need to coexist in the same application. * * @export * @class AsgardeoSPAClient @@ -170,15 +172,22 @@ export class AsgardeoSPAClient { } /** - * This method returns the instance of the singleton class. + * This method returns the instance of the client for the specified ID. + * Implements a Multiton pattern to support multiple authentication contexts. * If an ID is provided, it will return the instance with the given ID. - * If no ID is provided, it will return the default instance value 0. + * If no ID is provided, it will return the default instance (ID: 0). * - * @return {AsgardeoSPAClient} - Returns the instance of the singleton class. + * @param {number} id - Optional unique identifier for the instance. + * @return {AsgardeoSPAClient} - Returns the instance associated with the ID. * * @example * ``` + * // Single tenant application (default instance) * const auth = AsgardeoSPAClient.getInstance(); + * + * // Multi-instance application + * const instance1 = AsgardeoSPAClient.getInstance(1); + * const instance2 = AsgardeoSPAClient.getInstance(2); * ``` * * @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getinstance @@ -187,22 +196,113 @@ export class AsgardeoSPAClient { * * @preserve */ - public static getInstance(id?: number): AsgardeoSPAClient | undefined { - if (id && this._instances?.get(id)) { - return this._instances.get(id); - } else if (!id && this._instances?.get(0)) { - return this._instances.get(0); + public static getInstance(id: number = 0): AsgardeoSPAClient { + if (!this._instances.has(id)) { + this._instances.set(id, new AsgardeoSPAClient(id)); } - if (id) { - this._instances.set(id, new AsgardeoSPAClient(id)); + return this._instances.get(id)!; + } - return this._instances.get(id); - } + /** + * This method checks if an instance exists for the given ID. + * + * @param {number} id - Optional unique identifier for the instance. + * @return {boolean} - Returns true if an instance exists for the ID. + * + * @example + * ``` + * if (AsgardeoSPAClient.hasInstance(1)) { + * const auth = AsgardeoSPAClient.getInstance(1); + * } + * ``` + * + * @memberof AsgardeoSPAClient + * + * @preserve + */ + public static hasInstance(id: number = 0): boolean { + return this._instances.has(id); + } - this._instances.set(0, new AsgardeoSPAClient(0)); + /** + * This method removes and cleans up a specific instance. + * Useful when an instance is no longer needed. + * + * @param {number} id - Optional unique identifier for the instance to destroy. + * @return {boolean} - Returns true if the instance was found and removed. + * + * @example + * ``` + * // Remove a specific instance + * AsgardeoSPAClient.destroyInstance(1); + * + * // Remove the default instance + * AsgardeoSPAClient.destroyInstance(); + * ``` + * + * @memberof AsgardeoSPAClient + * + * @preserve + */ + public static destroyInstance(id: number = 0): boolean { + return this._instances.delete(id); + } - return this._instances.get(0); + /** + * This method returns all active instance IDs. + * Useful for debugging or managing multiple instances. + * + * @return {number[]} - Returns an array of all active instance IDs. + * + * @example + * ``` + * const activeInstances = AsgardeoSPAClient.getInstanceKeys(); + * console.log('Active instances:', activeInstances); + * ``` + * + * @memberof AsgardeoSPAClient + * + * @preserve + */ + public static getInstanceKeys(): number[] { + return Array.from(this._instances.keys()); + } + + /** + * This method removes all instances. + * Useful for cleanup in testing scenarios or application teardown. + * + * @example + * ``` + * AsgardeoSPAClient.destroyAllInstances(); + * ``` + * + * @memberof AsgardeoSPAClient + * + * @preserve + */ + public static destroyAllInstances(): void { + this._instances.clear(); + } + + /** + * This method returns the instance ID for this client instance. + * + * @return {number} - The instance ID. + * + * @example + * ``` + * const auth = AsgardeoSPAClient.getInstance(1); + * console.log(auth.getInstanceId()); // 1 + * ``` + * + * @memberof AsgardeoSPAClient + * + * @preserve + */ + public getInstanceId(): number { + return this._instanceID; } /** diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 5b687a17..f1634fd8 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -46,6 +46,7 @@ export * from './__legacy__/worker/worker-receiver'; export {AsgardeoBrowserConfig} from './models/config'; export {default as hasAuthParamsInUrl} from './utils/hasAuthParamsInUrl'; +export {default as hasCalledForThisInstanceInUrl} from './utils/hasCalledForThisInstanceInUrl'; export {default as navigate} from './utils/navigate'; export {default as AsgardeoBrowserClient} from './AsgardeoBrowserClient'; diff --git a/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts b/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts new file mode 100644 index 00000000..3960e328 --- /dev/null +++ b/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025, 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. + */ + +/** + * Utility to check if `state` is available in the URL as a search param and matches the provided instance. + * + * @param params - The URL search params to check. Defaults to `window.location.search`. + * @param instanceId - The instance ID to match against the `state` param. + * @return `true` if the URL contains a matching `state` search param, otherwise `false`. + */ +const hasCalledForThisInstanceInUrl = (instanceId: number, params: string = window.location.search): boolean => { + const MATCHER: RegExp = new RegExp(`[?&]state=instance_${instanceId}-[^&]+`); + + return MATCHER.test(params); +}; + +export default hasCalledForThisInstanceInUrl; \ No newline at end of file diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index e54ddaec..03c1173e 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -68,7 +68,7 @@ export class AsgardeoAuthClient { private _cryptoUtils: Crypto; private _cryptoHelper: IsomorphicCrypto; - private static _instanceID: number; + private _instanceID: number; // FIXME: Validate this. // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 @@ -118,20 +118,20 @@ export class AsgardeoAuthClient { ): Promise { const clientId: string = config.clientId; - if (!AsgardeoAuthClient._instanceID) { - AsgardeoAuthClient._instanceID = 0; + if (!this._instanceID) { + this._instanceID = 0; } else { - AsgardeoAuthClient._instanceID += 1; + this._instanceID += 1; } - if (instanceID) { - AsgardeoAuthClient._instanceID = instanceID; + if (instanceID !== undefined) { + this._instanceID = instanceID; } if (!clientId) { - this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}`, store); + this._storageManager = new StorageManager(`instance_${this._instanceID}`, store); } else { - this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}-${clientId}`, store); + this._storageManager = new StorageManager(`instance_${this._instanceID}-${clientId}`, store); } this._cryptoUtils = cryptoUtils; @@ -182,7 +182,7 @@ export class AsgardeoAuthClient { * @preserve */ public getInstanceId(): number { - return AsgardeoAuthClient._instanceID; + return this._instanceID; } /** @@ -245,6 +245,8 @@ export class AsgardeoAuthClient { authRequestConfig['client_secret'] = configData.clientSecret; } + authRequestConfig['state'] = 'instance_' + this.getInstanceId() + '-' + configData.clientId; + const authorizeRequestParams: Map = getAuthorizeRequestUrlParams( { redirectUri: configData.afterSignInUrl, diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 85d494f4..7509dfac 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -68,12 +68,26 @@ import getAllOrganizations from './api/getAllOrganizations'; class AsgardeoReactClient extends AsgardeoBrowserClient { private asgardeo: AuthAPI; private _isLoading: boolean = false; + private _instanceId: number; - constructor() { + /** + * Creates a new AsgardeoReactClient instance. + * @param instanceId - Optional instance ID for multi-auth context support. Defaults to 0 for backward compatibility. + */ + constructor(instanceId: number = 0) { super(); + this._instanceId = instanceId; // FIXME: This has to be the browser client from `@asgardeo/browser` package. - this.asgardeo = new AuthAPI(); + this.asgardeo = new AuthAPI(undefined, instanceId); + } + + /** + * Get the instance ID for this client. + * @returns The instance ID used for multi-auth context support. + */ + public getInstanceId(): number { + return this._instanceId; } /** @@ -168,8 +182,8 @@ class AsgardeoReactClient e baseUrl = configData?.baseUrl; } - const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({baseUrl}); + const profile = await getScim2Me({baseUrl, instanceId: this._instanceId}); + const schemas = await getSchemas({baseUrl, instanceId: this._instanceId}); const processedSchemas = flattenUserSchema(schemas); @@ -199,7 +213,7 @@ class AsgardeoReactClient e baseUrl = configData?.baseUrl; } - return getMeOrganizations({baseUrl}); + return getMeOrganizations({baseUrl, instanceId: this._instanceId}); } catch (error) { throw new AsgardeoRuntimeError( `Failed to fetch the user's associated organizations: ${ @@ -221,7 +235,7 @@ class AsgardeoReactClient e baseUrl = configData?.baseUrl; } - return getAllOrganizations({baseUrl}); + return getAllOrganizations({baseUrl, instanceId: this._instanceId}); } catch (error) { throw new AsgardeoRuntimeError( `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts index 35a21908..ca6064ee 100644 --- a/packages/react/src/__temp__/api.ts +++ b/packages/react/src/__temp__/api.ts @@ -37,11 +37,13 @@ class AuthAPI { private _authState = AuthAPI.DEFAULT_STATE; private _client: AsgardeoSPAClient; + private _instanceId: number; private _isLoading: boolean; - constructor(spaClient?: AsgardeoSPAClient) { - this._client = spaClient ?? AsgardeoSPAClient.getInstance(); + constructor(spaClient?: AsgardeoSPAClient, instanceId: number = 0) { + this._instanceId = instanceId; + this._client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId); this.getState = this.getState.bind(this); this.init = this.init.bind(this); @@ -50,6 +52,14 @@ class AuthAPI { this.updateState = this.updateState.bind(this); } + /** + * Get the instance ID for this AuthAPI instance. + * @returns The instance ID used for multi-auth context support. + */ + public getInstanceId(): number { + return this._instanceId; + } + public _setIsLoading(isLoading: boolean): void { this._isLoading = isLoading; } diff --git a/packages/react/src/api/createOrganization.ts b/packages/react/src/api/createOrganization.ts index 8e0dfa74..3487f64b 100644 --- a/packages/react/src/api/createOrganization.ts +++ b/packages/react/src/api/createOrganization.ts @@ -26,8 +26,6 @@ import { CreateOrganizationPayload, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the createOrganization request (React-specific) */ @@ -37,6 +35,10 @@ export interface CreateOrganizationConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -90,8 +92,11 @@ export interface CreateOrganizationConfig extends Omit => { +const createOrganization = async ({fetcher, instanceId = 0, ...requestConfig}: CreateOrganizationConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'POST', diff --git a/packages/react/src/api/getAllOrganizations.ts b/packages/react/src/api/getAllOrganizations.ts index 48f79aa7..203fdb51 100644 --- a/packages/react/src/api/getAllOrganizations.ts +++ b/packages/react/src/api/getAllOrganizations.ts @@ -25,8 +25,6 @@ import { AllOrganizationsApiResponse, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the getAllOrganizations request (React-specific) */ @@ -36,6 +34,10 @@ export interface GetAllOrganizationsConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -83,9 +85,13 @@ export interface GetAllOrganizationsConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'GET', diff --git a/packages/react/src/api/getMeOrganizations.ts b/packages/react/src/api/getMeOrganizations.ts index 9ed9a56e..c962b2bd 100644 --- a/packages/react/src/api/getMeOrganizations.ts +++ b/packages/react/src/api/getMeOrganizations.ts @@ -25,8 +25,6 @@ import { GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the getMeOrganizations request (React-specific) */ @@ -36,6 +34,10 @@ export interface GetMeOrganizationsConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -85,8 +87,11 @@ export interface GetMeOrganizationsConfig extends Omit => { +const getMeOrganizations = async ({fetcher, instanceId = 0, ...requestConfig}: GetMeOrganizationsConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'GET', diff --git a/packages/react/src/api/getOrganization.ts b/packages/react/src/api/getOrganization.ts index 47741545..f8817dca 100644 --- a/packages/react/src/api/getOrganization.ts +++ b/packages/react/src/api/getOrganization.ts @@ -25,8 +25,6 @@ import { OrganizationDetails, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the getOrganization request (React-specific) */ @@ -36,6 +34,10 @@ export interface GetOrganizationConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -77,8 +79,11 @@ export interface GetOrganizationConfig extends Omit => { +const getOrganization = async ({fetcher, instanceId = 0, ...requestConfig}: GetOrganizationConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'GET', diff --git a/packages/react/src/api/getSchemas.ts b/packages/react/src/api/getSchemas.ts index ff1e25a3..8ffbb0e7 100644 --- a/packages/react/src/api/getSchemas.ts +++ b/packages/react/src/api/getSchemas.ts @@ -25,8 +25,6 @@ import { GetSchemasConfig as BaseGetSchemasConfig, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the getSchemas request (React-specific) */ @@ -36,6 +34,10 @@ export interface GetSchemasConfig extends Omit * which is a wrapper around axios http.request */ fetcher?: (url: string, config: RequestInit) => Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -75,8 +77,11 @@ export interface GetSchemasConfig extends Omit * } * ``` */ -const getSchemas = async ({fetcher, ...requestConfig}: GetSchemasConfig): Promise => { +const getSchemas = async ({fetcher, instanceId = 0, ...requestConfig}: GetSchemasConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'GET', diff --git a/packages/react/src/api/getScim2Me.ts b/packages/react/src/api/getScim2Me.ts index d5c5bb4c..5d75995a 100644 --- a/packages/react/src/api/getScim2Me.ts +++ b/packages/react/src/api/getScim2Me.ts @@ -26,8 +26,6 @@ import { GetScim2MeConfig as BaseGetScim2MeConfig } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the getScim2Me request (React-specific) */ @@ -37,6 +35,10 @@ export interface GetScim2MeConfig extends Omit * which is a wrapper around axios http.request */ fetcher?: (url: string, config: RequestInit) => Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -76,8 +78,11 @@ export interface GetScim2MeConfig extends Omit * } * ``` */ -const getScim2Me = async ({fetcher, ...requestConfig}: GetScim2MeConfig): Promise => { +const getScim2Me = async ({fetcher, instanceId = 0, ...requestConfig}: GetScim2MeConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'GET', diff --git a/packages/react/src/api/updateMeProfile.ts b/packages/react/src/api/updateMeProfile.ts index 61285a77..97e4a410 100644 --- a/packages/react/src/api/updateMeProfile.ts +++ b/packages/react/src/api/updateMeProfile.ts @@ -25,8 +25,6 @@ import { UpdateMeProfileConfig as BaseUpdateMeProfileConfig, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the updateMeProfile request (React-specific) */ @@ -36,6 +34,10 @@ export interface UpdateMeProfileConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -63,8 +65,11 @@ export interface UpdateMeProfileConfig extends Omit => { +const updateMeProfile = async ({fetcher, instanceId = 0, ...requestConfig}: UpdateMeProfileConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'PATCH', diff --git a/packages/react/src/api/updateOrganization.ts b/packages/react/src/api/updateOrganization.ts index 4a94efb1..6be8fb2c 100644 --- a/packages/react/src/api/updateOrganization.ts +++ b/packages/react/src/api/updateOrganization.ts @@ -26,8 +26,6 @@ import { createPatchOperations, } from '@asgardeo/browser'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - /** * Configuration for the updateOrganization request (React-specific) */ @@ -37,6 +35,10 @@ export interface UpdateOrganizationConfig extends Omit Promise; + /** + * Optional instance ID for multi-instance support. Defaults to 0. + */ + instanceId?: number; } /** @@ -86,9 +88,13 @@ export interface UpdateOrganizationConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind( + AsgardeoSPAClient.getInstance(instanceId) + ); const response = await httpClient({ url, method: config.method || 'PATCH', diff --git a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx index 7b88c55a..0794e858 100644 --- a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -76,7 +76,7 @@ export const CreateOrganization: FC = ({ defaultParentId, ...props }: CreateOrganizationProps): ReactElement => { - const {isSignedIn, baseUrl} = useAsgardeo(); + const {isSignedIn, baseUrl, instanceId} = useAsgardeo(); const {currentOrganization, revalidateMyOrganizations} = useOrganization(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -112,6 +112,7 @@ export const CreateOrganization: FC = ({ ...payload, parentId, }, + instanceId, }); } diff --git a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx index ffdba6e0..ec3e671d 100644 --- a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx @@ -141,7 +141,7 @@ const OrganizationProfile: FC = ({ errorFallback =
Failed to load organization data
, ...rest }: OrganizationProfileProps): ReactElement => { - const {baseUrl} = useAsgardeo(); + const {baseUrl, instanceId} = useAsgardeo(); const {t} = useTranslation(); const [organization, setOrganization] = useState(null); const [loading, setLoading] = useState(true); @@ -160,6 +160,7 @@ const OrganizationProfile: FC = ({ const orgData = await getOrganization({ baseUrl, organizationId, + instanceId, }); setOrganization(orgData); } catch (err) { @@ -186,6 +187,7 @@ const OrganizationProfile: FC = ({ baseUrl, organizationId, operations, + instanceId, }); // Refetch organization data after update await fetchOrganization(); diff --git a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx index 2f7fdb95..2149f774 100644 --- a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx @@ -64,7 +64,7 @@ export type UserProfileProps = Omit = ({...rest}: UserProfileProps): ReactElement => { - const {baseUrl, isLoading} = useAsgardeo(); + const {baseUrl, isLoading, instanceId} = useAsgardeo(); const {profile, flattenedProfile, schemas, onUpdateProfile} = useUser(); const {t} = useTranslation(); @@ -74,7 +74,7 @@ const UserProfile: FC = ({...rest}: UserProfileProps): ReactEl setError(null); try { - const response: User = await updateMeProfile({baseUrl, payload}); + const response: User = await updateMeProfile({baseUrl, payload, instanceId}); onUpdateProfile(response); } catch (error: unknown) { let message: string = t('user.profile.update.generic.error'); diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index 03a40f7a..f10f1e81 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -138,6 +138,10 @@ export type AsgardeoContextProps = { * @returns Promise resolving to boolean indicating success. */ reInitialize: (config: Partial) => Promise; + /** + * Instance ID for multi-instance support. + */ + instanceId: number; } & Pick & Pick; @@ -173,6 +177,7 @@ const AsgardeoContext: Context = createContext> = ({ applicationId, signInOptions, syncSession, + instanceId = 0, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); - const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(), []); - const {hasAuthParams} = useBrowserUrl(); + const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(instanceId), [instanceId]); + const {hasAuthParams, hasCalledForThisInstance} = useBrowserUrl(); const [user, setUser] = useState(null); const [currentOrganization, setCurrentOrganization] = useState(null); @@ -153,7 +154,7 @@ const AsgardeoProvider: FC> = ({ } const currentUrl: URL = new URL(window.location.href); - const hasAuthParamsResult: boolean = hasAuthParams(currentUrl, afterSignInUrl); + const hasAuthParamsResult: boolean = hasAuthParams(currentUrl, afterSignInUrl) && hasCalledForThisInstance(currentUrl, instanceId ?? 1); const isV2Platform = config.platform === Platform.AsgardeoV2; @@ -580,6 +581,7 @@ const AsgardeoProvider: FC> = ({ syncSession, platform: config?.platform, switchOrganization, + instanceId, }), [ applicationId, @@ -608,6 +610,7 @@ const AsgardeoProvider: FC> = ({ signUp, clearSession, reInitialize, + instanceId, ], ); diff --git a/packages/react/src/hooks/useBrowserUrl.ts b/packages/react/src/hooks/useBrowserUrl.ts index fd4bbd30..8a3a8a8c 100644 --- a/packages/react/src/hooks/useBrowserUrl.ts +++ b/packages/react/src/hooks/useBrowserUrl.ts @@ -16,7 +16,7 @@ * under the License. */ -import {hasAuthParamsInUrl} from '@asgardeo/browser'; +import {hasAuthParamsInUrl, hasCalledForThisInstanceInUrl} from '@asgardeo/browser'; /** * Interface for the useBrowserUrl hook return value. @@ -30,6 +30,15 @@ export interface UseBrowserUrl { * @returns True if the URL contains authentication parameters and matches the afterSignInUrl, or if it contains an error parameter */ hasAuthParams: (url: URL, afterSignInUrl: string) => boolean; + + /** + * Checks if the URL indicates that the authentication flow has been called for this instance. + * + * @param url - The URL object to check + * @param instanceId - The instance ID to check against + * @returns True if the URL indicates the flow has been called for this instance + */ + hasCalledForThisInstance: (url: URL, instanceId: number) => boolean; } /** @@ -53,7 +62,10 @@ const useBrowserUrl = (): UseBrowserUrl => { // authParams?.authorizationCode || // FIXME: These are sent externally. Need to see what we can do about this. url.searchParams.get('error') !== null; - return {hasAuthParams}; + const hasCalledForThisInstance = (url: URL, instanceId: number): boolean => + (hasCalledForThisInstanceInUrl(instanceId, url.search)); + + return {hasAuthParams, hasCalledForThisInstance}; }; export default useBrowserUrl; diff --git a/packages/react/src/models/config.ts b/packages/react/src/models/config.ts index f53fdd48..af4a37de 100644 --- a/packages/react/src/models/config.ts +++ b/packages/react/src/models/config.ts @@ -18,4 +18,11 @@ import {AsgardeoBrowserConfig} from '@asgardeo/browser'; -export type AsgardeoReactConfig = AsgardeoBrowserConfig; +export interface AsgardeoReactConfig extends AsgardeoBrowserConfig { + /** + * Optional instance ID for multi-auth context support. + * Use this when you need multiple authentication contexts in the same application. + * Defaults to 0 for backward compatibility. + */ + instanceId?: number; +} \ No newline at end of file