diff --git a/.changeset/yummy-suns-trade.md b/.changeset/yummy-suns-trade.md new file mode 100644 index 00000000..be0d719f --- /dev/null +++ b/.changeset/yummy-suns-trade.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/react': patch +'@asgardeo/i18n': patch +--- + +improve `SignIn` & `SignUp` styling diff --git a/packages/i18n/src/translations/en-US.ts b/packages/i18n/src/translations/en-US.ts index cda31c64..6c8a29b9 100644 --- a/packages/i18n/src/translations/en-US.ts +++ b/packages/i18n/src/translations/en-US.ts @@ -54,7 +54,7 @@ const translations: I18nTranslations = { /* Base Sign In */ 'signin.title': 'Sign In', - 'signin.subtitle': 'Enter your credentials to continue.', + 'signin.subtitle': 'Welcome back! Please sign in to continue.', /* Base Sign Up */ 'signup.title': 'Sign Up', diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.styles.ts b/packages/react/src/components/presentation/SignIn/BaseSignIn.styles.ts index 0b86e3db..dfeb9322 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.styles.ts +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.styles.ts @@ -44,11 +44,12 @@ const useStyles = (theme: Theme, colorScheme: string) => { display: flex; flex-direction: column; align-items: center; - margin-bottom: calc(${theme.vars.spacing.unit} * 3); + margin-bottom: calc(${theme.vars.spacing.unit} * 2); `; const header = css` gap: 0; + align-items: center; `; const title = css` @@ -57,7 +58,7 @@ const useStyles = (theme: Theme, colorScheme: string) => { `; const subtitle = css` - margin-top: calc(${theme.vars.spacing.unit} * 1); + margin-bottom: calc(${theme.vars.spacing.unit} * 1); color: ${theme.vars.colors.text.secondary}; `; @@ -144,7 +145,7 @@ const useStyles = (theme: Theme, colorScheme: string) => { `; const flowMessagesContainer = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); + margin-bottom: calc(${theme.vars.spacing.unit} * 2); `; const flowMessageItem = css` diff --git a/packages/react/src/components/presentation/SignIn/SignIn.tsx b/packages/react/src/components/presentation/SignIn/SignIn.tsx index bc400ea4..07af3b21 100644 --- a/packages/react/src/components/presentation/SignIn/SignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/SignIn.tsx @@ -70,7 +70,7 @@ const SignIn: FC = ({className, size = 'medium', children, ...rest} * Initialize the authentication flow. */ const handleInitialize = async (): Promise => { - return await signIn({response_mode: 'direct'}) as EmbeddedSignInFlowInitiateResponse; + return (await signIn({response_mode: 'direct'})) as EmbeddedSignInFlowInitiateResponse; }; /** @@ -80,7 +80,7 @@ const SignIn: FC = ({className, size = 'medium', children, ...rest} payload: EmbeddedSignInFlowHandleRequestPayload, request: Request, ): Promise => { - return await signIn(payload, request) as EmbeddedSignInFlowHandleResponse; + return (await signIn(payload, request)) as EmbeddedSignInFlowHandleResponse; }; /** @@ -123,6 +123,9 @@ const SignIn: FC = ({className, size = 'medium', children, ...rest} onSuccess={handleSuccess} className={className} size={size} + showLogo={true} + showSubtitle={true} + showTitle={true} {...rest} /> ); diff --git a/packages/react/src/components/presentation/SignIn/component-driven/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/component-driven/BaseSignIn.tsx index f38f268a..8695049d 100644 --- a/packages/react/src/components/presentation/SignIn/component-driven/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/component-driven/BaseSignIn.tsx @@ -31,6 +31,7 @@ import useFlow from '../../../../contexts/Flow/useFlow'; import FlowProvider from '../../../../contexts/Flow/FlowProvider'; import {FormField, useForm} from '../../../../hooks/useForm'; import {renderSignInComponents} from './SignInOptionFactory'; +import {extractErrorMessage} from '../../SignUp/transformer'; /** * Render props for custom UI rendering @@ -61,11 +62,6 @@ export interface BaseSignInRenderProps { */ isLoading: boolean; - /** - * Current error message - */ - error: string | null; - /** * Flow components */ @@ -174,6 +170,21 @@ export interface BaseSignInProps { * Render props function for custom UI */ children?: (props: BaseSignInRenderProps) => ReactNode; + + /** + * Whether to show the title. + */ + showTitle?: boolean; + + /** + * Whether to show the subtitle. + */ + showSubtitle?: boolean; + + /** + * Whether to show the logo. + */ + showLogo?: boolean; } /** @@ -225,17 +236,19 @@ export interface BaseSignInProps { * * ``` */ -const BaseSignIn: FC = props => { +const BaseSignIn: FC = ({showLogo = true, ...rest}: BaseSignInProps): ReactElement => { const {theme} = useTheme(); const styles = useStyles(theme, theme.vars.colors.text.primary); return (
-
- -
+ {showLogo && ( +
+ +
+ )} - +
); @@ -257,17 +270,37 @@ const BaseSignInContent: FC = ({ variant = 'outlined', isLoading: externalIsLoading, children, + showTitle = true, + showSubtitle = true, + showLogo = true, }) => { const {theme} = useTheme(); const {t} = useTranslation(); - const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); + const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow(); const styles = useStyles(theme, theme.vars.colors.text.primary); const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); const isLoading: boolean = externalIsLoading || isSubmitting; + /** + * Handle error responses and extract meaningful error messages + * Uses the transformer's extractErrorMessage function for consistency + */ + const handleError = useCallback( + (error: any) => { + const errorMessage: string = extractErrorMessage(error, t); + + // Clear existing messages and add the error message + clearMessages(); + addMessage({ + type: 'error', + message: errorMessage, + }); + }, + [t, addMessage, clearMessages], + ); + /** * Extract form fields from flow components */ @@ -351,7 +384,7 @@ const BaseSignInContent: FC = ({ } setIsSubmitting(true); - setError(null); + clearMessages(); try { // Filter out empty or undefined input values @@ -380,8 +413,7 @@ const BaseSignInContent: FC = ({ await onSubmit?.(payload, component); } catch (err) { - const errorMessage = err instanceof Error ? err.message : t('errors.sign.in.flow.failure'); - setError(errorMessage); + handleError(err); onError?.(err as Error); } finally { setIsSubmitting(false); @@ -435,7 +467,6 @@ const BaseSignInContent: FC = ({ handleInputChange, { buttonClassName: buttonClasses, - error, inputClassName: inputClasses, onInputBlur: handleInputBlur, onSubmit: handleSubmit, @@ -451,7 +482,6 @@ const BaseSignInContent: FC = ({ isLoading, size, variant, - error, inputClasses, buttonClasses, handleInputBlur, @@ -467,7 +497,6 @@ const BaseSignInContent: FC = ({ touched: touchedFields, isValid: isFormValid, isLoading, - error, components, handleInputChange, handleSubmit, @@ -507,13 +536,21 @@ const BaseSignInContent: FC = ({ return ( - - - {flowTitle || t('signin.title')} - - - {flowSubtitle || t('signin.subtitle')} - + {(showTitle || showSubtitle) && ( + + {showTitle && ( + + {flowTitle || t('signin.title')} + + )} + {showSubtitle && ( + + {flowSubtitle || t('signin.subtitle')} + + )} + + )} + {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message, index) => ( @@ -527,16 +564,6 @@ const BaseSignInContent: FC = ({ ))}
)} -
- - - {error && ( - - {t('errors.title')} - {error} - - )} -
{components && renderComponents(components)}
diff --git a/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx b/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx index 76d70b83..02a5d327 100644 --- a/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx @@ -71,7 +71,6 @@ const createSignInComponentFromFlow = ( onInputChange: (name: string, value: string) => void, options: { buttonClassName?: string; - error?: string | null; inputClassName?: string; key?: string | number; onInputBlur?: (name: string) => void; @@ -216,7 +215,6 @@ export const renderSignInComponents = ( onInputChange: (name: string, value: string) => void, options?: { buttonClassName?: string; - error?: string | null; inputClassName?: string; onInputBlur?: (name: string) => void; onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; diff --git a/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.styles.ts b/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.styles.ts deleted file mode 100644 index 0b86e3db..00000000 --- a/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.styles.ts +++ /dev/null @@ -1,191 +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 {css} from '@emotion/css'; -import {useMemo} from 'react'; -import {Theme} from '@asgardeo/browser'; - -/** - * Creates styles for the BaseSignIn component - * @param theme - The theme object containing design tokens - * @param colorScheme - The current color scheme (used for memoization) - * @returns Object containing CSS class names for component styling - */ -const useStyles = (theme: Theme, colorScheme: string) => { - return useMemo(() => { - const signIn = css` - min-width: 420px; - margin: 0 auto; - `; - - const card = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; - gap: calc(${theme.vars.spacing.unit} * 2); - min-width: 420px; - `; - - const logoContainer = css` - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: calc(${theme.vars.spacing.unit} * 3); - `; - - const header = css` - gap: 0; - `; - - const title = css` - margin: 0 0 calc(${theme.vars.spacing.unit} * 1) 0; - color: ${theme.vars.colors.text.primary}; - `; - - const subtitle = css` - margin-top: calc(${theme.vars.spacing.unit} * 1); - color: ${theme.vars.colors.text.secondary}; - `; - - const messagesContainer = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); - `; - - const messageItem = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 1); - `; - - const errorContainer = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 2); - `; - - const contentContainer = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 2); - `; - - const loadingContainer = css` - display: flex; - flex-direction: column; - align-items: center; - padding: calc(${theme.vars.spacing.unit} * 4); - `; - - const loadingText = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); - color: ${theme.vars.colors.text.secondary}; - `; - - const divider = css` - margin: calc(${theme.vars.spacing.unit} * 1) 0; - `; - - const centeredContainer = css` - text-align: center; - padding: calc(${theme.vars.spacing.unit} * 4); - `; - - const passkeyContainer = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 2); - `; - - const passkeyText = css` - margin-top: calc(${theme.vars.spacing.unit} * 1); - color: ${theme.vars.colors.text.secondary}; - `; - - const form = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 2); - `; - - const formDivider = css` - margin: calc(${theme.vars.spacing.unit} * 1) 0; - `; - - const authenticatorSection = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 1); - `; - - const authenticatorItem = css` - width: 100%; - `; - - const noAuthenticatorCard = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; - padding: calc(${theme.vars.spacing.unit} * 2); - `; - - const errorAlert = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 2); - `; - - const messagesAlert = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 1); - `; - - const flowMessagesContainer = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); - `; - - const flowMessageItem = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 1); - `; - - return { - signIn, - card, - logoContainer, - header, - title, - subtitle, - messagesContainer, - messageItem, - errorContainer, - contentContainer, - loadingContainer, - loadingText, - divider, - centeredContainer, - passkeyContainer, - passkeyText, - form, - formDivider, - authenticatorSection, - authenticatorItem, - noAuthenticatorCard, - errorAlert, - messagesAlert, - flowMessagesContainer, - flowMessageItem, - }; - }, [ - theme.vars.colors.background.surface, - theme.vars.colors.text.primary, - theme.vars.colors.text.secondary, - theme.vars.borderRadius.large, - theme.vars.spacing.unit, - colorScheme, - ]); -}; - -export default useStyles; diff --git a/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.tsx index c959d222..9847adba 100644 --- a/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/non-component-driven/BaseSignIn.tsx @@ -31,7 +31,7 @@ import { handleWebAuthnAuthentication, } from '@asgardeo/browser'; import {cx} from '@emotion/css'; -import {FC, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; +import {FC, FormEvent, useEffect, useState, useCallback, useRef, ReactElement} from 'react'; import {createSignInOptionFromAuthenticator} from './options/SignInOptionFactory'; import FlowProvider from '../../../../contexts/Flow/FlowProvider'; import useFlow from '../../../../contexts/Flow/useFlow'; @@ -44,7 +44,7 @@ import Divider from '../../../primitives/Divider/Divider'; import Logo from '../../../primitives/Logo/Logo'; import Spinner from '../../../primitives/Spinner/Spinner'; import Typography from '../../../primitives/Typography/Typography'; -import useStyles from './BaseSignIn.styles'; +import useStyles from '../BaseSignIn.styles'; /** * Check if the authenticator is a passkey/FIDO authenticator @@ -132,6 +132,21 @@ export interface BaseSignInProps { * Theme variant for the component. */ variant?: CardProps['variant']; + + /** + * Whether to show the title. + */ + showTitle?: boolean; + + /** + * Whether to show the subtitle. + */ + showSubtitle?: boolean; + + /** + * Whether to show the logo. + */ + showLogo?: boolean; } /** @@ -166,17 +181,19 @@ export interface BaseSignInProps { * }; * ``` */ -const BaseSignIn: FC = props => { +const BaseSignIn: FC = ({showLogo = true, ...rest}: BaseSignInProps): ReactElement => { const {theme} = useTheme(); const styles = useStyles(theme, theme.vars.colors.text.primary); return (
-
- -
+ {showLogo && ( +
+ +
+ )} - +
); @@ -207,6 +224,8 @@ const BaseSignInContent: FC = ({ messageClassName = '', size = 'medium', variant = 'outlined', + showTitle = true, + showSubtitle = true, }: BaseSignInProps) => { const {theme} = useTheme(); const {t} = useTranslation(); @@ -989,15 +1008,21 @@ const BaseSignInContent: FC = ({ return ( - - - {flowTitle || t('signin.title')} - - {flowSubtitle && ( - - {flowSubtitle || t('signin.subtitle')} - - )} + {(showTitle || showSubtitle) && ( + + {showTitle && ( + + {flowTitle || t('signin.title')} + + )} + {showSubtitle && ( + + {flowSubtitle || t('signin.subtitle')} + + )} + + )} + {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((flowMessage, index) => ( @@ -1031,9 +1056,6 @@ const BaseSignInContent: FC = ({ })}
)} -
- - {error && ( Error diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.styles.ts b/packages/react/src/components/presentation/SignUp/BaseSignUp.styles.ts index f6037a1f..60bd445f 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.styles.ts +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.styles.ts @@ -44,11 +44,12 @@ const useStyles = (theme: Theme, colorScheme: string) => { display: flex; flex-direction: column; align-items: center; - margin-bottom: calc(${theme.vars.spacing.unit} * 3); + margin-bottom: calc(${theme.vars.spacing.unit} * 2); `; const header = css` gap: 0; + align-items: center; `; const title = css` @@ -57,14 +58,10 @@ const useStyles = (theme: Theme, colorScheme: string) => { `; const subtitle = css` - margin-top: calc(${theme.vars.spacing.unit} * 1); + margin-bottom: calc(${theme.vars.spacing.unit} * 1); color: ${theme.vars.colors.text.secondary}; `; - const messagesContainer = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); - `; - const messageItem = css` margin-bottom: calc(${theme.vars.spacing.unit} * 1); `; @@ -144,7 +141,7 @@ const useStyles = (theme: Theme, colorScheme: string) => { `; const flowMessagesContainer = css` - margin-top: calc(${theme.vars.spacing.unit} * 2); + margin-bottom: calc(${theme.vars.spacing.unit} * 2); `; const flowMessageItem = css` @@ -158,7 +155,6 @@ const useStyles = (theme: Theme, colorScheme: string) => { header, title, subtitle, - messagesContainer, messageItem, errorContainer, contentContainer, diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 63e166c2..53d98867 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -192,6 +192,21 @@ export interface BaseSignUpProps { * Render props function for custom UI */ children?: (props: BaseSignUpRenderProps) => ReactNode; + + /** + * Whether to show the title. + */ + showTitle?: boolean; + + /** + * Whether to show the subtitle. + */ + showSubtitle?: boolean; + + /** + * Whether to show the logo. + */ + showLogo?: boolean; } /** @@ -251,17 +266,19 @@ export interface BaseSignUpProps { * * ``` */ -const BaseSignUp: FC = props => { +const BaseSignUp: FC = ({showLogo = true, ...rest}: BaseSignUpProps): ReactElement => { const {theme, colorScheme} = useTheme(); const styles = useStyles(theme, colorScheme); return (
-
- -
+ {showLogo && ( +
+ +
+ )} - +
); @@ -286,6 +303,8 @@ const BaseSignUpContent: FC = ({ variant = 'outlined', isInitialized, children, + showTitle = true, + showSubtitle = true, }) => { const {theme, colorScheme} = useTheme(); const {t} = useTranslation(); @@ -821,13 +840,21 @@ const BaseSignUpContent: FC = ({ return ( - - - {flowTitle || t('signup.title')} - - - {flowSubtitle || t('signup.subtitle')} - + {(showTitle || showSubtitle) && ( + + {showTitle && ( + + {flowTitle || t('signup.title')} + + )} + {showSubtitle && ( + + {flowSubtitle || t('signup.subtitle')} + + )} + + )} + {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message: any, index: number) => ( @@ -841,9 +868,6 @@ const BaseSignUpContent: FC = ({ ))}
)} -
- -
{currentFlow.data?.components && currentFlow.data.components.length > 0 ? ( renderComponents(currentFlow.data.components) diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx index 587e6f3a..22c30bb4 100644 --- a/packages/react/src/components/presentation/SignUp/SignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx @@ -132,12 +132,17 @@ const SignUp: FC = ({ ): Promise => { // For Thunder/AsgardeoV2 platform, it uses the same API but might return different response format // The transformation will be handled by BaseSignUp's normalizeFlowResponse function + const urlParams: URLSearchParams = new URL(window.location.href).searchParams; + const applicationIdFromUrl: string = urlParams.get('applicationId'); + + // Priority order: flowId from URL > applicationId from context > applicationId from URL + const effectiveApplicationId = applicationId || applicationIdFromUrl; // If no payload provided, create initial payload // For Thunder (Platform.AsgardeoV2), include applicationId for proper initialization const initialPayload = payload || { flowType: EmbeddedFlowType.Registration, - ...(platform === Platform.AsgardeoV2 && applicationId && {applicationId}), + ...(platform === Platform.AsgardeoV2 && effectiveApplicationId && {applicationId: effectiveApplicationId}), }; return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse; @@ -184,6 +189,9 @@ const SignUp: FC = ({ size={size} isInitialized={isInitialized} children={children} + showLogo={true} + showTitle={platform === Platform.AsgardeoV2} + showSubtitle={platform === Platform.AsgardeoV2} {...rest} /> ); diff --git a/packages/react/src/components/primitives/Card/Card.styles.ts b/packages/react/src/components/primitives/Card/Card.styles.ts index f890db1d..19078f39 100644 --- a/packages/react/src/components/primitives/Card/Card.styles.ts +++ b/packages/react/src/components/primitives/Card/Card.styles.ts @@ -66,7 +66,8 @@ const useStyles = (theme: Theme, colorScheme: string, variant: CardVariant, clic `; const headerStyles = css` - padding: calc(${theme.vars.spacing.unit} * 2) calc(${theme.vars.spacing.unit} * 2) 0; + padding: 0 calc(${theme.vars.spacing.unit} * 2); + margin-top: calc(${theme.vars.spacing.unit} * 2); display: flex; flex-direction: column; gap: ${theme.vars.spacing.unit}; @@ -89,7 +90,8 @@ const useStyles = (theme: Theme, colorScheme: string, variant: CardVariant, clic `; const contentStyles = css` - padding: calc(${theme.vars.spacing.unit} * 2); + padding: 0 calc(${theme.vars.spacing.unit} * 2); + margin-bottom: calc(${theme.vars.spacing.unit} * 2); flex: 1; `;