From 488b225ca66cb74b1b303f1949403c13cb882982 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 25 Jun 2025 17:03:39 +0530 Subject: [PATCH 01/10] chore: update QUICKSTART.md for clarity and structure --- packages/nextjs/QUICKSTART.md | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/nextjs/QUICKSTART.md b/packages/nextjs/QUICKSTART.md index 54b01581..4ef1d3b4 100644 --- a/packages/nextjs/QUICKSTART.md +++ b/packages/nextjs/QUICKSTART.md @@ -212,6 +212,51 @@ pnpm dev yarn dev ``` +## Step 10: Embedded Login Page (Optional) + +If you want to use an embedded login page instead of redirecting to Asgardeo, you can use the `SignIn` component: + +Configure the path of the sign-in page in the `middleware.ts` file: + +```diff +import { AsgardeoNext } from '@asgardeo/nextjs'; +import { NextRequest } from 'next/server'; + +const asgardeo = new AsgardeoNext(); + +asgardeo.initialize({ + baseUrl: process.env.NEXT_PUBLIC_ASGARDEO_BASE_URL, + clientId: process.env.NEXT_PUBLIC_ASGARDEO_CLIENT_ID, + clientSecret: process.env.ASGARDEO_CLIENT_SECRET, ++ signInUrl: '/signin', +}); + +export async function middleware(request: NextRequest) { + return await asgardeo.middleware(request); +} + +export const config = { + matcher: [ + '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + '/(api|trpc)(.*)', + ], +}; +``` + +Then, create a new page for the sign-in component in `app/signin/page.tsx`: + +```tsx +'use client'; + +import {SignIn} from '@asgardeo/nextjs'; + +export default function SignInPage() { + return ; +} +``` + +Once you have set this up, clicking on the "Sign In" button will render the embedded login page instead of redirecting to Asgardeo. + ## Next Steps 🎉 **Congratulations!** You've successfully integrated Asgardeo authentication into your Next.js app. From 1b71f9249fa73abdf6cef416f3338653dd707ce6 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 26 Jun 2025 00:23:29 +0530 Subject: [PATCH 02/10] feat(react): add signInUrl and signUpUrl to BaseConfig and update SignInButton behavior --- packages/javascript/src/models/config.ts | 15 +++++++++ .../actions/SignInButton/SignInButton.tsx | 11 +++++-- .../src/contexts/Asgardeo/AsgardeoContext.ts | 6 ++-- .../contexts/Asgardeo/AsgardeoProvider.tsx | 2 ++ .../src/components/Header/PublicActions.tsx | 33 ++----------------- samples/teamspace-react/src/main.tsx | 1 + 6 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index c2410614..056363fd 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -76,6 +76,21 @@ export interface BaseConfig extends WithPreferences { * scopes: ["openid", "profile", "email"] */ scopes?: string | string[] | undefined; + + /** + * Optional URL to redirect the user to sign-in. + * By default, this will be the sign-in page of Asgardeo. + * If you want to use a custom sign-in page, you can provide the URL here and use the `SignIn` component to render it. + */ + signInUrl?: string | undefined; + + /** + * Optional URL to redirect the user to sign-up. + * By default, this will be the sign-up page of Asgardeo. + * If you want to use a custom sign-up page, you can provide the URL here + * and use the `SignUp` component to render it. + */ + signUpUrl?: string | undefined; } export interface WithPreferences { diff --git a/packages/react/src/components/actions/SignInButton/SignInButton.tsx b/packages/react/src/components/actions/SignInButton/SignInButton.tsx index cb3268cc..df5f3833 100644 --- a/packages/react/src/components/actions/SignInButton/SignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/SignInButton.tsx @@ -71,7 +71,7 @@ const SignInButton: ForwardRefExoticComponent(({children, onClick, preferences, ...rest}: SignInButtonProps, ref: Ref): ReactElement => { - const {signIn} = useAsgardeo(); + const {signIn, signInUrl} = useAsgardeo(); const {t} = useTranslation(preferences?.i18n); const [isLoading, setIsLoading] = useState(false); @@ -80,7 +80,14 @@ const SignInButton: ForwardRefExoticComponent = createContext({ - afterSignInUrl: '', - baseUrl: '', + signInUrl: undefined, + afterSignInUrl: undefined, + baseUrl: undefined, isInitialized: false, isLoading: true, isSignedIn: false, diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 43a792ea..b08766d4 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -50,6 +50,7 @@ const AsgardeoProvider: FC> = ({ children, scopes, preferences, + signInUrl, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); @@ -230,6 +231,7 @@ const AsgardeoProvider: FC> = ({ return ( - - {({isLoading}) => ( - - )} - + ); } @@ -39,22 +25,7 @@ export default function PublicActions({className = '', showMobileActions = false
{/* Desktop CTA */}
- Sign In with Redirect - - {({isLoading}) => ( - - )} - + {({isLoading}) => ( diff --git a/samples/teamspace-nextjs/middleware.ts b/samples/teamspace-nextjs/middleware.ts index 7d4f40f6..7f506e37 100644 --- a/samples/teamspace-nextjs/middleware.ts +++ b/samples/teamspace-nextjs/middleware.ts @@ -1,22 +1,12 @@ -import {AsgardeoNext} from '@asgardeo/nextjs'; -import {NextRequest} from 'next/server'; +import {asgardeoMiddleware} from '@asgardeo/nextjs'; -const asgardeo = new AsgardeoNext(); - -asgardeo.initialize({ - baseUrl: process.env.NEXT_PUBLIC_ASGARDEO_BASE_URL, - clientId: process.env.NEXT_PUBLIC_ASGARDEO_CLIENT_ID, - clientSecret: process.env.NEXT_PUBLIC_ASGARDEO_CLIENT_SECRET, - afterSignInUrl: 'http://localhost:3000/dashboard', -}); - -export async function middleware(request: NextRequest) { - return await asgardeo.middleware(request); -} +export default asgardeoMiddleware(); export const config = { matcher: [ + // Skip Next.js internals and all static files, unless found in search params '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + // Always run for API routes '/(api|trpc)(.*)', ], }; From 5eca0fb98ba7db9e644cef68ab3749244376ab81 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 26 Jun 2025 14:32:24 +0530 Subject: [PATCH 04/10] feat: add getConfiguration method to Asgardeo client interfaces and implementations --- packages/javascript/src/AsgardeoJavaScriptClient.ts | 2 ++ packages/javascript/src/models/client.ts | 2 ++ packages/nextjs/src/AsgardeoNextClient.ts | 7 ++++++- .../src/client/contexts/Asgardeo/AsgardeoContext.ts | 8 +------- .../src/client/contexts/Asgardeo/AsgardeoProvider.tsx | 2 ++ packages/nextjs/src/server/AsgardeoProvider.tsx | 10 +++++++++- packages/node/src/AsgardeoNodeClient.ts | 2 +- packages/react/src/AsgardeoReactClient.ts | 4 ++++ 8 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index 1ae6ad68..905ad078 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -46,6 +46,8 @@ abstract class AsgardeoJavaScriptClient implements AsgardeoClient abstract isSignedIn(): Promise; + abstract getConfiguration(): T; + abstract signIn( options?: SignInOptions, sessionId?: string, diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index 0443acc0..405c7e26 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -57,6 +57,8 @@ export interface AsgardeoClient { */ switchOrganization(organization: Organization): Promise; + getConfiguration(): T; + /** * Gets user information from the session. * diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index e4e4e70a..016e60c2 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -90,7 +90,8 @@ class AsgardeoNextClient exte return Promise.resolve(true); } - const {baseUrl, clientId, clientSecret, signInUrl, afterSignInUrl, afterSignOutUrl, signUpUrl, ...rest} = decorateConfigWithNextEnv(config); + const {baseUrl, clientId, clientSecret, signInUrl, afterSignInUrl, afterSignOutUrl, signUpUrl, ...rest} = + decorateConfigWithNextEnv(config); this.isInitialized = true; @@ -149,6 +150,10 @@ class AsgardeoNextClient exte return this.asgardeo.isSignedIn(sessionId as string); } + override getConfiguration(): T { + return this.asgardeo.getConfigData() as unknown as T; + } + override signIn( options?: SignInOptions, sessionId?: string, diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts index 1c91a7e2..db115618 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts @@ -25,13 +25,7 @@ import {Context, createContext} from 'react'; /** * Props interface of {@link AsgardeoContext} */ -export type AsgardeoContextProps = Partial & { - user?: User | null; - isSignedIn?: boolean; - isLoading?: boolean; - signIn?: (payload: EmbeddedSignInFlowHandleRequestPayload, request: EmbeddedFlowExecuteRequestConfig) => Promise; - signOut?: () => Promise; -}; +export type AsgardeoContextProps = AsgardeoReactContextProps; /** * Context object for managing the Authentication flow builder core context. diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index da277fd7..0e4fdd62 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -35,6 +35,7 @@ const AsgardeoClientProvider: FC> signIn, signOut, preferences, + signInUrl, }: PropsWithChildren) => { const router = useRouter(); const [isDarkMode, setIsDarkMode] = useState(false); @@ -127,6 +128,7 @@ const AsgardeoClientProvider: FC> isLoading, signIn: handleSignIn, signOut: handleSignOut, + signInUrl, }), [user, isSignedIn, isLoading], ); diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index a3296113..07d55642 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -74,8 +74,16 @@ const AsgardeoServerProvider: FC> ); } + const configuration = await asgardeoClient.getConfiguration(); + console.log('Asgardeo client initialized with configuration:', configuration); + return ( - + {children} ); diff --git a/packages/node/src/AsgardeoNodeClient.ts b/packages/node/src/AsgardeoNodeClient.ts index 470eefb3..ec6d6583 100644 --- a/packages/node/src/AsgardeoNodeClient.ts +++ b/packages/node/src/AsgardeoNodeClient.ts @@ -23,7 +23,7 @@ import {SignOutOptions} from '@asgardeo/javascript/dist/models/client'; /** * Base class for implementing Asgardeo in Node.js based applications. * This class provides the core functionality for managing user authentication and sessions. - * + *getConfigData * @typeParam T - Configuration type that extends AsgardeoNodeConfig. */ abstract class AsgardeoNodeClient extends AsgardeoJavaScriptClient {} diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 9119d3cc..d531e5d8 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -185,6 +185,10 @@ class AsgardeoReactClient e return this.asgardeo.isSignedIn(); } + override getConfiguration(): T { + return this.asgardeo.getConfigData() as unknown as T; + } + override signIn( options?: SignInOptions, sessionId?: string, From def1d475ae87ef06aac5578e917a7888ef9900d6 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 26 Jun 2025 15:34:01 +0530 Subject: [PATCH 05/10] feat: enhance authentication flow by refactoring SignInButton and SignOutButton, adding loading states, and improving error handling chore: remove unused authRouter file and clean up middleware logic fix: update AsgardeoContext and related types to support optional properties --- packages/nextjs/src/AsgardeoNextClient.ts | 1 - .../actions/SignInButton/SignInButton.tsx | 39 +++-- .../actions/SignOutButton/SignOutButton.tsx | 44 ++++-- .../contexts/Asgardeo/AsgardeoContext.ts | 15 +- .../contexts/Asgardeo/AsgardeoProvider.tsx | 7 +- .../src/middleware/asgardeoMiddleware.ts | 137 +++++++++--------- .../nextjs/src/server/AsgardeoProvider.tsx | 2 + .../nextjs/src/server/actions/authRouter.ts | 83 ----------- .../src/server/actions/gerClientOrigin.ts | 6 +- .../src/server/actions/handlePostSignIn.ts | 4 +- .../src/server/actions/signOutAction.ts | 2 +- .../src/contexts/Asgardeo/AsgardeoContext.ts | 6 +- 12 files changed, 158 insertions(+), 188 deletions(-) delete mode 100644 packages/nextjs/src/server/actions/authRouter.ts diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 016e60c2..c93b996d 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -40,7 +40,6 @@ import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; import getSessionId from './server/actions/getSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; -import authRouter from './server/actions/authRouter'; const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path.slice(0, -1) : path); /** diff --git a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx index 27a53e40..e6476536 100644 --- a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx @@ -18,7 +18,8 @@ 'use client'; -import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes} from 'react'; +import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes, useState, MouseEvent} from 'react'; +import {AsgardeoRuntimeError} from '@asgardeo/node'; import {BaseSignInButton, BaseSignInButtonProps, useTranslation} from '@asgardeo/react'; import useAsgardeo from '../../../../client/contexts/Asgardeo/useAsgardeo'; import {useRouter} from 'next/navigation'; @@ -54,22 +55,39 @@ export type SignInButtonProps = BaseSignInButtonProps; */ const SignInButton = forwardRef( ( - {className, style, children, preferences, ...rest}: SignInButtonProps, + {className, style, children, preferences, onClick, ...rest}: SignInButtonProps, ref: Ref, ): ReactElement => { const {signIn, signInUrl} = useAsgardeo(); const router = useRouter(); const {t} = useTranslation(preferences?.i18n); - const handleOnSubmit = (...args) => { - console.log('SignInButton: handleOnSubmit called with: ', signInUrl); - if (signInUrl) { - router.push(signInUrl); + const [isLoading, setIsLoading] = useState(false); - return; - } + const handleOnClick = async (e: MouseEvent): Promise => { + try { + setIsLoading(true); + + // If a custom `signInUrl` is provided, use it for navigation. + if (signInUrl) { + router.push(signInUrl); + } else { + await signIn(); + } - signIn(); + if (onClick) { + onClick(e); + } + } catch (error) { + throw new AsgardeoRuntimeError( + `Sign in failed: ${error instanceof Error ? error.message : String(error)}`, + 'SignInButton-handleSignIn-RuntimeError-001', + 'next', + 'Something went wrong while trying to sign in. Please try again later.', + ); + } finally { + setIsLoading(false); + } }; return ( @@ -78,8 +96,7 @@ const SignInButton = forwardRef( style={style} ref={ref} preferences={preferences} - type="submit" - onClick={handleOnSubmit} + onClick={handleOnClick} {...rest} > {children ?? t('elements.buttons.signIn')} diff --git a/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx index 681006b9..7d85d835 100644 --- a/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx @@ -18,8 +18,9 @@ 'use client'; -import {FC, forwardRef, PropsWithChildren, ReactElement, Ref} from 'react'; -import {BaseSignOutButton, BaseSignOutButtonProps} from '@asgardeo/react'; +import {FC, forwardRef, PropsWithChildren, ReactElement, Ref, useState, MouseEvent} from 'react'; +import {BaseSignOutButton, BaseSignOutButtonProps, useTranslation} from '@asgardeo/react'; +import {AsgardeoRuntimeError} from '@asgardeo/node'; import useAsgardeo from '../../../../client/contexts/Asgardeo/useAsgardeo'; /** @@ -45,21 +46,42 @@ export type SignOutButtonProps = BaseSignOutButtonProps; * ``` */ const SignOutButton = forwardRef( - ({className, style, ...rest}: SignOutButtonProps, ref: Ref): ReactElement => { + ({className, style, preferences, onClick, children, ...rest}: SignOutButtonProps, ref: Ref): ReactElement => { const {signOut} = useAsgardeo(); + const {t} = useTranslation(preferences?.i18n); + + const [isLoading, setIsLoading] = useState(false); + + const handleOnClick = async (e: MouseEvent): Promise => { + try { + setIsLoading(true); + await signOut(); + + if (onClick) { + onClick(e); + } + } catch (error) { + throw new AsgardeoRuntimeError( + `Sign out failed: ${error instanceof Error ? error.message : String(error)}`, + 'SignOutButton-handleOnClick-RuntimeError-001', + 'next', + 'Something went wrong while trying to sign out. Please try again later.', + ); + } finally { + setIsLoading(false); + } + }; return ( { - console.log('[SignOutButton] signOut called'); - signOut(); - }} + onClick={handleOnClick} + isLoading={isLoading} + preferences={preferences} {...rest} - /> + > + {children ?? t('elements.buttons.signOut')} + ); }, ); diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts index db115618..807c287c 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts @@ -25,12 +25,23 @@ import {Context, createContext} from 'react'; /** * Props interface of {@link AsgardeoContext} */ -export type AsgardeoContextProps = AsgardeoReactContextProps; +export type AsgardeoContextProps = Partial; /** * Context object for managing the Authentication flow builder core context. */ -const AsgardeoContext: Context = createContext({}); +const AsgardeoContext: Context = createContext({ + signInUrl: undefined, + afterSignInUrl: undefined, + baseUrl: undefined, + isInitialized: false, + isLoading: true, + isSignedIn: false, + signIn: null, + signOut: null, + signUp: null, + user: null, +}); AsgardeoContext.displayName = 'AsgardeoContext'; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 0e4fdd62..fb90dd94 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -22,13 +22,16 @@ import {EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowHandleRequestPayload import {I18nProvider, FlowProvider, UserProvider, ThemeProvider, AsgardeoProviderProps} from '@asgardeo/react'; import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; import {useRouter} from 'next/navigation'; -import AsgardeoContext from './AsgardeoContext'; +import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; import {getIsSignedInAction, getUserAction} from '../../../server/actions/authActions'; /** * Props interface of {@link AsgardeoClientProvider} */ -export type AsgardeoClientProviderProps = AsgardeoProviderProps; +export type AsgardeoClientProviderProps = Partial> & Pick & { + signOut: AsgardeoContextProps['signOut']; + signIn: AsgardeoContextProps['signIn']; +}; const AsgardeoClientProvider: FC> = ({ children, diff --git a/packages/nextjs/src/middleware/asgardeoMiddleware.ts b/packages/nextjs/src/middleware/asgardeoMiddleware.ts index a8be6b9f..abcbd3fd 100644 --- a/packages/nextjs/src/middleware/asgardeoMiddleware.ts +++ b/packages/nextjs/src/middleware/asgardeoMiddleware.ts @@ -1,4 +1,3 @@ -import {asgardeoMiddleware} from '@asgardeo/nextjs/middleware'; /** * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * @@ -77,73 +76,75 @@ const asgardeoMiddleware = ( const asgardeoClient = AsgardeoNextClient.getInstance(); - // Initialize client if not already done - if (!asgardeoClient.isInitialized && resolvedOptions) { - asgardeoClient.initialize(resolvedOptions); - } - - // Debug logging - if (resolvedOptions.debug) { - console.log(`[Asgardeo Middleware] Processing request: ${request.nextUrl.pathname}`); - } - - // Handle auth API routes automatically - if (request.nextUrl.pathname.startsWith('/api/auth/asgardeo')) { - if (resolvedOptions.debug) { - console.log(`[Asgardeo Middleware] Handling auth route: ${request.nextUrl.pathname}`); - } - return await asgardeoClient.handleAuthRequest(request); - } - - // Create auth object for the handler - const auth: AsgardeoAuth = { - protect: async (options?: {redirect?: string}) => { - const isSignedIn = await asgardeoClient.isSignedIn(request); - if (!isSignedIn) { - const afterSignInUrl = options?.redirect || '/api/auth/asgardeo/signin'; - return NextResponse.redirect(new URL(afterSignInUrl, request.url)); - } - }, - - isSignedIn: async () => { - return await asgardeoClient.isSignedIn(request); - }, - - getUser: async () => { - return await asgardeoClient.getUser(request); - }, - - redirectToSignIn: (afterSignInUrl?: string) => { - const signInUrl = afterSignInUrl || '/api/auth/asgardeo/signin'; - return NextResponse.redirect(new URL(signInUrl, request.url)); - }, - }; - - // Execute user-provided handler if present - let handlerResponse: NextResponse | void; - if (handler) { - handlerResponse = await handler(auth, request); - } - - // If handler returned a response, use it - if (handlerResponse) { - return handlerResponse; - } - - // Otherwise, continue with default behavior - const response = NextResponse.next(); - - // Add authentication context to response headers - const isSignedIn = await asgardeoClient.isSignedIn(request); - if (isSignedIn) { - response.headers.set('x-asgardeo-authenticated', 'true'); - const user = await asgardeoClient.getUser(request); - if (user?.sub) { - response.headers.set('x-asgardeo-user-id', user.sub); - } - } - - return response; + // // Initialize client if not already done + // if (!asgardeoClient.isInitialized && resolvedOptions) { + // asgardeoClient.initialize(resolvedOptions); + // } + + // // Debug logging + // if (resolvedOptions.debug) { + // console.log(`[Asgardeo Middleware] Processing request: ${request.nextUrl.pathname}`); + // } + + // // Handle auth API routes automatically + // if (request.nextUrl.pathname.startsWith('/api/auth/asgardeo')) { + // if (resolvedOptions.debug) { + // console.log(`[Asgardeo Middleware] Handling auth route: ${request.nextUrl.pathname}`); + // } + // return await asgardeoClient.handleAuthRequest(request); + // } + + // // Create auth object for the handler + // const auth: AsgardeoAuth = { + // protect: async (options?: {redirect?: string}) => { + // const isSignedIn = await asgardeoClient.isSignedIn(request); + // if (!isSignedIn) { + // const afterSignInUrl = options?.redirect || '/api/auth/asgardeo/signin'; + // return NextResponse.redirect(new URL(afterSignInUrl, request.url)); + // } + // }, + + // isSignedIn: async () => { + // return await asgardeoClient.isSignedIn(request); + // }, + + // getUser: async () => { + // return await asgardeoClient.getUser(request); + // }, + + // redirectToSignIn: (afterSignInUrl?: string) => { + // const signInUrl = afterSignInUrl || '/api/auth/asgardeo/signin'; + // return NextResponse.redirect(new URL(signInUrl, request.url)); + // }, + // }; + + // // Execute user-provided handler if present + // let handlerResponse: NextResponse | void; + // if (handler) { + // handlerResponse = await handler(auth, request); + // } + + // // If handler returned a response, use it + // if (handlerResponse) { + // return handlerResponse; + // } + + // // Otherwise, continue with default behavior + // const response = NextResponse.next(); + + // // Add authentication context to response headers + // const isSignedIn = await asgardeoClient.isSignedIn(request); + // if (isSignedIn) { + // response.headers.set('x-asgardeo-authenticated', 'true'); + // const user = await asgardeoClient.getUser(request); + // if (user?.sub) { + // response.headers.set('x-asgardeo-user-id', user.sub); + // } + // } + + // return response; + + return NextResponse.next(); }; }; diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 07d55642..4e3ff230 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -79,10 +79,12 @@ const AsgardeoServerProvider: FC> return ( {children} diff --git a/packages/nextjs/src/server/actions/authRouter.ts b/packages/nextjs/src/server/actions/authRouter.ts deleted file mode 100644 index 5a17d46f..00000000 --- a/packages/nextjs/src/server/actions/authRouter.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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. - */ - -import {NextRequest, NextResponse} from 'next/server'; -import InternalAuthAPIRoutesConfig from '../../configs/InternalAuthAPIRoutesConfig'; -import handlePostSignIn from './handlePostSignIn'; -import handleGetSignIn from './handleGetSignIn'; -import handleSessionRequest from './handleSessionRequest'; -import handleUserRequest from './handleUserRequest'; -import handleSignOut from './handleSignOut'; - -const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path.slice(0, -1) : path); - -/** - * Main authentication request router. - * Routes incoming requests to appropriate handlers based on method and path. - * - * @param req - The Next.js request object - * @returns NextResponse from the appropriate handler or NextResponse.next() if no handler matches - */ -export async function authRouter(req: NextRequest): Promise { - const {pathname, searchParams} = req.nextUrl; - const sanitizedPathname: string = removeTrailingSlash(pathname); - const {method} = req; - - // Handle POST sign-in request - if (method === 'POST' && sanitizedPathname === InternalAuthAPIRoutesConfig.signIn) { - return handlePostSignIn(req); - } - - // Handle GET sign-in request or callback with code - if ((method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.signIn) || searchParams.get('code')) { - return handleGetSignIn(req); - } - - // Handle session status request - if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.session) { - return handleSessionRequest(req); - } - - // Handle user profile request - if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.user) { - return handleUserRequest(req); - } - - // Handle sign-out request - if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.signOut) { - return handleSignOut(req); - } - - // No auth handler found, simply continue to next middleware - // TODO: this should only happen if rolling sessions are enabled. Also, we should - // try to avoid reading from the DB (for stateful sessions) on every request if possible. - // const res = NextResponse.next(); - // const session = await this.sessionStore.get(req.cookies); - - // if (session) { - // // we pass the existing session (containing an `createdAt` timestamp) to the set method - // // which will update the cookie's `maxAge` property based on the `createdAt` time - // await this.sessionStore.set(req.cookies, res.cookies, { - // ...session, - // }); - // } - - return NextResponse.next(); -} - -export default authRouter; diff --git a/packages/nextjs/src/server/actions/gerClientOrigin.ts b/packages/nextjs/src/server/actions/gerClientOrigin.ts index ea19ecc6..b3f8e74e 100644 --- a/packages/nextjs/src/server/actions/gerClientOrigin.ts +++ b/packages/nextjs/src/server/actions/gerClientOrigin.ts @@ -3,9 +3,9 @@ import {headers} from 'next/headers'; const gerClientOrigin = async () => { - const headersList = headers(); - const host = await headersList.get('host'); - const protocol = await headersList.get('x-forwarded-proto') ?? 'http'; + const headersList = await headers(); + const host = headersList.get('host'); + const protocol = headersList.get('x-forwarded-proto') ?? 'http'; return `${protocol}://${host}`; }; diff --git a/packages/nextjs/src/server/actions/handlePostSignIn.ts b/packages/nextjs/src/server/actions/handlePostSignIn.ts index 79a970c5..bc722f08 100644 --- a/packages/nextjs/src/server/actions/handlePostSignIn.ts +++ b/packages/nextjs/src/server/actions/handlePostSignIn.ts @@ -87,9 +87,7 @@ export async function handlePostSignIn(req: NextRequest): Promise (afterSignInUrl: string) => null, ); - const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - const afterSignInUrl = String(afterSignInUrl); - console.log('[AsgardeoNextClient] Sign-in successful, redirecting to:', afterSignInUrl); + const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); return NextResponse.redirect(afterSignInUrl, 303); } diff --git a/packages/nextjs/src/server/actions/signOutAction.ts b/packages/nextjs/src/server/actions/signOutAction.ts index 8f072830..5aaa73ee 100644 --- a/packages/nextjs/src/server/actions/signOutAction.ts +++ b/packages/nextjs/src/server/actions/signOutAction.ts @@ -22,7 +22,7 @@ import {NextRequest, NextResponse} from 'next/server'; import AsgardeoNextClient from '../../AsgardeoNextClient'; import deleteSessionId from './deleteSessionId'; -const signOutAction = async (): Promise<{success: boolean; afterSignInUrl?: string; error?: string}> => { +const signOutAction = async (): Promise<{success: boolean; afterSignOutUrl?: string; error?: unknown}> => { try { const client = AsgardeoNextClient.getInstance(); const afterSignOutUrl: string = await client.signOut(); diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index e2585810..689f842f 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -22,9 +22,9 @@ import {Context, createContext} from 'react'; * Props interface of {@link AsgardeoContext} */ export type AsgardeoContextProps = { - signInUrl: string; - afterSignInUrl: string; - baseUrl: string; + signInUrl: string | undefined; + afterSignInUrl: string | undefined; + baseUrl: string | undefined; isInitialized: boolean; /** * Flag indicating whether the SDK is working in the background. From 5f003f0504a941e901077ba16b103aab7d874f2f Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 26 Jun 2025 15:57:06 +0530 Subject: [PATCH 06/10] chore(nextjs): integrate organization context into Asgardeo components and update Dashboard pages --- packages/nextjs/src/index.ts | 3 ++ .../src/contexts/Asgardeo/AsgardeoContext.ts | 3 ++ .../contexts/Asgardeo/AsgardeoProvider.tsx | 1 + .../teamspace-nextjs/app/dashboard/page.tsx | 39 ++++++++----------- .../teamspace-react/src/pages/Dashboard.tsx | 9 ++--- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 60923889..056d69ed 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -19,6 +19,9 @@ export {default as AsgardeoProvider} from './server/AsgardeoProvider'; export * from './server/AsgardeoProvider'; +export {default as useAsgardeo} from './client/contexts/Asgardeo/useAsgardeo'; +export * from './client/contexts/Asgardeo/useAsgardeo'; + export {default as isSignedIn} from './server/actions/isSignedIn'; export {default as SignedIn} from './client/components/control/SignedIn/SignedIn'; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index 689f842f..db4f5ad1 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -17,6 +17,7 @@ */ import {Context, createContext} from 'react'; +import {Organization} from '@asgardeo/browser'; /** * Props interface of {@link AsgardeoContext} @@ -53,6 +54,7 @@ export type AsgardeoContextProps = { */ signUp: any; user: any; + organization: Organization; }; /** @@ -65,6 +67,7 @@ const AsgardeoContext: Context = createContext> = ({ isInitialized: isInitializedSync, isLoading: asgardeo.isLoading(), isSignedIn: isSignedInSync, + organization: currentOrganization, signIn, signOut, signUp, diff --git a/samples/teamspace-nextjs/app/dashboard/page.tsx b/samples/teamspace-nextjs/app/dashboard/page.tsx index 4293c22c..ac144be8 100644 --- a/samples/teamspace-nextjs/app/dashboard/page.tsx +++ b/samples/teamspace-nextjs/app/dashboard/page.tsx @@ -7,28 +7,10 @@ import {Users, MessageSquare, Calendar, FileText, TrendingUp, Clock, CheckCircle import {redirect} from 'next/navigation'; import {useEffect} from 'react'; import {Header} from '@/components/Header/Header'; +import {useAsgardeo, User} from '@asgardeo/nextjs'; export default function DashboardPage() { - const {user, currentOrg, isAuthenticated, isLoading} = { - user: null, // Replace with actual user data fetching logic - currentOrg: null, // Replace with actual organization data fetching logic - isAuthenticated: false, // Replace with actual authentication check - isLoading: true, // Replace with actual loading state - }; - - useEffect(() => { - if (!isLoading && !isAuthenticated) { - redirect('/'); - } - }, [isAuthenticated, isLoading]); - - if (isLoading) { - return
Loading...
; - } - - if (!isAuthenticated) { - return null; - } + const {organization, user} = useAsgardeo(); const stats = [ { @@ -40,7 +22,7 @@ export default function DashboardPage() { }, { name: 'Team Members', - value: currentOrg?.memberCount.toString() || '0', + value: '0', change: '+5.4%', changeType: 'positive' as const, icon: Users, @@ -122,8 +104,19 @@ export default function DashboardPage() {
-

Welcome back {user?.name}!

-

Here's what's happening with {currentOrg?.name} today.

+

+ Welcome back{' '} + + {user => ( + + {user?.givenName || user?.name?.givenName || user?.given_name}{' '} + {user?.name?.familyName || user?.familyName || user?.family_name} + + )} + + ! +

+

Here's what's happening with {organization?.orgHandle} today.

{/* Stats Grid */} diff --git a/samples/teamspace-react/src/pages/Dashboard.tsx b/samples/teamspace-react/src/pages/Dashboard.tsx index 13ed4640..adb3d10b 100644 --- a/samples/teamspace-react/src/pages/Dashboard.tsx +++ b/samples/teamspace-react/src/pages/Dashboard.tsx @@ -1,9 +1,8 @@ -import {User} from '@asgardeo/react'; -import {useApp} from '../App'; +import {User, useAsgardeo} from '@asgardeo/react'; import {Users, MessageSquare, Calendar, FileText, TrendingUp, Clock, CheckCircle2, AlertCircle} from 'lucide-react'; export default function Dashboard() { - const {currentOrg} = useApp(); + const {organization} = useAsgardeo(); const stats = [ { @@ -15,7 +14,7 @@ export default function Dashboard() { }, { name: 'Team Members', - value: currentOrg?.memberCount.toString() || '0', + value: '0', change: '+5.4%', changeType: 'positive', icon: Users, @@ -108,7 +107,7 @@ export default function Dashboard() { ! -

Here's what's happening with {currentOrg?.name} today.

+

Here's what's happening with @{organization?.orgHandle} today.

{/* Stats Grid */} From 6cc7a29310a4c91da046981044b9003c1ecb63c0 Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 27 Jun 2025 15:24:02 +0530 Subject: [PATCH 07/10] chore(javascript): implement getScim2Me function and related tests, replacing getMeProfile references --- .../src/api/__tests__/getScim2Me.test.ts | 120 ++++++++++++++ packages/javascript/src/api/getScim2Me.ts | 149 ++++++++++++++++++ .../api/scim2/__tests__/getMeProfile.test.ts | 0 .../javascript/src/api/scim2/getMeProfile.ts | 0 packages/javascript/src/api/scim2/index.ts | 0 packages/javascript/src/index.ts | 1 + packages/react/src/AsgardeoReactClient.ts | 6 +- packages/react/src/api/getScim2Me.ts | 102 ++++++++++++ packages/react/src/api/scim2/getMeProfile.ts | 79 ---------- .../presentation/UserProfile/UserProfile.tsx | 1 - .../react/src/contexts/User/UserProvider.tsx | 4 - packages/react/src/index.ts | 3 + 12 files changed, 378 insertions(+), 87 deletions(-) create mode 100644 packages/javascript/src/api/__tests__/getScim2Me.test.ts create mode 100644 packages/javascript/src/api/getScim2Me.ts create mode 100644 packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts create mode 100644 packages/javascript/src/api/scim2/getMeProfile.ts create mode 100644 packages/javascript/src/api/scim2/index.ts create mode 100644 packages/react/src/api/getScim2Me.ts diff --git a/packages/javascript/src/api/__tests__/getScim2Me.test.ts b/packages/javascript/src/api/__tests__/getScim2Me.test.ts new file mode 100644 index 00000000..dc9ff20b --- /dev/null +++ b/packages/javascript/src/api/__tests__/getScim2Me.test.ts @@ -0,0 +1,120 @@ +/** + * 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. + */ + +import {describe, it, expect, vi} from 'vitest'; +import getScim2Me from '../getScim2Me'; +import AsgardeoAPIError from '../../../errors/AsgardeoAPIError'; + +// Mock user data +const mockUser = { + id: '123', + username: 'testuser', + email: 'test@example.com', + givenName: 'Test', + familyName: 'User', +}; + +describe('getScim2Me', () => { + it('should fetch user profile successfully with default fetch', async () => { + // Mock fetch + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: () => Promise.resolve(mockUser), + text: () => Promise.resolve(JSON.stringify(mockUser)), + }); + + // Replace global fetch + global.fetch = mockFetch; + + const result = await getScim2Me({ + url: 'https://api.asgardeo.io/t/test/scim2/Me', + }); + + expect(result).toEqual(mockUser); + expect(mockFetch).toHaveBeenCalledWith('https://api.asgardeo.io/t/test/scim2/Me', { + method: 'GET', + headers: { + 'Content-Type': 'application/scim+json', + Accept: 'application/json', + }, + }); + }); + + it('should use custom fetcher when provided', async () => { + const customFetcher = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: () => Promise.resolve(mockUser), + text: () => Promise.resolve(JSON.stringify(mockUser)), + }); + + const result = await getScim2Me({ + url: 'https://api.asgardeo.io/t/test/scim2/Me', + fetcher: customFetcher, + }); + + expect(result).toEqual(mockUser); + expect(customFetcher).toHaveBeenCalledWith('https://api.asgardeo.io/t/test/scim2/Me', { + method: 'GET', + headers: { + 'Content-Type': 'application/scim+json', + Accept: 'application/json', + }, + }); + }); + + it('should throw AsgardeoAPIError for invalid URL', async () => { + await expect( + getScim2Me({ + url: 'invalid-url', + }), + ).rejects.toThrow(AsgardeoAPIError); + }); + + it('should throw AsgardeoAPIError for failed response', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + text: () => Promise.resolve('User not found'), + }); + + global.fetch = mockFetch; + + await expect( + getScim2Me({ + url: 'https://api.asgardeo.io/t/test/scim2/Me', + }), + ).rejects.toThrow(AsgardeoAPIError); + }); + + it('should handle network errors', async () => { + const mockFetch = vi.fn().mockRejectedValue(new Error('Network error')); + + global.fetch = mockFetch; + + await expect( + getScim2Me({ + url: 'https://api.asgardeo.io/t/test/scim2/Me', + }), + ).rejects.toThrow(AsgardeoAPIError); + }); +}); diff --git a/packages/javascript/src/api/getScim2Me.ts b/packages/javascript/src/api/getScim2Me.ts new file mode 100644 index 00000000..a690678c --- /dev/null +++ b/packages/javascript/src/api/getScim2Me.ts @@ -0,0 +1,149 @@ +/** + * 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. + */ + +import {User} from '../models/user'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Configuration for the getScim2Me request + */ +export interface GetScim2MeConfig extends Omit { + /** + * The absolute API endpoint. + */ + url?: string; + /** + * The base path of the API endpoint. + */ + baseUrl?: string; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Retrieves the user profile information from the specified SCIM2 /Me endpoint. + * + * @param config - Request configuration object. + * @returns A promise that resolves with the user profile information. + * @example + * ```typescript + * // Using default fetch + * try { + * const userProfile = await getScim2Me({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * }); + * console.log(userProfile); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get user profile:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher (e.g., axios-based httpClient) + * try { + * const userProfile = await getScim2Me({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * fetcher: async (url, config) => { + * const response = await httpClient({ + * url, + * method: config.method, + * headers: config.headers, + * ...config + * }); + * // Convert axios-like response to fetch-like Response + * return { + * ok: response.status >= 200 && response.status < 300, + * status: response.status, + * statusText: response.statusText, + * json: () => Promise.resolve(response.data), + * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) + * } as Response; + * } + * }); + * console.log(userProfile); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get user profile:', error.message); + * } + * } + * ``` + */ +const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeConfig): Promise => { + try { + new URL(url ?? baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid URL provided. ${error?.toString()}`, + 'getScim2Me-ValidationError-001', + 'javascript', + 400, + 'The provided `url` or `baseUrl` path does not adhere to the URL schema.', + ); + } + + const fetchFn = fetcher || fetch; + const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me` + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/scim+json', + Accept: 'application/json', + ...requestConfig.headers, + }, + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to fetch user profile: ${errorText}`, + 'getScim2Me-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as User; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'getScim2Me-NetworkError-001', + 'javascript', + 0, + 'Network Error', + ); + } +}; + +export default getScim2Me; diff --git a/packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts b/packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/getMeProfile.ts b/packages/javascript/src/api/scim2/getMeProfile.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/index.ts b/packages/javascript/src/api/scim2/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 2f810d59..2a110aed 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -25,6 +25,7 @@ export {default as initializeEmbeddedSignInFlow} from './api/initializeEmbeddedS export {default as executeEmbeddedSignInFlow} from './api/executeEmbeddedSignInFlow'; export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpFlow'; export {default as getUserInfo} from './api/getUserInfo'; +export {default as getScim2Me, GetScim2MeConfig} from './api/getScim2Me'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index d531e5d8..b20393e6 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -38,7 +38,7 @@ import { } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/scim2/getMeOrganizations'; -import getMeProfile from './api/scim2/getMeProfile'; +import getScim2Me from './api/getScim2Me'; import getSchemas from './api/scim2/getSchemas'; import {AsgardeoReactConfig} from './models/config'; @@ -67,7 +67,7 @@ class AsgardeoReactClient e const configData = await this.asgardeo.getConfigData(); const baseUrl = configData?.baseUrl; - const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const profile = await getScim2Me({baseUrl}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); return generateUserProfile(profile, flattenUserSchema(schemas)); @@ -81,7 +81,7 @@ class AsgardeoReactClient e const configData = await this.asgardeo.getConfigData(); const baseUrl = configData?.baseUrl; - const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const profile = await getScim2Me({baseUrl}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); const processedSchemas = flattenUserSchema(schemas); diff --git a/packages/react/src/api/getScim2Me.ts b/packages/react/src/api/getScim2Me.ts new file mode 100644 index 00000000..d5c5bb4c --- /dev/null +++ b/packages/react/src/api/getScim2Me.ts @@ -0,0 +1,102 @@ +/** + * 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. + */ + +import { + User, + AsgardeoAPIError, + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + getScim2Me as baseGetScim2Me, + GetScim2MeConfig as BaseGetScim2MeConfig +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the getScim2Me request (React-specific) + */ +export interface GetScim2MeConfig extends Omit { + /** + * Optional custom fetcher function. If not provided, the Asgardeo SPA client's httpClient will be used + * which is a wrapper around axios http.request + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Retrieves the user profile information from the specified SCIM2 /Me endpoint. + * This function uses the Asgardeo SPA client's httpClient by default, but allows for custom fetchers. + * + * @param requestConfig - Request configuration object. + * @returns A promise that resolves with the user profile information. + * @example + * ```typescript + * // Using default Asgardeo SPA client httpClient + * try { + * const userProfile = await getScim2Me({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * }); + * console.log(userProfile); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get user profile:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const userProfile = await getScim2Me({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * fetcher: customFetchFunction + * }); + * console.log(userProfile); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get user profile:', error.message); + * } + * } + * ``` + */ +const getScim2Me = async ({fetcher, ...requestConfig}: GetScim2MeConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const response = await httpClient({ + url, + method: config.method || 'GET', + headers: config.headers as Record, + } as HttpRequestConfig); + + return { + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText || '', + json: () => Promise.resolve(response.data), + text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), + } as Response; + }; + + return baseGetScim2Me({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getScim2Me; diff --git a/packages/react/src/api/scim2/getMeProfile.ts b/packages/react/src/api/scim2/getMeProfile.ts index a0620ee3..e69de29b 100644 --- a/packages/react/src/api/scim2/getMeProfile.ts +++ b/packages/react/src/api/scim2/getMeProfile.ts @@ -1,79 +0,0 @@ -/** - * 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. - */ - -import {User, AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * Retrieves the user profile information from the specified selfcare profile endpoint. - * - * @param requestConfig - Request configuration object. - * @returns A promise that resolves with the user profile information. - * @example - * ```typescript - * try { - * const userProfile = await getUserProfile({ - * url: "https://api.asgardeo.io/t//scim2/Me", - * }); - * console.log(userProfile); - * } catch (error) { - * if (error instanceof AsgardeoAPIError) { - * console.error('Failed to get user profile:', error.message); - * } - * } - * ``` - */ -const getMeProfile = async ({url, ...requestConfig}: Partial): Promise => { - try { - new URL(url); - } catch (error) { - throw new AsgardeoAPIError( - 'Invalid endpoint URL provided', - 'getMeProfile-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const response: any = await httpClient({ - url, - method: 'GET', - headers: { - 'Content-Type': 'application/scim+json', - Accept: 'application/json', - }, - } as HttpRequestConfig); - - if (!response.data) { - const errorText = await response.text(); - - throw new AsgardeoAPIError( - `Failed to fetch user profile: ${errorText}`, - 'getMeProfile-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data; -}; - -export default getMeProfile; diff --git a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx index e64deb87..266759d9 100644 --- a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx @@ -18,7 +18,6 @@ import {FC, ReactElement} from 'react'; import BaseUserProfile, {BaseUserProfileProps} from './BaseUserProfile'; -import getMeProfile from '../../../api/scim2/getMeProfile'; import updateMeProfile from '../../../api/scim2/updateMeProfile'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import useUser from '../../../contexts/User/useUser'; diff --git a/packages/react/src/contexts/User/UserProvider.tsx b/packages/react/src/contexts/User/UserProvider.tsx index 0a33b04c..c2c486fa 100644 --- a/packages/react/src/contexts/User/UserProvider.tsx +++ b/packages/react/src/contexts/User/UserProvider.tsx @@ -19,10 +19,6 @@ import {UserProfile} from '@asgardeo/browser'; import {FC, PropsWithChildren, ReactElement, useEffect, useState, useCallback, useMemo} from 'react'; import UserContext from './UserContext'; -import getMeProfile from '../../api/scim2/getMeProfile'; -import getSchemas from '../../api/scim2/getSchemas'; -import updateMeProfile from '../../api/scim2/updateMeProfile'; -import useAsgardeo from '../Asgardeo/useAsgardeo'; /** * Props interface of {@link UserProvider} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cf2f8972..c6105ac2 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -254,3 +254,6 @@ export {default as createOrganization} from './api/scim2/createOrganization'; export * from './api/scim2/createOrganization'; export {default as getMeOrganizations} from './api/scim2/getMeOrganizations'; + +export {default as getMeProfile} from './api/getScim2Me'; +export * from './api/getScim2Me'; From 4d10de894e2c51e3cddb73bb0066d2241feab0dd Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 27 Jun 2025 18:10:53 +0530 Subject: [PATCH 08/10] feat: add loading state to BaseSignIn component and integrate with SignIn props --- .../presentation/SignIn/BaseSignIn.tsx | 127 ++++++++++-------- .../components/presentation/SignIn/SignIn.tsx | 12 +- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index b1ddbcfc..c0e46864 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -252,6 +252,11 @@ export interface BaseSignInProps { */ onFlowChange?: (response: EmbeddedSignInFlowInitiateResponse | EmbeddedSignInFlowHandleResponse) => void; + /** + * Flag to determine the component is ready to be rendered. + */ + isLoading?: boolean; + /** * Function to initialize authentication flow. * @returns Promise resolving to the initial authentication response. @@ -328,6 +333,7 @@ const BaseSignIn: FC = props => ( const BaseSignInContent: FC = ({ afterSignInUrl, onInitialize, + isLoading: externalIsLoading, onSubmit, onSuccess, onError, @@ -343,15 +349,16 @@ const BaseSignInContent: FC = ({ const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); - const [isLoading, setIsLoading] = useState(false); + const [isSignInInitializationRequestLoading, setIsSignInInitializationRequestLoading] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [currentFlow, setCurrentFlow] = useState(null); const [currentAuthenticator, setCurrentAuthenticator] = useState(null); const [error, setError] = useState(null); const [messages, setMessages] = useState>([]); - // Ref to track if initialization has been attempted to prevent multiple calls - const initializationAttemptedRef = useRef(false); + const isLoading = externalIsLoading || isSignInInitializationRequestLoading; + + const reRenderCheckRef = useRef(false); const formFields: FormField[] = currentAuthenticator?.metadata?.params?.map(param => ({ @@ -598,7 +605,7 @@ const BaseSignInContent: FC = ({ return; } - setIsLoading(true); + setIsSignInInitializationRequestLoading(true); setError(null); setMessages([]); @@ -666,7 +673,7 @@ const BaseSignInContent: FC = ({ setError(errorMessage); onError?.(err as Error); } finally { - setIsLoading(false); + setIsSignInInitializationRequestLoading(false); } }; @@ -686,7 +693,7 @@ const BaseSignInContent: FC = ({ touchAllFields(); } - setIsLoading(true); + setIsSignInInitializationRequestLoading(true); setError(null); setMessages([]); @@ -955,7 +962,7 @@ const BaseSignInContent: FC = ({ setError(errorMessage); onError?.(err as Error); } finally { - setIsLoading(false); + setIsSignInInitializationRequestLoading(false); } }; @@ -1020,69 +1027,71 @@ const BaseSignInContent: FC = ({ const errorClasses = clsx([withVendorCSSClassPrefix('signin__error')], errorClassName); - const messageClasses = clsx([withVendorCSSClassPrefix('signin__messages')], messageClassName); + const messageClasses = clsx([withVendorCSSClassPrefix('signin__messages')], messageClassName); // Initialize the flow on component mount - // Initialize the flow on component mount useEffect(() => { - if (!isInitialized && !initializationAttemptedRef.current) { - initializationAttemptedRef.current = true; + if (isLoading) { + return; + } - // Inline initialization to avoid dependency issues - const performInitialization = async () => { - setIsLoading(true); - setError(null); + // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature. + // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state + // This will remount all the useEffects to ensure that there are no unexpected side effects. + // When react remounts the SignIn, it will send two authorize requests. + // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623 + if (reRenderCheckRef.current) { + return; + } - try { - const response = await onInitialize(); + reRenderCheckRef.current = true; - setCurrentFlow(response); - setIsInitialized(true); - onFlowChange?.(response); + (async () => { + setIsSignInInitializationRequestLoading(true); + setError(null); - if (response?.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { - onSuccess?.((response as any).authData || {}); - return; - } + try { + const response = await onInitialize(); - if (response?.nextStep?.authenticators?.length > 0) { - if ( - response.nextStep.stepType === EmbeddedSignInFlowStepType.MultiOptionsPrompt && - response.nextStep.authenticators.length > 1 - ) { - setCurrentAuthenticator(null); - } else { - const authenticator = response.nextStep.authenticators[0]; - setCurrentAuthenticator(authenticator); - setupFormFields(authenticator); - } - } + setCurrentFlow(response); + setIsInitialized(true); + onFlowChange?.(response); - if (response && 'nextStep' in response && response.nextStep && 'messages' in response.nextStep) { - const stepMessages = (response.nextStep as any).messages || []; - setMessages( - stepMessages.map((msg: any) => ({ - type: msg.type || 'INFO', - message: msg.message || '', - })), - ); - } - } catch (err) { - const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.in.initialization'); - setError(errorMessage); - onError?.(err as Error); - } finally { - setIsLoading(false); + if (response?.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { + onSuccess?.((response as any).authData || {}); + return; } - }; - performInitialization(); - } + if (response?.nextStep?.authenticators?.length > 0) { + if ( + response.nextStep.stepType === EmbeddedSignInFlowStepType.MultiOptionsPrompt && + response.nextStep.authenticators.length > 1 + ) { + setCurrentAuthenticator(null); + } else { + const authenticator = response.nextStep.authenticators[0]; + setCurrentAuthenticator(authenticator); + setupFormFields(authenticator); + } + } - // Cleanup function to reset initialization state on unmount - return () => { - initializationAttemptedRef.current = false; - }; - }, [isInitialized]); + if (response && 'nextStep' in response && response.nextStep && 'messages' in response.nextStep) { + const stepMessages = (response.nextStep as any).messages || []; + setMessages( + stepMessages.map((msg: any) => ({ + type: msg.type || 'INFO', + message: msg.message || '', + })), + ); + } + } catch (err) { + const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.in.initialization'); + setError(errorMessage); + onError?.(err as Error); + } finally { + setIsSignInInitializationRequestLoading(false); + } + })(); + }, [isLoading]); if (!isInitialized && isLoading) { return ( diff --git a/packages/react/src/components/presentation/SignIn/SignIn.tsx b/packages/react/src/components/presentation/SignIn/SignIn.tsx index ad6cd147..49f96ff6 100644 --- a/packages/react/src/components/presentation/SignIn/SignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/SignIn.tsx @@ -72,13 +72,14 @@ export interface SignInProps { * ``` */ const SignIn: FC = ({className, size = 'medium', variant = 'outlined'}: SignInProps) => { - const {signIn, afterSignInUrl} = useAsgardeo(); + const {signIn, afterSignInUrl, isInitialized, isLoading} = useAsgardeo(); /** * Initialize the authentication flow. */ - const handleInitialize = async (): Promise => - await signIn({response_mode: 'direct'}); + const handleInitialize = async (): Promise => { + return await signIn({response_mode: 'direct'}); + }; /** * Handle authentication steps. @@ -86,7 +87,9 @@ const SignIn: FC = ({className, size = 'medium', variant = 'outline const handleOnSubmit = async ( payload: EmbeddedSignInFlowHandleRequestPayload, request: Request, - ): Promise => await signIn(payload, request); + ): Promise => { + return await signIn(payload, request); + }; /** * Handle successful authentication and redirect with query params. @@ -107,6 +110,7 @@ const SignIn: FC = ({className, size = 'medium', variant = 'outline return ( Date: Fri, 27 Jun 2025 18:40:09 +0530 Subject: [PATCH 09/10] feat: update error handling in SignIn, SignOut, and SignUp buttons; enhance AsgardeoLoading and SignedIn components with CSR remarks; clean up FieldFactory and BaseCreateOrganization components --- .../actions/SignInButton/SignInButton.tsx | 2 +- .../actions/SignOutButton/SignOutButton.tsx | 3 +- .../actions/SignUpButton/SignUpButton.tsx | 3 +- .../components/control/AsgardeoLoading.tsx | 4 +- .../react/src/components/control/SignedIn.tsx | 2 + .../src/components/control/SignedOut.tsx | 2 + .../src/components/factories/FieldFactory.tsx | 28 ++--------- .../BaseCreateOrganization.tsx | 50 ------------------- packages/react/src/index.ts | 2 - 9 files changed, 17 insertions(+), 79 deletions(-) diff --git a/packages/react/src/components/actions/SignInButton/SignInButton.tsx b/packages/react/src/components/actions/SignInButton/SignInButton.tsx index df5f3833..dcd7b5e9 100644 --- a/packages/react/src/components/actions/SignInButton/SignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/SignInButton.tsx @@ -95,7 +95,7 @@ const SignInButton: ForwardRefExoticComponent): Promise => { try { setIsLoading(true); + await signOut(); if (onClick) { @@ -87,7 +88,7 @@ const SignOutButton: ForwardRefExoticComponent): Promise => { try { setIsLoading(true); + await signUp(); if (onClick) { @@ -88,7 +89,7 @@ const SignUpButton: ForwardRefExoticComponent> = ({ return <>{children}; }; -AsgardeoLoading.displayName = 'Loading'; +AsgardeoLoading.displayName = 'AsgardeoLoading'; export default AsgardeoLoading; diff --git a/packages/react/src/components/control/SignedIn.tsx b/packages/react/src/components/control/SignedIn.tsx index 298e7725..76c9502c 100644 --- a/packages/react/src/components/control/SignedIn.tsx +++ b/packages/react/src/components/control/SignedIn.tsx @@ -32,6 +32,8 @@ export interface SignedInProps { /** * A component that only renders its children when the user is signed in. * + * @remarks This component is only supported in browser based React applications (CSR). + * * @example * ```tsx * import { SignedIn } from '@asgardeo/auth-react'; diff --git a/packages/react/src/components/control/SignedOut.tsx b/packages/react/src/components/control/SignedOut.tsx index 61a9a14c..af89c52b 100644 --- a/packages/react/src/components/control/SignedOut.tsx +++ b/packages/react/src/components/control/SignedOut.tsx @@ -32,6 +32,8 @@ export interface SignedOutProps { /** * A component that only renders its children when the user is signed out. * + * @remarks This component is only supported in browser based React applications (CSR). + * * @example * ```tsx * import { SignedOut } from '@asgardeo/auth-react'; diff --git a/packages/react/src/components/factories/FieldFactory.tsx b/packages/react/src/components/factories/FieldFactory.tsx index bd005583..0bf2c90d 100644 --- a/packages/react/src/components/factories/FieldFactory.tsx +++ b/packages/react/src/components/factories/FieldFactory.tsx @@ -30,9 +30,12 @@ import {FieldType} from '@asgardeo/browser'; * Interface for field configuration. */ export interface FieldConfig { + /** + * The name of the field. + */ name: string; /** - * The field type based on EmbeddedSignInFlowAuthenticatorParamType. + * The field type. */ type: FieldType; /** @@ -77,24 +80,6 @@ export interface FieldConfig { placeholder?: string; } -/** - * Utility function to parse multi-valued string into array - */ -export const parseMultiValuedString = (value: string): string[] => { - if (!value || value.trim() === '') return []; - return value - .split(',') - .map(item => item.trim()) - .filter(item => item.length > 0); -}; - -/** - * Utility function to format array into multi-valued string - */ -export const formatMultiValuedString = (values: string[]): string => { - return values.join(', '); -}; - /** * Utility function to validate field values based on type */ @@ -104,12 +89,10 @@ export const validateFieldValue = ( required: boolean = false, touched: boolean = false, ): string | null => { - // Only show required field errors if the field has been touched if (required && touched && (!value || value.trim() === '')) { return 'This field is required'; } - // If not required and empty, no validation needed if (!value || value.trim() === '') { return null; } @@ -161,7 +144,6 @@ export const createField = (config: FieldConfig): ReactElement => { placeholder, } = config; - // Auto-validate the field value const validationError = error || validateFieldValue(value, type, required, touched); const commonProps = { @@ -237,7 +219,7 @@ export const createField = (config: FieldConfig): ReactElement => { /** * React component wrapper for the field factory. */ -export const FieldFactory: FC = props => { +export const FieldFactory: FC = (props: FieldConfig): ReactElement => { return createField(props); }; diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx index 2884cf52..1dfce0d7 100644 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx @@ -19,11 +19,9 @@ import {withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; import {ChangeEvent, CSSProperties, FC, ReactElement, ReactNode, useMemo, useState} from 'react'; - import {CreateOrganizationPayload} from '../../../api/scim2/createOrganization'; import useTheme from '../../../contexts/Theme/useTheme'; import useTranslation from '../../../hooks/useTranslation'; -import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; import FormControl from '../../primitives/FormControl/FormControl'; @@ -181,8 +179,6 @@ export const BaseCreateOrganization: FC = ({ const styles = useStyles(); const {theme} = useTheme(); const {t} = useTranslation(); - const [avatarUrl, setAvatarUrl] = useState(''); - const [avatarFile, setAvatarFile] = useState(null); const [formData, setFormData] = useState({ description: '', handle: '', @@ -227,44 +223,6 @@ export const BaseCreateOrganization: FC = ({ } }; - const handleAvatarUpload = (event: ChangeEvent): void => { - const file = event.target.files?.[0]; - if (file) { - // Validate file type - if (!file.type.startsWith('image/')) { - setFormErrors(prev => ({ - ...prev, - avatar: 'Please select a valid image file', - })); - return; - } - - // Validate file size (max 2MB) - if (file.size > 2 * 1024 * 1024) { - setFormErrors(prev => ({ - ...prev, - avatar: 'Image size must be less than 2MB', - })); - return; - } - - setAvatarFile(file); - - // Create preview URL - const reader = new FileReader(); - reader.onload = e => { - setAvatarUrl(e.target?.result as string); - }; - reader.readAsDataURL(file); - - // Clear any previous avatar errors - setFormErrors(prev => ({ - ...prev, - avatar: undefined, - })); - } - }; - const handleNameChange = (value: string): void => { handleInputChange('name', value); @@ -310,14 +268,6 @@ export const BaseCreateOrganization: FC = ({ } }; - const defaultRenderHeader = (): ReactElement => ( -
- - {t('organization.create.title')} - -
- ); - const containerStyle = { ...styles.root, ...(cardLayout ? styles.card : {}), diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index c6105ac2..cf52b5b9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -239,8 +239,6 @@ export {default as LogOut} from './components/primitives/Icons/LogOut'; export { createField, FieldFactory, - parseMultiValuedString, - formatMultiValuedString, validateFieldValue, } from './components/factories/FieldFactory'; export * from './components/factories/FieldFactory'; From 9d5eca748006654ff415d86501281f00b8a3800a Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 27 Jun 2025 18:42:59 +0530 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20add=20changeset=20=F0=9F=A6=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/metal-colts-lose.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/metal-colts-lose.md diff --git a/.changeset/metal-colts-lose.md b/.changeset/metal-colts-lose.md new file mode 100644 index 00000000..1690cee8 --- /dev/null +++ b/.changeset/metal-colts-lose.md @@ -0,0 +1,5 @@ +--- +'@asgardeo/nextjs': patch +--- + +Stabilize the SDK