diff --git a/.changeset/calm-deers-hug.md b/.changeset/calm-deers-hug.md new file mode 100644 index 00000000..2a7aeca3 --- /dev/null +++ b/.changeset/calm-deers-hug.md @@ -0,0 +1,6 @@ +--- +"@asgardeo/react": patch +"@asgardeo/js": patch +--- + +Improvements to Identifier first authenticator in the React SDK diff --git a/packages/core/src/i18n/screens/common/en-US.ts b/packages/core/src/i18n/screens/common/en-US.ts index 2e90d059..5c66e5c7 100644 --- a/packages/core/src/i18n/screens/common/en-US.ts +++ b/packages/core/src/i18n/screens/common/en-US.ts @@ -28,5 +28,5 @@ export const common: Common = { 'privacy.policy': 'Privacy Policy', register: 'Register', 'site.title': 'WSO2 Identity Server', - 'terms.of.service': 'Terms of Servicet', + 'terms.of.service': 'Terms of Service', }; diff --git a/packages/core/src/i18n/screens/identifierFirst/en-US.ts b/packages/core/src/i18n/screens/identifierFirst/en-US.ts new file mode 100644 index 00000000..f783f78f --- /dev/null +++ b/packages/core/src/i18n/screens/identifierFirst/en-US.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024, 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 {IdentifierFirst} from './model'; + +export const identifierFirst: IdentifierFirst = { + continue: 'Continue', + 'enter.your.username': 'Enter your username', + 'login.button': 'Sign In', + 'login.heading': 'Sign In', + 'remember.me': 'Remember me on this computer', + retry: 'Login failed! Please check your username and password and try again.', + username: 'Username', +}; diff --git a/packages/core/src/i18n/screens/identifierFirst/fr-FR.ts b/packages/core/src/i18n/screens/identifierFirst/fr-FR.ts new file mode 100644 index 00000000..05a834d9 --- /dev/null +++ b/packages/core/src/i18n/screens/identifierFirst/fr-FR.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024, 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 {IdentifierFirst} from './model'; + +export const identifierFirst: IdentifierFirst = { + continue: 'Continuer', + 'enter.your.username': "Entrez votre nom d'utilisateur", + 'login.button': 'Se connecter', + 'login.heading': 'Se connecter', + 'remember.me': 'Se souvenir de moi sur cet ordinateur', + retry: "Échec de la connexion! Veuillez vérifier votre nom d'utilisateur et votre mot de passe et réessayer.", + username: "Nom d'utilisateur", +}; diff --git a/packages/core/src/i18n/screens/identifierFirst/model.ts b/packages/core/src/i18n/screens/identifierFirst/model.ts new file mode 100644 index 00000000..739eb60c --- /dev/null +++ b/packages/core/src/i18n/screens/identifierFirst/model.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024, 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. + */ + +/** + * Interface for the IdentifierFirst text. + */ +export interface IdentifierFirst { + continue: string; + 'enter.your.username': string; + 'login.button': string; + 'login.heading': string; + 'remember.me': string; + retry: string; + username: string; +} diff --git a/packages/core/src/i18n/screens/keys.ts b/packages/core/src/i18n/screens/keys.ts index 7f777d6b..2d074abd 100644 --- a/packages/core/src/i18n/screens/keys.ts +++ b/packages/core/src/i18n/screens/keys.ts @@ -75,6 +75,23 @@ interface Keys { placeholder: string; }; }; + identifierFirst: { + button: string; + continue: string; + enter: { + your: { + username: string; + }; + }; + login: { + heading: string; + }; + remember: { + me: string; + }; + retry: string; + username: string; + }; login: { button: string; enter: { @@ -183,6 +200,23 @@ export const keys: Keys = { placeholder: 'emailOtp.username.placeholder', }, }, + identifierFirst: { + button: 'identifierFirst.button', + continue: 'identifierFirst.continue', + enter: { + your: { + username: 'identifierFirst.enter.your.username', + }, + }, + login: { + heading: 'identifierFirst.login.heading', + }, + remember: { + me: 'identifierFirst.remember.me', + }, + retry: 'identifierFirst.retry', + username: 'identifierFirst.username', + }, login: { button: 'login.login.button', enter: { diff --git a/packages/core/src/i18n/screens/model.ts b/packages/core/src/i18n/screens/model.ts index 3c15943d..f9c28a87 100644 --- a/packages/core/src/i18n/screens/model.ts +++ b/packages/core/src/i18n/screens/model.ts @@ -17,6 +17,7 @@ */ import {Common} from './common/model'; +import {IdentifierFirst} from './identifierFirst/model'; import {Login} from './login/model'; import {TOTP} from './totp/model'; @@ -25,6 +26,7 @@ import {TOTP} from './totp/model'; */ export interface TextPreference { common: Common; + identifierFirst: IdentifierFirst; login: Login; totp: TOTP; } @@ -32,4 +34,4 @@ export interface TextPreference { /** * Interface for the return type of the getLocalization function. */ -export type TextObject = Login | TOTP | Common; +export type TextObject = Login | TOTP | Common | IdentifierFirst; diff --git a/packages/react/src/assets/building-icon.svg b/packages/react/src/assets/building-icon.svg new file mode 100644 index 00000000..fc8ff94a --- /dev/null +++ b/packages/react/src/assets/building-icon.svg @@ -0,0 +1,25 @@ + + + + + + + diff --git a/packages/react/src/components/SignIn/SignIn.tsx b/packages/react/src/components/SignIn/SignIn.tsx index 73844755..682a37d7 100644 --- a/packages/react/src/components/SignIn/SignIn.tsx +++ b/packages/react/src/components/SignIn/SignIn.tsx @@ -62,7 +62,17 @@ import IdentifierFirst from './fragments/IdentifierFirst'; * @returns {ReactElement} - React element. */ const SignIn: FC = (props: SignInProps): ReactElement => { - const {brandingProps, showFooter = true, showLogo = true, showSignUp} = props; + const { + basicAuthChildren, + brandingProps, + emailOtpChildren, + identifierFirstChildren, + showFooter = true, + showLogo = true, + showSignUp, + smsOtpChildren, + totpChildren, + } = props; const [isComponentLoading, setIsComponentLoading] = useState(true); const [alert, setAlert] = useState(); @@ -231,7 +241,9 @@ const SignIn: FC = (props: SignInProps): ReactElement => { (auth: Authenticator) => auth.authenticatorId !== usernamePasswordAuthenticator.authenticatorId, ), )} - /> + > + {basicAuthChildren} + ); } @@ -252,7 +264,9 @@ const SignIn: FC = (props: SignInProps): ReactElement => { (auth: Authenticator) => auth.authenticatorId !== identifierFirstAuthenticator.authenticatorId, ), )} - /> + > + {identifierFirstChildren} + ); } @@ -264,7 +278,9 @@ const SignIn: FC = (props: SignInProps): ReactElement => { authenticator={authenticators[0]} alert={alert} handleAuthenticate={handleAuthenticate} - /> + > + {totpChildren} + ); } if ( @@ -279,7 +295,9 @@ const SignIn: FC = (props: SignInProps): ReactElement => { brandingProps={brandingProps} authenticator={authenticators[0]} handleAuthenticate={handleAuthenticate} - /> + > + {emailOtpChildren} + ); } @@ -295,7 +313,9 @@ const SignIn: FC = (props: SignInProps): ReactElement => { brandingProps={brandingProps} authenticator={authenticators[0]} handleAuthenticate={handleAuthenticate} - /> + > + {smsOtpChildren} + ); } } diff --git a/packages/react/src/components/SignIn/fragments/BasicAuth.tsx b/packages/react/src/components/SignIn/fragments/BasicAuth.tsx index 722eba08..bc1baadf 100644 --- a/packages/react/src/components/SignIn/fragments/BasicAuth.tsx +++ b/packages/react/src/components/SignIn/fragments/BasicAuth.tsx @@ -18,7 +18,7 @@ import {ScreenType, keys} from '@asgardeo/js'; import {CircularProgress, Grid, Skeleton} from '@oxygen-ui/react'; -import {ReactElement, useContext, useState} from 'react'; +import {PropsWithChildren, ReactElement, useContext, useState} from 'react'; import AsgardeoContext from '../../../contexts/asgardeo-context'; import useTranslations from '../../../hooks/use-translations'; import BasicAuthProps from '../../../models/basic-auth-props'; @@ -41,15 +41,15 @@ import './basic-auth.scss'; const BasicAuth = ({ handleAuthenticate, authenticator, + children, alert, brandingProps, showSelfSignUp, renderLoginOptions, -}: BasicAuthProps): ReactElement => { - const [username, setUsername] = useState(''); +}: PropsWithChildren): ReactElement => { const [password, setPassword] = useState(''); - const {isAuthLoading} = useContext(AsgardeoContext); + const {isAuthLoading, username, setUsername} = useContext(AsgardeoContext); const {t, isLoading} = useTranslations({ componentLocaleOverride: brandingProps?.locale, @@ -103,6 +103,8 @@ const BasicAuth = ({ onChange={(e: React.ChangeEvent): void => setPassword(e.target.value)} /> + {children} + {t(keys.login.button)} diff --git a/packages/react/src/components/SignIn/fragments/EmailOtp.tsx b/packages/react/src/components/SignIn/fragments/EmailOtp.tsx index eb94da42..1fb8e7a0 100644 --- a/packages/react/src/components/SignIn/fragments/EmailOtp.tsx +++ b/packages/react/src/components/SignIn/fragments/EmailOtp.tsx @@ -18,7 +18,7 @@ import {ScreenType, keys} from '@asgardeo/js'; import {CircularProgress, Skeleton} from '@oxygen-ui/react'; -import {ReactElement, useContext, useState} from 'react'; +import {PropsWithChildren, ReactElement, useContext, useState} from 'react'; import AsgardeoContext from '../../../contexts/asgardeo-context'; import useTranslations from '../../../hooks/use-translations'; import EmailOtpProps from '../../../models/email-otp-props'; @@ -35,7 +35,7 @@ import './email-otp.scss'; * @param {AlertType} props.alert - Alert type. * @return {ReactElement} */ -const EmailOtp = ({alert, brandingProps, authenticator, handleAuthenticate}: EmailOtpProps): ReactElement => { +const EmailOtp = ({alert, brandingProps, authenticator, children, handleAuthenticate}: PropsWithChildren): ReactElement => { const [inputValue, setInputValue] = useState(); const [isContinueLoading, setIsContinueLoading] = useState(false); const [isResendLoading, setIsResendLoading] = useState(false); @@ -90,6 +90,8 @@ const EmailOtp = ({alert, brandingProps, authenticator, handleAuthenticate}: Ema onChange={(e: React.ChangeEvent): void => setInputValue(e.target.value)} /> + { children } + { - const [username, setUsername] = useState(''); - - const {isAuthLoading} = useContext(AsgardeoContext); +}: PropsWithChildren): ReactElement => { + const {isAuthLoading, username, setUsername} = useContext(AsgardeoContext); const {t, isLoading} = useTranslations({ componentLocaleOverride: brandingProps?.locale, @@ -91,6 +90,8 @@ const IdentifierFirst = ({ onChange={(e: React.ChangeEvent): void => setUsername(e.target.value)} /> + {children} + {t(keys.login.button)} diff --git a/packages/react/src/components/SignIn/fragments/LoginOptionsBox.tsx b/packages/react/src/components/SignIn/fragments/LoginOptionsBox.tsx index 537b4246..3faeedab 100644 --- a/packages/react/src/components/SignIn/fragments/LoginOptionsBox.tsx +++ b/packages/react/src/components/SignIn/fragments/LoginOptionsBox.tsx @@ -17,6 +17,7 @@ */ import {ReactElement} from 'react'; +import buildingIcon from '../../../assets/building-icon.svg'; import emailSolid from '../../../assets/email-solid.svg'; import smsIcon from '../../../assets/sms-icon.svg'; import facebook from '../../../assets/social-logins/facebook.svg'; @@ -55,7 +56,7 @@ const LoginOptionsBox = ({ } + startIcon={{socialName}} onClick={handleOnClick} disabled={isAuthLoading} > diff --git a/packages/react/src/components/SignIn/fragments/SmsOtp.tsx b/packages/react/src/components/SignIn/fragments/SmsOtp.tsx index e97f60a8..6f3c5fe8 100644 --- a/packages/react/src/components/SignIn/fragments/SmsOtp.tsx +++ b/packages/react/src/components/SignIn/fragments/SmsOtp.tsx @@ -18,12 +18,18 @@ import {ScreenType, keys} from '@asgardeo/js'; import {CircularProgress} from '@oxygen-ui/react'; -import {ReactElement, useState} from 'react'; +import {PropsWithChildren, ReactElement, useState} from 'react'; import useTranslations from '../../../hooks/use-translations'; import EmailOtpProps from '../../../models/email-otp-props'; import {SignIn as UISignIn} from '../../../oxygen-ui-react-auth-components'; -const SmsOtp = ({alert, brandingProps, authenticator, handleAuthenticate}: EmailOtpProps): ReactElement => { +const SmsOtp = ({ + alert, + brandingProps, + authenticator, + children, + handleAuthenticate, +}: PropsWithChildren): ReactElement => { const [otp, setOtp] = useState(); const {isLoading, t} = useTranslations({ @@ -50,6 +56,8 @@ const SmsOtp = ({alert, brandingProps, authenticator, handleAuthenticate}: Email + {children} + { +const Totp = ({ + brandingProps, + authenticator, + children, + handleAuthenticate, + alert, +}: PropsWithChildren): ReactElement => { const [totp, setTotp] = useState(); const {isAuthLoading} = useContext(AsgardeoContext); @@ -79,6 +85,8 @@ const Totp = ({brandingProps, authenticator, handleAuthenticate, alert}: TotpPro + {children} + { const contextValue: AuthContext = useContext(AsgardeoContext); - const {user, isAuthenticated, accessToken, authResponse} = contextValue; + const {accessToken, authResponse, isAuthenticated, setUsername, user, username} = contextValue; const signOut: () => void = () => { signOutApiCall().then(() => { @@ -47,8 +47,10 @@ const useAuthentication = (): UseAuthentication => { accessToken, authResponse, isAuthenticated, + setUsername, signOut, user, + username, }; }; diff --git a/packages/react/src/models/auth-context.ts b/packages/react/src/models/auth-context.ts index cd618a32..ae6794de 100644 --- a/packages/react/src/models/auth-context.ts +++ b/packages/react/src/models/auth-context.ts @@ -35,7 +35,9 @@ interface AuthContext { setIsTextLoading: (value: boolean) => void; setOnSignIn: (response?: any) => void | Promise; setOnSignOut: (response?: any) => void | Promise; + setUsername: (username: string) => void; user: MeAPIResponse; + username: string; } export default AuthContext; diff --git a/packages/react/src/models/sign-in.ts b/packages/react/src/models/sign-in.ts index 4d4b2c8b..fecde462 100644 --- a/packages/react/src/models/sign-in.ts +++ b/packages/react/src/models/sign-in.ts @@ -20,10 +20,15 @@ import {BrandingProps} from '@asgardeo/js'; import {ReactElement} from 'react'; export interface SignInProps { + basicAuthChildren?: ReactElement; brandingProps?: BrandingProps; + emailOtpChildren?: ReactElement; + identifierFirstChildren?: ReactElement; showFooter?: boolean; showLogo?: boolean; showSignUp?: boolean; + smsOtpChildren?: ReactElement; + totpChildren?: ReactElement; } export type AlertType = { diff --git a/packages/react/src/models/use-authentication.ts b/packages/react/src/models/use-authentication.ts index 695df5f7..9d6dc59d 100644 --- a/packages/react/src/models/use-authentication.ts +++ b/packages/react/src/models/use-authentication.ts @@ -22,8 +22,10 @@ interface UseAuthentication { accessToken: string; authResponse: AuthApiResponse; isAuthenticated: Promise | boolean; + setUsername: (username: string) => void; signOut: () => void; user: MeAPIResponse; + username: string; } export default UseAuthentication; diff --git a/packages/react/src/providers/AsgardeoProvider.tsx b/packages/react/src/providers/AsgardeoProvider.tsx index 6513ed47..77a47fa4 100644 --- a/packages/react/src/providers/AsgardeoProvider.tsx +++ b/packages/react/src/providers/AsgardeoProvider.tsx @@ -60,6 +60,7 @@ const AsgardeoProvider: FC> = ( const [isTextLoading, setIsTextLoading] = useState(true); const [isAuthLoading, setIsAuthLoading] = useState(false); const [authResponse, setAuthResponse] = useState(); + const [username, setUsername] = useState(''); const onSignInRef: React.MutableRefObject = useRef(); const onSignOutRef: React.MutableRefObject = useRef(); @@ -146,7 +147,9 @@ const AsgardeoProvider: FC> = ( setIsTextLoading, setOnSignIn, setOnSignOut, + setUsername, user, + username, }), [ accessToken, @@ -160,7 +163,9 @@ const AsgardeoProvider: FC> = ( setAuthentication, setOnSignIn, setOnSignOut, + setUsername, user, + username, ], );