From 4f42f36e8d09c88cc10ce7160b06ebe5e9d17c5d Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 11:50:57 +0530 Subject: [PATCH 01/96] fix: simplify SignIn component usage in API documentation --- packages/react/API.md | 141 +++++++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 24 deletions(-) diff --git a/packages/react/API.md b/packages/react/API.md index b76f0f650..6ac507565 100644 --- a/packages/react/API.md +++ b/packages/react/API.md @@ -102,13 +102,7 @@ const SignInPage = () => { return (

Welcome Back

-+ ++
); }; @@ -661,33 +655,132 @@ Replace default button text with custom content: ### Bring your own UI Library -For applications using popular UI libraries, you can easily integrate Asgardeo components: +For applications using popular UI libraries, you can leverage render props for maximum flexibility and control: -#### Material-UI Integration +#### Material-UI Integration with Render Props ```tsx -import { Button } from '@mui/material' -import { useAsgardeo } from '@asgardeo/react' +import { Button, CircularProgress } from '@mui/material' +import { SignIn } from '@asgardeo/react' function CustomSignInButton() { - const { signIn } = useAsgardeo() - return ( - + + {({ signIn, isLoading, error }) => ( + + )} + ) } ``` -#### Tailwind CSS Integration +#### Tailwind CSS Integration with Render Props ```tsx - - Sign In - +import { SignIn } from '@asgardeo/react' + +function TailwindSignInButton() { + return ( + + {({ signIn, isLoading, error }) => ( +
+ + {error && ( +

{error.message}

+ )} +
+ )} +
+ ) +} +``` + +#### Ant Design Integration with Render Props +```tsx +import { Button, Alert } from 'antd' +import { SignIn } from '@asgardeo/react' + +function AntdSignInButton() { + return ( + + {({ signIn, isLoading, error }) => ( +
+ + {error && ( + + )} +
+ )} +
+ ) +} +``` + +#### Chakra UI Integration with Render Props +```tsx +import { Button, Alert, AlertIcon, Spinner } from '@chakra-ui/react' +import { SignIn } from '@asgardeo/react' + +function ChakraSignInButton() { + return ( + + {({ signIn, isLoading, error }) => ( +
+ + {error && ( + + + {error.message} + + )} +
+ )} +
+ ) +} ``` ### Custom Loading States From 5d28d1c2a3203bb3422c1490b679911605b49cca Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 12:04:58 +0530 Subject: [PATCH 02/96] ci(samples): add initial vercel configuration for rewrites --- samples/teamspace-react/vercel.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 samples/teamspace-react/vercel.json diff --git a/samples/teamspace-react/vercel.json b/samples/teamspace-react/vercel.json new file mode 100644 index 000000000..1323cdac3 --- /dev/null +++ b/samples/teamspace-react/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} From 02f4bff9f86965316e2c0543320c20d3b3c42191 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 13:17:41 +0530 Subject: [PATCH 03/96] chore(react): enhance UserDropdown and BaseUserDropdown with loading state and improve API documentation --- packages/react/API.md | 35 ++++- .../UserDropdown/BaseUserDropdown.tsx | 144 +++++++++++------- .../UserDropdown/UserDropdown.tsx | 4 +- .../presentation/UserProfile/UserProfile.tsx | 4 +- .../components/primitives/Avatar/Avatar.tsx | 1 - .../primitives/Checkbox/Checkbox.tsx | 2 +- .../primitives/DatePicker/DatePicker.tsx | 2 +- .../primitives/OtpField/OtpField.tsx | 2 +- .../components/primitives/Select/Select.tsx | 2 +- .../primitives/TextField/TextField.tsx | 2 +- samples/teamspace-react/src/App.tsx | 6 +- .../src/components/header/UserDropdown.tsx | 7 +- 12 files changed, 137 insertions(+), 74 deletions(-) diff --git a/packages/react/API.md b/packages/react/API.md index 6ac507565..eacae61e1 100644 --- a/packages/react/API.md +++ b/packages/react/API.md @@ -5,7 +5,7 @@ This document provides complete API documentation for the Asgardeo React SDK, in ## Table of Contents - [Components](#components) - - [AsgardeoProvider](#asgardeyprovider) + - [AsgardeoProvider](#asgardeoprovider) - [SignIn](#signin) - [SignedIn](#signedin) - [SignedOut](#signedout) @@ -36,6 +36,10 @@ The root provider component that configures the Asgardeo SDK and provides authen #### Example +##### Minimal Setup + +Following is a minimal setup with the mandatory configuration for the `AsgardeoProvider` in your main application file (e.g., `index.tsx`): + ```diff import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; @@ -49,8 +53,32 @@ root.render( + + ++ + +); +``` + +##### Advanced Usage + +For more advanced configurations, you can specify additional props like `afterSignInUrl`, `afterSignOutUrl`, and `scopes`: + +```diff +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; ++ import { AsgardeoProvider } from '@asgardeo/react'; +import App from './App'; + +const root = createRoot(document.getElementById('root')); + +root.render( + ++ @@ -59,6 +87,7 @@ root.render( ); ``` + **Customization:** See [Customization](#customization) section for theming and styling options. The provider doesn't render any visual elements but can be styled through CSS custom properties. #### Available CSS Classes diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 981a73652..bbae95bfe 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -35,6 +35,7 @@ import { import useTheme from '../../../contexts/Theme/useTheme'; import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; +import Spinner from '../../primitives/Spinner/Spinner'; import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; const useStyles = () => { @@ -120,6 +121,17 @@ const useStyles = () => { fontSize: '0.875rem', margin: 0, } as CSSProperties, + loadingContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '80px', + gap: theme.spacing.unit + 'px', + } as CSSProperties, + loadingText: { + color: theme.colors.text.secondary, + fontSize: '0.875rem', + } as CSSProperties, }), [theme, colorScheme], ); @@ -145,6 +157,10 @@ export interface BaseUserDropdownProps { * The user object containing profile information */ user: any; + /** + * Whether the user data is currently loading + */ + isLoading?: boolean; /** * The HTML element ID where the portal should be mounted */ @@ -187,10 +203,10 @@ export const BaseUserDropdown: FC = ({ fallback = null, className = '', user, + isLoading = false, portalId = 'asgardeo-user-dropdown', menuItems = [], showTriggerLabel = false, - showDropdownHeader = true, avatarSize = 32, attributeMapping = {}, }): ReactElement => { @@ -231,7 +247,7 @@ export const BaseUserDropdown: FC = ({ return getMappedUserProfileValue('username', mergedMappings, user) || ''; }; - if (!user) { + if (fallback && !user && !isLoading) { return fallback; } @@ -246,7 +262,7 @@ export const BaseUserDropdown: FC = ({
{isOpen && ( @@ -267,63 +287,77 @@ export const BaseUserDropdown: FC = ({
- {showDropdownHeader && ( -
- -
- - {getDisplayName()} - - {getMappedUserProfileValue('email', mergedMappings, user) !== getDisplayName() && - getMappedUserProfileValue('email', mergedMappings, user) && ( - - {getMappedUserProfileValue('email', mergedMappings, user)} - - )} -
+ {isLoading ? ( +
+
- )} -
- {menuItems.map((item, index) => ( -
- {item.href ? ( - +
+ +
+ - {item.icon} - {item.label} - - ) : ( - - )} - {index < menuItems.length - 1 &&
} + {getDisplayName()} + + {getMappedUserProfileValue('email', mergedMappings, user) !== getDisplayName() && + getMappedUserProfileValue('email', mergedMappings, user) && ( + + {getMappedUserProfileValue('email', mergedMappings, user)} + + )} +
+
+
+ {menuItems.map((item, index) => ( +
+ {item.href ? ( + + {item.icon} + {item.label} + + ) : ( + + )} + {index < menuItems.length - 1 && ( +
+ )} +
+ ))}
- ))} -
+ + )}
diff --git a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx index f3fe9296e..0bf8000ab 100644 --- a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx @@ -50,9 +50,9 @@ export type UserDropdownProps = Omit; * ``` */ const UserDropdown: FC = ({...rest}: UserDropdownProps): ReactElement => { - const {user} = useAsgardeo(); + const {user, isLoading} = useAsgardeo(); - return ; + return ; }; export default UserDropdown; diff --git a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx index 1f792d4f9..2a420ef00 100644 --- a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx @@ -20,8 +20,8 @@ import {FC, ReactElement} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import useUser from '../../../contexts/User/useUser'; import BaseUserProfile, {BaseUserProfileProps} from './BaseUserProfile'; -import updateMeProfile from 'packages/react/src/api/scim2/updateMeProfile'; -import getMeProfile from 'packages/react/src/api/scim2/getMeProfile'; +import updateMeProfile from '../../../api/scim2/updateMeProfile'; +import getMeProfile from '../../..//api/scim2/getMeProfile'; /** * Props for the UserProfile component. diff --git a/packages/react/src/components/primitives/Avatar/Avatar.tsx b/packages/react/src/components/primitives/Avatar/Avatar.tsx index 899c479f3..947819b6c 100644 --- a/packages/react/src/components/primitives/Avatar/Avatar.tsx +++ b/packages/react/src/components/primitives/Avatar/Avatar.tsx @@ -62,7 +62,6 @@ const useStyles = ({size}) => { fontWeight: 500, color: theme.colors.text.primary, border: `1px solid ${theme.colors.border}`, - boxShadow: colorScheme === 'dark' ? 'none' : '0 2px 4px rgba(0, 0, 0, 0.1)', } as CSSProperties, image: { width: '100%', diff --git a/packages/react/src/components/primitives/Checkbox/Checkbox.tsx b/packages/react/src/components/primitives/Checkbox/Checkbox.tsx index 9628f97c4..ab01a1b10 100644 --- a/packages/react/src/components/primitives/Checkbox/Checkbox.tsx +++ b/packages/react/src/components/primitives/Checkbox/Checkbox.tsx @@ -21,7 +21,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; import FormControl from '../FormControl/FormControl'; import InputLabel from '../InputLabel/InputLabel'; -import {withVendorCSSClassPrefix} from 'packages/browser/dist'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; export interface CheckboxProps extends Omit, 'className' | 'type'> { /** diff --git a/packages/react/src/components/primitives/DatePicker/DatePicker.tsx b/packages/react/src/components/primitives/DatePicker/DatePicker.tsx index b74d7d95b..5f4696ba6 100644 --- a/packages/react/src/components/primitives/DatePicker/DatePicker.tsx +++ b/packages/react/src/components/primitives/DatePicker/DatePicker.tsx @@ -21,7 +21,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; import FormControl from '../FormControl/FormControl'; import InputLabel from '../InputLabel/InputLabel'; -import {withVendorCSSClassPrefix} from 'packages/browser/dist'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; export interface DatePickerProps extends Omit, 'className' | 'type'> { /** diff --git a/packages/react/src/components/primitives/OtpField/OtpField.tsx b/packages/react/src/components/primitives/OtpField/OtpField.tsx index c87fbcbaf..8f2010379 100644 --- a/packages/react/src/components/primitives/OtpField/OtpField.tsx +++ b/packages/react/src/components/primitives/OtpField/OtpField.tsx @@ -21,7 +21,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; import FormControl from '../FormControl/FormControl'; import InputLabel from '../InputLabel/InputLabel'; -import {withVendorCSSClassPrefix} from 'packages/browser/dist'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; export interface OtpInputProps { /** diff --git a/packages/react/src/components/primitives/Select/Select.tsx b/packages/react/src/components/primitives/Select/Select.tsx index acdad0cac..128001814 100644 --- a/packages/react/src/components/primitives/Select/Select.tsx +++ b/packages/react/src/components/primitives/Select/Select.tsx @@ -21,7 +21,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; import FormControl from '../FormControl/FormControl'; import InputLabel from '../InputLabel/InputLabel'; -import {withVendorCSSClassPrefix} from 'packages/browser/dist'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; export interface SelectOption { /** diff --git a/packages/react/src/components/primitives/TextField/TextField.tsx b/packages/react/src/components/primitives/TextField/TextField.tsx index 0401cc473..8682c2419 100644 --- a/packages/react/src/components/primitives/TextField/TextField.tsx +++ b/packages/react/src/components/primitives/TextField/TextField.tsx @@ -21,7 +21,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; import FormControl from '../FormControl/FormControl'; import InputLabel from '../InputLabel/InputLabel'; -import {withVendorCSSClassPrefix} from 'packages/browser/dist'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; export interface TextFieldProps extends Omit, 'className'> { /** diff --git a/samples/teamspace-react/src/App.tsx b/samples/teamspace-react/src/App.tsx index fcf836b5f..da49113a9 100644 --- a/samples/teamspace-react/src/App.tsx +++ b/samples/teamspace-react/src/App.tsx @@ -52,7 +52,7 @@ const mockUser: User = { id: '1', name: 'John Doe', email: 'john@example.com', - avatar: '/placeholder.svg?height=32&width=32', + avatar: 'https://avatar.vercel.sh/john?size=30', username: 'johndoe', }; @@ -61,7 +61,7 @@ const mockOrganizations: Organization[] = [ id: '1', name: 'Acme Corp', slug: 'acme-corp', - avatar: '/placeholder.svg?height=32&width=32', + avatar: 'https://avatar.vercel.sh/acme-corp?size=32', role: 'owner', memberCount: 12, }, @@ -69,7 +69,7 @@ const mockOrganizations: Organization[] = [ id: '2', name: 'Tech Startup', slug: 'tech-startup', - avatar: '/placeholder.svg?height=32&width=32', + avatar: 'https://avatar.vercel.sh/tech-startup?size=30', role: 'admin', memberCount: 8, }, diff --git a/samples/teamspace-react/src/components/header/UserDropdown.tsx b/samples/teamspace-react/src/components/header/UserDropdown.tsx index bef0425ca..3b71148b5 100644 --- a/samples/teamspace-react/src/components/header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/header/UserDropdown.tsx @@ -3,7 +3,7 @@ import {useState, useRef, useEffect} from 'react'; import {Link} from 'react-router-dom'; import {ChevronDown, Settings, User as UserIcon, LogOut} from 'lucide-react'; -import {SignOutButton, User, UserProfile} from '@asgardeo/react'; +import {SignOutButton, User, UserProfile, UserDropdown as ReactUserDropdown} from '@asgardeo/react'; import {useApp} from '../../App'; export default function UserDropdown() { @@ -26,13 +26,14 @@ export default function UserDropdown() { return ( <>
- + */} + {showUserDropdown && (
From 0530089042a044bffb5b2552dae1115eac0ce82f Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 13:32:53 +0530 Subject: [PATCH 04/96] fix(react): update BaseUserDropdown to use Typography component and improve loading state handling --- .../UserDropdown/BaseUserDropdown.tsx | 122 ++++++++---------- 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index bbae95bfe..211291cc7 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -35,7 +35,7 @@ import { import useTheme from '../../../contexts/Theme/useTheme'; import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; -import Spinner from '../../primitives/Spinner/Spinner'; +import Typography from '../../primitives/Typography/Typography'; import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; const useStyles = () => { @@ -108,7 +108,7 @@ const useStyles = () => { headerInfo: { display: 'flex', flexDirection: 'column', - gap: theme.spacing.unit / 2 + 'px', + gap: theme.spacing.unit / 4 + 'px', } as CSSProperties, headerName: { color: theme.colors.text.primary, @@ -232,6 +232,7 @@ export const BaseUserDropdown: FC = ({ firstName: 'name.givenName', lastName: 'name.familyName', email: 'emails', + username: 'userName', }; const mergedMappings = {...defaultAttributeMappings, ...attributeMapping}; @@ -291,73 +292,62 @@ export const BaseUserDropdown: FC = ({ style={{...floatingStyles, ...styles.dropdownContent}} {...getFloatingProps()} > - {isLoading ? ( -
- +
+ +
+ + {getDisplayName()} + + + {getMappedUserProfileValue('username', mergedMappings, user) || + getMappedUserProfileValue('email', mergedMappings, user)} +
- ) : ( - <> -
- -
- + -
- {menuItems.map((item, index) => ( -
- {item.href ? ( - - {item.icon} - {item.label} - - ) : ( - - )} - {index < menuItems.length - 1 && ( -
- )} -
- ))} + {item.icon} + {item.label} + + ) : ( + + )} + {index < menuItems.length - 1 && ( +
+ )}
- - )} + ))} +
From 6bd4f01e143a3fe90363cbe61afd28a7975540f2 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 14:15:39 +0530 Subject: [PATCH 05/96] feat(react): enhance BaseUserDropdown with profile management and sign-out options --- .../UserDropdown/BaseUserDropdown.tsx | 81 ++++++++++++++++--- .../UserDropdown/UserDropdown.tsx | 31 +++++-- .../components/primitives/Button/Button.tsx | 10 +-- .../components/primitives/Icons/LogOut.tsx | 43 ++++++++++ .../src/components/primitives/Icons/User.tsx | 42 ++++++++++ .../contexts/Asgardeo/AsgardeoProvider.tsx | 8 +- packages/react/src/index.ts | 2 + 7 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 packages/react/src/components/primitives/Icons/LogOut.tsx create mode 100644 packages/react/src/components/primitives/Icons/User.tsx diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 211291cc7..c375216eb 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -36,6 +36,8 @@ import useTheme from '../../../contexts/Theme/useTheme'; import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; import Typography from '../../primitives/Typography/Typography'; +import User from '../../primitives/Icons/User'; +import LogOut from '../../primitives/Icons/LogOut'; import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; const useStyles = () => { @@ -58,8 +60,11 @@ const useStyles = () => { } as CSSProperties, userName: { color: theme.colors.text.primary, - fontSize: '1rem', fontWeight: 500, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: '120px', } as CSSProperties, dropdownContent: { minWidth: '200px', @@ -81,6 +86,7 @@ const useStyles = () => { menuItem: { display: 'flex', alignItems: 'center', + justifyContent: 'flex-start', gap: theme.spacing.unit + 'px', padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 2}px`, width: '100%', @@ -90,6 +96,7 @@ const useStyles = () => { background: 'none', cursor: 'pointer', fontSize: '0.875rem', + textAlign: 'left', '&:hover': { backgroundColor: theme.colors.background, }, @@ -109,17 +116,28 @@ const useStyles = () => { display: 'flex', flexDirection: 'column', gap: theme.spacing.unit / 4 + 'px', + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', } as CSSProperties, headerName: { color: theme.colors.text.primary, fontSize: '1rem', fontWeight: 500, margin: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', } as CSSProperties, headerEmail: { color: theme.colors.text.secondary, fontSize: '0.875rem', margin: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', } as CSSProperties, loadingContainer: { display: 'flex', @@ -181,6 +199,14 @@ export interface BaseUserDropdownProps { * Optional size for the avatar */ avatarSize?: number; + /** + * Callback function for "Manage Profile" action + */ + onManageProfile?: () => void; + /** + * Callback function for "Sign Out" action + */ + onSignOut?: () => void; /** * Mapping of component attribute names to identity provider field names. * Allows customizing which user profile fields should be used for each attribute. @@ -206,8 +232,10 @@ export const BaseUserDropdown: FC = ({ isLoading = false, portalId = 'asgardeo-user-dropdown', menuItems = [], - showTriggerLabel = false, + showTriggerLabel = true, avatarSize = 32, + onManageProfile, + onSignOut, attributeMapping = {}, }): ReactElement => { const styles = useStyles(); @@ -259,6 +287,35 @@ export const BaseUserDropdown: FC = ({ setIsOpen(false); }; + // Create default menu items + const defaultMenuItems: MenuItem[] = []; + + if (onManageProfile) { + defaultMenuItems.push({ + label: 'Manage Profile', + onClick: onManageProfile, + icon: , + }); + } + + if (onSignOut) { + defaultMenuItems.push({ + label: 'Sign Out', + onClick: onSignOut, + icon: , + }); + } + + // Merge custom menu items with default ones + const allMenuItems = [...menuItems]; + if (defaultMenuItems.length > 0) { + // Add divider before default items if there are custom items + if (menuItems.length > 0) { + allMenuItems.push({label: '', onClick: undefined}); // Divider placeholder + } + allMenuItems.push(...defaultMenuItems); + } + return (
@@ -301,6 +362,7 @@ export const BaseUserDropdown: FC = ({ />
= ({ {getDisplayName()} = ({
- {menuItems.map((item, index) => ( + {allMenuItems.map((item, index) => (
- {item.href ? ( + {item.label === '' ? ( + // Render divider for empty label placeholder + diff --git a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx index 0bf8000ab..78fe3cc56 100644 --- a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx @@ -16,15 +16,17 @@ * under the License. */ -import {FC, ReactElement} from 'react'; +import {FC, ReactElement, useState} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import BaseUserDropdown, {BaseUserDropdownProps} from './BaseUserDropdown'; +import UserProfile from '../UserProfile/UserProfile'; +import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; /** * Props for the UserDropdown component. - * Extends BaseUserDropdownProps but makes the user prop optional since it will be obtained from useAsgardeo + * Extends BaseUserDropdownProps but excludes user, onManageProfile, and onSignOut since they're handled internally */ -export type UserDropdownProps = Omit; +export type UserDropdownProps = Omit; /** * UserDropdown component displays a user avatar with a dropdown menu. @@ -50,9 +52,28 @@ export type UserDropdownProps = Omit; * ``` */ const UserDropdown: FC = ({...rest}: UserDropdownProps): ReactElement => { - const {user, isLoading} = useAsgardeo(); + const {user, isLoading, signOut} = useAsgardeo(); + const [isProfileOpen, setIsProfileOpen] = useState(false); - return ; + const handleManageProfile = () => { + setIsProfileOpen(true); + }; + + const handleSignOut = () => { + signOut(); + }; + + return ( + <> + + + ); }; export default UserDropdown; diff --git a/packages/react/src/components/primitives/Button/Button.tsx b/packages/react/src/components/primitives/Button/Button.tsx index 0559b4e0c..7b27ab89c 100644 --- a/packages/react/src/components/primitives/Button/Button.tsx +++ b/packages/react/src/components/primitives/Button/Button.tsx @@ -327,17 +327,17 @@ const Button = forwardRef( {...rest} > {loading && ( - )} {!loading && startIcon && {startIcon}} - {children && {children}} + {children && <>{children}} {!loading && endIcon && {endIcon}} ); diff --git a/packages/react/src/components/primitives/Icons/LogOut.tsx b/packages/react/src/components/primitives/Icons/LogOut.tsx new file mode 100644 index 000000000..95445326e --- /dev/null +++ b/packages/react/src/components/primitives/Icons/LogOut.tsx @@ -0,0 +1,43 @@ +/** + * 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 {FC, SVGProps} from 'react'; + +/** + * LogOut icon component. + */ +const LogOut: FC> = props => ( + + + + + +); + +export default LogOut; diff --git a/packages/react/src/components/primitives/Icons/User.tsx b/packages/react/src/components/primitives/Icons/User.tsx new file mode 100644 index 000000000..94f7c3d09 --- /dev/null +++ b/packages/react/src/components/primitives/Icons/User.tsx @@ -0,0 +1,42 @@ +/** + * 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 {FC, SVGProps} from 'react'; + +/** + * User icon component. + */ +const User: FC> = props => ( + + + + +); + +export default User; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index ff14085c0..92197a40f 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -142,7 +142,11 @@ const AsgardeoProvider: FC> = ({ const signIn = async (options?: SignInOptions): Promise => { try { const response = await asgardeo.signIn(options); - // setUser(await asgardeo.getUser()); + + if (await asgardeo.isSignedIn()) { + setUser(await asgardeo.getUser()); + setUserProfile(await asgardeo.getUserProfile()); + } return response; } catch (error) { @@ -177,7 +181,7 @@ const AsgardeoProvider: FC> = ({ }, user, baseUrl, - afterSignInUrl + afterSignInUrl, }} > diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 3de2b0b45..567332f62 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -182,6 +182,8 @@ export {default as CircleCheck} from './components/primitives/Icons/CircleCheck' export {default as CircleAlert} from './components/primitives/Icons/CircleAlert'; export {default as TriangleAlert} from './components/primitives/Icons/TriangleAlert'; export {default as Info} from './components/primitives/Icons/Info'; +export {default as UserIcon} from './components/primitives/Icons/User'; +export {default as LogOut} from './components/primitives/Icons/LogOut'; export { createField, From 36077d8b1662041ecc42c1df7b551bbd55d2cad3 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 14:29:37 +0530 Subject: [PATCH 06/96] feat(react): enhance UserDropdown with additional menu items and icons --- .../UserDropdown/BaseUserDropdown.tsx | 50 +++++++++++++++---- .../src/components/header/UserDropdown.tsx | 26 +++++++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index c375216eb..024ba143a 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -88,7 +88,7 @@ const useStyles = () => { alignItems: 'center', justifyContent: 'flex-start', gap: theme.spacing.unit + 'px', - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 2}px`, + padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, width: '100%', color: theme.colors.text.primary, textDecoration: 'none', @@ -97,9 +97,25 @@ const useStyles = () => { cursor: 'pointer', fontSize: '0.875rem', textAlign: 'left', - '&:hover': { - backgroundColor: theme.colors.background, - }, + borderRadius: theme.borderRadius.small, + transition: 'background-color 0.15s ease-in-out', + } as CSSProperties, + menuItemAnchor: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + gap: theme.spacing.unit + 'px', + padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, + width: '100%', + color: theme.colors.text.primary, + textDecoration: 'none', + border: 'none', + background: 'none', + cursor: 'pointer', + fontSize: '0.875rem', + textAlign: 'left', + borderRadius: theme.borderRadius.small, + transition: 'background-color 0.15s ease-in-out', } as CSSProperties, divider: { margin: `${theme.spacing.unit * 0.5}px 0`, @@ -156,7 +172,7 @@ const useStyles = () => { }; export interface MenuItem { - label: string; + label: ReactNode; icon?: ReactNode; onClick?: () => void; href?: string; @@ -232,7 +248,7 @@ export const BaseUserDropdown: FC = ({ isLoading = false, portalId = 'asgardeo-user-dropdown', menuItems = [], - showTriggerLabel = true, + showTriggerLabel = false, avatarSize = 32, onManageProfile, onSignOut, @@ -240,6 +256,10 @@ export const BaseUserDropdown: FC = ({ }): ReactElement => { const styles = useStyles(); const [isOpen, setIsOpen] = useState(false); + const [hoveredItemIndex, setHoveredItemIndex] = useState(null); + const {theme, colorScheme} = useTheme(); + + const hoverBackgroundColor = colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; const {refs, floatingStyles, context} = useFloating({ open: isOpen, @@ -389,21 +409,33 @@ export const BaseUserDropdown: FC = ({ ) : item.href ? ( setHoveredItemIndex(index)} + onMouseLeave={() => setHoveredItemIndex(null)} + onFocus={() => setHoveredItemIndex(index)} + onBlur={() => setHoveredItemIndex(null)} > {item.icon} - {item.label} + {item.label} ) : ( diff --git a/samples/teamspace-react/src/components/header/UserDropdown.tsx b/samples/teamspace-react/src/components/header/UserDropdown.tsx index 3b71148b5..19b14d761 100644 --- a/samples/teamspace-react/src/components/header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/header/UserDropdown.tsx @@ -2,8 +2,9 @@ import {useState, useRef, useEffect} from 'react'; import {Link} from 'react-router-dom'; -import {ChevronDown, Settings, User as UserIcon, LogOut} from 'lucide-react'; +import {ChevronDown, Settings, User as UserIcon, LogOut, CogIcon} from 'lucide-react'; import {SignOutButton, User, UserProfile, UserDropdown as ReactUserDropdown} from '@asgardeo/react'; +import {PoundSterling} from 'lucide-react'; import {useApp} from '../../App'; export default function UserDropdown() { @@ -33,7 +34,28 @@ export default function UserDropdown() { {user => {user?.userName}} */} - + + + Your profile + + ), + onClick: () => null, + }, + { + label: ( + + + Settings + + ), + href: '/settings', + }, + ]} + /> {showUserDropdown && (
From 0e05c939f46ba50101450838ab2b5b3242d46074 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 15:04:34 +0530 Subject: [PATCH 07/96] feat(react): enhance UserDropdown with custom rendering options and improved user profile display --- .../UserDropdown/UserDropdown.tsx | 133 ++++++++++++- .../src/components/header/UserDropdown.tsx | 188 +++++++++--------- 2 files changed, 228 insertions(+), 93 deletions(-) diff --git a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx index 78fe3cc56..1fcd7da50 100644 --- a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx @@ -16,17 +16,51 @@ * under the License. */ -import {FC, ReactElement, useState} from 'react'; +import {FC, ReactElement, ReactNode, useState} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import BaseUserDropdown, {BaseUserDropdownProps} from './BaseUserDropdown'; import UserProfile from '../UserProfile/UserProfile'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; +/** + * Render props data passed to the children function + */ +export interface UserDropdownRenderProps { + /** The authenticated user object */ + user: any; + /** Whether user data is currently loading */ + isLoading: boolean; + /** Function to open the user profile dialog */ + openProfile: () => void; + /** Function to sign out the user */ + signOut: () => void; + /** Whether the profile dialog is currently open */ + isProfileOpen: boolean; + /** Function to close the profile dialog */ + closeProfile: () => void; +} + /** * Props for the UserDropdown component. * Extends BaseUserDropdownProps but excludes user, onManageProfile, and onSignOut since they're handled internally */ -export type UserDropdownProps = Omit; +export type UserDropdownProps = Omit & { + /** + * Render prop function that receives user state and actions. + * When provided, this completely replaces the default dropdown rendering. + */ + children?: (props: UserDropdownRenderProps) => ReactNode; + /** + * Custom render function for the trigger button. + * When provided, this replaces just the trigger button while keeping the dropdown. + */ + renderTrigger?: (props: UserDropdownRenderProps) => ReactNode; + /** + * Custom render function for the dropdown content. + * When provided, this replaces just the dropdown content while keeping the trigger. + */ + renderDropdown?: (props: UserDropdownRenderProps) => ReactNode; +}; /** * UserDropdown component displays a user avatar with a dropdown menu. @@ -34,6 +68,8 @@ export type UserDropdownProps = OmitPlease sign in
} * /> + * + * // Using render props for complete customization + * + * {({ user, isLoading, openProfile, signOut }) => ( + *
+ * + * + *
+ * )} + *
+ * + * // Using partial render props + * ( + * + * )} + * /> * ``` */ -const UserDropdown: FC = ({...rest}: UserDropdownProps): ReactElement => { +const UserDropdown: FC = ({ + children, + renderTrigger, + renderDropdown, + ...rest +}: UserDropdownProps): ReactElement => { const {user, isLoading, signOut} = useAsgardeo(); const [isProfileOpen, setIsProfileOpen] = useState(false); @@ -63,6 +125,68 @@ const UserDropdown: FC = ({...rest}: UserDropdownProps): Reac signOut(); }; + const closeProfile = () => { + setIsProfileOpen(false); + }; + + // Prepare render props data + const renderProps: UserDropdownRenderProps = { + user, + isLoading, + openProfile: handleManageProfile, + signOut: handleSignOut, + isProfileOpen, + closeProfile, + }; + + // If children render prop is provided, use it for complete customization + if (children) { + return ( + <> + {children(renderProps)} + {isProfileOpen && ( + + + User Profile + + + + )} + + ); + } + + // If partial render props are provided, customize specific parts + if (renderTrigger || renderDropdown) { + // This would require significant changes to BaseUserDropdown to support partial customization + // For now, we'll provide a simple implementation that shows how it could work + return ( + <> + {renderTrigger ? ( + renderTrigger(renderProps) + ) : ( + + )} + {/* Note: renderDropdown would need BaseUserDropdown modifications to implement properly */} + {isProfileOpen && ( + + + User Profile + + + + )} + + ); + } + + // Default behavior - use BaseUserDropdown as before return ( <> = ({...rest}: UserDropdownProps): Reac onSignOut={handleSignOut} {...rest} /> + {isProfileOpen && } ); }; diff --git a/samples/teamspace-react/src/components/header/UserDropdown.tsx b/samples/teamspace-react/src/components/header/UserDropdown.tsx index 19b14d761..2aecf33d6 100644 --- a/samples/teamspace-react/src/components/header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/header/UserDropdown.tsx @@ -1,109 +1,119 @@ 'use client'; -import {useState, useRef, useEffect} from 'react'; -import {Link} from 'react-router-dom'; -import {ChevronDown, Settings, User as UserIcon, LogOut, CogIcon} from 'lucide-react'; -import {SignOutButton, User, UserProfile, UserDropdown as ReactUserDropdown} from '@asgardeo/react'; +import {ChevronDown, CogIcon, LogOut, Settings} from 'lucide-react'; +import {UserDropdown as _UserDropdown, SignOutButton, UserIcon, UserProfile} from '@asgardeo/react'; import {PoundSterling} from 'lucide-react'; -import {useApp} from '../../App'; +import {useState, useRef} from 'react'; +import {Link} from 'react-router-dom'; + +export type UserDropdownProps = { + mode?: 'custom' | 'default'; +}; -export default function UserDropdown() { - const {user} = useApp(); +export default function UserDropdown({mode = 'custom'}: UserDropdownProps) { const [showUserDropdown, setShowUserDropdown] = useState(false); const [showUserProfile, setShowUserProfile] = useState(false); const userDropdownRef = useRef(null); - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) { - setShowUserDropdown(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - return ( - <> -
- {/* */} - - - Your profile - - ), - onClick: () => null, - }, - { - label: ( - - - Settings - - ), - href: '/settings', - }, - ]} - /> - - {showUserDropdown && ( -
-
-
{user?.name}
-
@{user?.username}
-
- - + if (mode === 'custom') { + return ( + <_UserDropdown> + {({user, isLoading}) => ( + <> +
+ - setShowUserDropdown(false)} - > - - Settings - + {showUserDropdown && ( +
+
+
+ {user?.name?.givenName} {user?.name?.familyName} +
+
@{user?.userName}
+
-
- - {({signOut}) => ( - )} - + + setShowUserDropdown(false)} + > + + Settings + + +
+ + {({signOut}) => ( + + )} + +
+
+ )}
-
+ + {showUserProfile && } + )} -
+ + ); + } - {showUserProfile && } - + return ( + <_UserDropdown + menuItems={[ + { + label: ( + + + Billing + + ), + onClick: () => null, + }, + { + label: ( + + + Settings + + ), + href: '/settings', + }, + ]} + /> ); } From 7812d5ee8f1057b8017d2602317f82f90efb2c03 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 16:01:29 +0530 Subject: [PATCH 08/96] feat(react): add error code convention documentation and implement embedded signup flow types and interfaces --- ERROR_CODES.md | 154 ++++++++++++++++++ .../src/api/executeEmbeddedSignUpFlow.ts | 125 ++++++++++++++ packages/javascript/src/index.ts | 10 ++ .../javascript/src/models/embedded-flow.ts | 54 ++++++ 4 files changed, 343 insertions(+) create mode 100644 ERROR_CODES.md create mode 100644 packages/javascript/src/api/executeEmbeddedSignUpFlow.ts create mode 100644 packages/javascript/src/models/embedded-flow.ts diff --git a/ERROR_CODES.md b/ERROR_CODES.md new file mode 100644 index 000000000..0525b1b50 --- /dev/null +++ b/ERROR_CODES.md @@ -0,0 +1,154 @@ +# Error Code Convention + +## Overview +This document defines the error code convention used throughout the Asgardeo JavaScript SDK to ensure consistency and maintainability. + +## Format +Error codes follow this format: +``` +[{packageName}-]{functionName}-{ErrorCategory}-{SequentialNumber} +``` + +### Components + +#### 1. Package Name (Optional) +- Use when the function might exist in multiple packages or when disambiguation is needed +- Use the package identifier (e.g., "javascript", "react", "node") +- Examples: `javascript-`, `react-`, `node-` + +#### 2. Function Name +- Use the exact function name as defined in the code +- Use camelCase format matching the function declaration +- Examples: `getUserInfo`, `executeEmbeddedSignUpFlow`, `initializeApplicationNativeAuthentication` + +#### 3. Error Category +Categories represent the type of error: + +- **ValidationError**: Input validation failures, missing required parameters, invalid parameter values +- **ResponseError**: HTTP response errors, network failures, server errors +- **ConfigurationError**: Configuration-related errors, missing configuration, invalid settings +- **AuthenticationError**: Authentication-specific errors, token issues, credential problems +- **AuthorizationError**: Authorization-specific errors, permission denied, access control +- **NetworkError**: Network connectivity issues, timeout errors +- **ParseError**: JSON parsing errors, response format issues + +#### 4. Sequential Number +- Three-digit zero-padded format: `001`, `002`, `003`, etc. +- Start from `001` for each function +- Increment sequentially within each function +- Group by error category for readability + +## Numbering Strategy + +For each function, allocate number ranges by category: +- **001-099**: ValidationError +- **100-199**: ResponseError +- **200-299**: ConfigurationError +- **300-399**: AuthenticationError +- **400-499**: AuthorizationError +- **500-599**: NetworkError +- **600-699**: ParseError + +## Package Prefix Guidelines + +Use the package prefix when: +1. **Multi-package scenarios**: When the same function name exists across different packages +2. **Public APIs**: For functions that are part of the public API and might be referenced externally +3. **Complex projects**: In large codebases where disambiguation aids debugging and maintenance + +Examples of when to use prefixes: +- `javascript-executeEmbeddedSignUpFlow-ValidationError-001` (public API function) +- `react-useAuth-ConfigurationError-201` (React-specific hook) +- `node-createServer-NetworkError-501` (Node.js-specific function) + +## Examples + +### With Package Prefix (Recommended for Public APIs) +```typescript +// executeEmbeddedSignUpFlow Function (JavaScript package) +'javascript-executeEmbeddedSignUpFlow-ValidationError-001' // Missing payload +'javascript-executeEmbeddedSignUpFlow-ValidationError-002' // Invalid flowType +'javascript-executeEmbeddedSignUpFlow-ResponseError-100' // HTTP error response +``` + +### Without Package Prefix (Internal/Simple Functions) +```typescript +// getUserInfo Function (internal utility) +'getUserInfo-ValidationError-001' // Invalid URL +'getUserInfo-ValidationError-002' // Missing access token +'getUserInfo-ResponseError-100' // HTTP error response +'getUserInfo-ResponseError-101' // Invalid response format +``` + +## Implementation Guidelines + +1. **Consistency**: Always use the exact function name in error codes +2. **Package Prefix**: Use package prefixes for public APIs and when disambiguation is needed +3. **Documentation**: Document each error code with clear description +4. **Categorization**: Choose the most appropriate category for each error +5. **Numbering**: Use the range-based numbering system for better organization +6. **Future-proofing**: Leave gaps in numbering for future error codes + +## Current Implementation Status + +### Updated Functions (Following New Convention) +- ✅ `executeEmbeddedSignUpFlow` - Uses `javascript-` prefix with range-based numbering + +### Functions Needing Updates +- ⏳ `getUserInfo` - Currently uses simple format, needs prefix evaluation +- ⏳ `initializeApplicationNativeAuthentication` - Currently uses simple format, needs prefix evaluation +- ⏳ `handleApplicationNativeAuthentication` - Currently uses simple format, needs prefix evaluation + +## Migration Notes + +When updating existing error codes: +1. **Evaluate prefix necessity**: Determine if the function needs a package prefix +2. **Update numbering**: Move to range-based numbering (ValidationError: 001-099, ResponseError: 100-199, etc.) +3. **Update tests**: Ensure all tests use the new error codes +4. **Update documentation**: Document the new error codes +5. **Consider backward compatibility**: If codes are exposed in public APIs, plan migration strategy + +## Example Migration + +### Before (Old Convention) +```typescript +'getUserInfo-ValidationError-001' +'getUserInfo-ResponseError-001' +``` + +### After (New Convention) +```typescript +// Option 1: With prefix (for public API) +'javascript-getUserInfo-ValidationError-001' +'javascript-getUserInfo-ResponseError-100' + +// Option 2: Without prefix (for internal use) +'getUserInfo-ValidationError-001' +'getUserInfo-ResponseError-100' +``` + +## Current Error Code Registry + +### executeEmbeddedSignUpFlow (Updated - New Convention) +- `javascript-executeEmbeddedSignUpFlow-ValidationError-001` - Missing payload +- `javascript-executeEmbeddedSignUpFlow-ValidationError-002` - Invalid flowType +- `javascript-executeEmbeddedSignUpFlow-ResponseError-100` - HTTP error response + +### getUserInfo (Legacy Format) +- `getUserInfo-ValidationError-001` - Invalid endpoint URL +- `getUserInfo-ResponseError-001` - Failed to fetch user info + +### initializeApplicationNativeAuthentication (Legacy Format) +- `initializeApplicationNativeAuthentication-ValidationError-002` - Missing authorization payload +- `initializeApplicationNativeAuthentication-ResponseError-001` - Authorization request failed + +### handleApplicationNativeAuthentication (Legacy Format) +- `handleApplicationNativeAuthentication-ValidationError-002` - Missing required parameter +- `initializeApplicationNativeAuthentication-ResponseError-001` - Response error (Note: incorrect function name reference) + +## Recommended Actions + +1. **Standardize numbering**: Update legacy functions to use range-based numbering +2. **Fix inconsistencies**: Correct the error code in `handleApplicationNativeAuthentication` that references the wrong function +3. **Add prefixes**: Evaluate which functions need package prefixes based on their public API status +4. **Document usage**: Add inline documentation in each file listing the error codes used diff --git a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts new file mode 100644 index 000000000..a7fcf01ec --- /dev/null +++ b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts @@ -0,0 +1,125 @@ +import {EmbeddedFlowExecuteResponse} from './../models/embedded-flows'; +/** + * 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 AsgardeoAPIError from '../errors/AsgardeoAPIError'; +import {EmbeddedFlowType} from '../models/embedded-flows'; + +/** + * Represents the embedded signup flow execution request payload. + */ +export interface EmbeddedSignUpFlowExecuteRequestPayload { + /** + * The type of flow to execute. + */ + flowType: EmbeddedFlowType; +} + +/** + * Request configuration for the embedded signup flow execution function. + */ +export interface EmbeddedSignUpFlowExecuteRequestConfig extends Partial { + url?: string; + /** + * The base URL of the Asgardeo server. + */ + baseUrl?: string; + /** + * The embedded signup flow execution request payload. + */ + payload: EmbeddedSignUpFlowExecuteRequestPayload; +} + +/** + * Executes an embedded signup flow by sending a request to the specified flow execution endpoint. + * + * @param requestConfig - Request configuration object containing URL and payload. + * @returns A promise that resolves with the flow execution response. + * @throws AsgardeoAPIError when the request fails or URL is invalid. + * + * @example + * ```typescript + * try { + * const embeddedSignUpResponse = await executeEmbeddedSignUpFlow({ + * url: "https://api.asgardeo.io/t//api/server/v1/flow/execute", + * payload: { + * flowType: "REGISTRATION" + * } + * }); + * console.log(embeddedSignUpResponse); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Embedded SignUp flow execution failed:', error.message); + * } + * } + * ``` + */ +const executeEmbeddedSignUpFlow = async ({ + url, + baseUrl, + payload, + ...requestConfig +}: EmbeddedSignUpFlowExecuteRequestConfig): Promise => { + if (!payload) { + throw new AsgardeoAPIError( + 'Embedded SignUp flow payload is required', + 'javascript-executeEmbeddedSignUpFlow-ValidationError-001', + 'javascript', + 400, + 'If an embedded signup flow payload is not provided, the request cannot be constructed correctly.', + ); + } + + if (!payload.flowType || payload.flowType !== 'REGISTRATION') { + throw new AsgardeoAPIError( + 'Invalid flowType. Must be "REGISTRATION"', + 'javascript-executeEmbeddedSignUpFlow-ValidationError-002', + 'javascript', + 400, + 'The flowType must be set to "REGISTRATION" for embedded signup flows.', + ); + } + + const {headers: customHeaders, ...otherConfig} = requestConfig; + const response: Response = await fetch(url ?? `${baseUrl}/api/server/v1/flow/execute`, { + method: requestConfig.method || 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...customHeaders, + }, + body: JSON.stringify(payload), + ...otherConfig, + }); + + if (!response.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Embedded SignUp flow execution failed: ${errorText}`, + 'javascript-executeEmbeddedSignUpFlow-ResponseError-100', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as EmbeddedFlowExecuteResponse; +}; + +export default executeEmbeddedSignUpFlow; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 348ac76da..90b7ef5f4 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -23,6 +23,8 @@ export * from './IsomorphicCrypto'; export {default as initializeApplicationNativeAuthentication} from './api/initializeApplicationNativeAuthentication'; export {default as handleApplicationNativeAuthentication} from './api/handleApplicationNativeAuthentication'; +export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpFlow'; +export {default as handleEmbeddedSignUpFlow} from './api/handleEmbeddedSignUpFlow'; export {default as getUserInfo} from './api/getUserInfo'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; @@ -46,7 +48,15 @@ export { ApplicationNativeAuthenticationAuthenticatorParamType, ApplicationNativeAuthenticationAuthenticatorPromptType, ApplicationNativeAuthenticationAuthenticatorKnownIdPType, + EmbeddedSignUpFlowInitiateResponse, + EmbeddedSignUpFlowResponseType, + EmbeddedSignUpFlowViewData, + EmbeddedSignUpFlowComponent, + EmbeddedSignUpFlowComponentType, + EmbeddedSignUpFlowHandleRequest, + EmbeddedSignUpFlowHandleResponse, } from './models/application-native-authentication'; +export {EmbeddedFlowType} from './models/embedded-flows'; export {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; export {BaseConfig, Config, Preferences, ThemePreferences, I18nPreferences, WithPreferences} from './models/config'; export {TokenResponse, IdTokenPayload, TokenExchangeRequestConfig} from './models/token'; diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts new file mode 100644 index 000000000..d828b077e --- /dev/null +++ b/packages/javascript/src/models/embedded-flow.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +export enum EmbeddedFlowType { + Registration = 'REGISTRATION', +} + +export interface EmbeddedFlowExecuteResponse { + flowId: string; + flowStatus: ApplicationNativeAuthenticationFlowStatus; + type: EmbeddedFlowResponseType; + data: EmbeddedSignUpFlowData; +} + +export enum EmbeddedFlowResponseType { + View = 'VIEW', +} + +export interface EmbeddedSignUpFlowData { + components: EmbeddedFlowComponent[]; +} + +export interface EmbeddedFlowComponent { + id: string; + type: EmbeddedFlowComponentType; + variant?: string; + components: EmbeddedFlowComponent[]; + config: Record; +} + +export enum EmbeddedFlowComponentType { + Typography = 'TYPOGRAPHY', + Form = 'FORM', + Button = 'BUTTON', + Input = 'INPUT', + Select = 'SELECT', + Checkbox = 'CHECKBOX', + Radio = 'RADIO', +} From cd1f24cce0c0704dd1edba4234d196fbc741ad1e Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 22:19:45 +0530 Subject: [PATCH 09/96] feat(react): refactor embedded flow types and update response handling in executeEmbeddedSignUpFlow --- .../src/api/executeEmbeddedSignUpFlow.ts | 3 +-- packages/javascript/src/index.ts | 19 +++++++++---------- .../javascript/src/models/embedded-flow.ts | 7 ++++++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts index a7fcf01ec..f04ed9e5c 100644 --- a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts +++ b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts @@ -1,4 +1,3 @@ -import {EmbeddedFlowExecuteResponse} from './../models/embedded-flows'; /** * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * @@ -18,7 +17,7 @@ import {EmbeddedFlowExecuteResponse} from './../models/embedded-flows'; */ import AsgardeoAPIError from '../errors/AsgardeoAPIError'; -import {EmbeddedFlowType} from '../models/embedded-flows'; +import {EmbeddedFlowType, EmbeddedFlowExecuteResponse} from '../models/embedded-flow'; /** * Represents the embedded signup flow execution request payload. diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 90b7ef5f4..d293297c9 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -24,7 +24,6 @@ export * from './IsomorphicCrypto'; export {default as initializeApplicationNativeAuthentication} from './api/initializeApplicationNativeAuthentication'; export {default as handleApplicationNativeAuthentication} from './api/handleApplicationNativeAuthentication'; export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpFlow'; -export {default as handleEmbeddedSignUpFlow} from './api/handleEmbeddedSignUpFlow'; export {default as getUserInfo} from './api/getUserInfo'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; @@ -47,16 +46,16 @@ export { ApplicationNativeAuthenticationHandleResponse, ApplicationNativeAuthenticationAuthenticatorParamType, ApplicationNativeAuthenticationAuthenticatorPromptType, - ApplicationNativeAuthenticationAuthenticatorKnownIdPType, - EmbeddedSignUpFlowInitiateResponse, - EmbeddedSignUpFlowResponseType, - EmbeddedSignUpFlowViewData, - EmbeddedSignUpFlowComponent, - EmbeddedSignUpFlowComponentType, - EmbeddedSignUpFlowHandleRequest, - EmbeddedSignUpFlowHandleResponse, + ApplicationNativeAuthenticationAuthenticatorKnownIdPType } from './models/application-native-authentication'; -export {EmbeddedFlowType} from './models/embedded-flows'; +export { + EmbeddedFlowType, + EmbeddedFlowExecuteResponse, + EmbeddedFlowResponseType, + EmbeddedSignUpFlowData, + EmbeddedFlowComponent, + EmbeddedFlowComponentType, +} from './models/embedded-flow'; export {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; export {BaseConfig, Config, Preferences, ThemePreferences, I18nPreferences, WithPreferences} from './models/config'; export {TokenResponse, IdTokenPayload, TokenExchangeRequestConfig} from './models/token'; diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index d828b077e..95dd1f41d 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -22,11 +22,16 @@ export enum EmbeddedFlowType { export interface EmbeddedFlowExecuteResponse { flowId: string; - flowStatus: ApplicationNativeAuthenticationFlowStatus; + flowStatus: EmbeddedFlowStatus; type: EmbeddedFlowResponseType; data: EmbeddedSignUpFlowData; } +export enum EmbeddedFlowStatus { + Complete = 'COMPLETE', + Incomplete = 'INCOMPLETE', +} + export enum EmbeddedFlowResponseType { View = 'VIEW', } From a73fbe5fc875ffe454cd24dc06bd8b92b13646be Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 17 Jun 2025 22:20:56 +0530 Subject: [PATCH 10/96] feat(react): implement embedded sign-up flow with error handling and update client interfaces --- .../src/AsgardeoJavaScriptClient.ts | 54 +++---------------- .../src/api/executeEmbeddedSignUpFlow.ts | 39 +++++--------- packages/javascript/src/index.ts | 5 +- packages/javascript/src/models/client.ts | 18 +++++++ .../javascript/src/models/embedded-flow.ts | 6 +++ packages/react/src/AsgardeoReactClient.ts | 40 +++++++++++++- .../contexts/Asgardeo/AsgardeoProvider.tsx | 27 +++++++--- 7 files changed, 104 insertions(+), 85 deletions(-) diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index 5d46f7b1a..730a87c7d 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -16,9 +16,10 @@ * under the License. */ -import {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; +import {AsgardeoClient, SignInOptions, SignOutOptions, SignUpOptions} from './models/client'; import {User, UserProfile} from './models/user'; import {Config} from './models/config'; +import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse} from './models/embedded-flow'; /** * Base class for implementing Asgardeo clients. @@ -27,70 +28,27 @@ import {Config} from './models/config'; * @typeParam T - Configuration type that extends Config. */ abstract class AsgardeoJavaScriptClient implements AsgardeoClient { - /** - * Initializes the authentication client with provided configuration. - * - * @param config - SDK Client instance configuration options. - * @returns Promise resolving to boolean indicating success. - */ abstract initialize(config: T): Promise; - /** - * Gets user information from the session. - * - * @returns User object containing user details. - */ abstract getUser(): Promise; abstract getUserProfile(): Promise; - /** - * Checks if the client is currently loading. - * This can be used to determine if the client is in the process of initializing or fetching user data. - * - * @returns Boolean indicating if the client is loading. - */ abstract isLoading(): boolean; - /** - * Checks if a user is signed in. - * FIXME: Check if this should return a boolean or a Promise. - * - * @returns Promise resolving to boolean indicating sign-in status. - */ abstract isSignedIn(): Promise; - /** - * Initiates the sign-in process for the user. - * - * @param options - Optional sign-in options like additional parameters to be sent in the authorize request, etc. - * @returns Promise resolving the user upon successful sign in. - */ abstract signIn(options?: SignInOptions): Promise; - - /** - * Signs out the currently signed-in user. - * - * @param options - Optional sign-out options like additional parameters to be sent in the sign-out request, etc. - * @param afterSignOut - Callback function to be executed after sign-out is complete. - * @returns A promise that resolves to true if sign-out is successful - */ abstract signOut(options?: SignOutOptions, afterSignOut?: (redirectUrl: string) => void): Promise; - - /** - * Signs out the currently signed-in user with an optional session ID. - * - * @param options - Optional sign-out options like additional parameters to be sent in the sign-out request, etc. - * @param sessionId - Optional session ID to be used for sign-out. - * This can be useful in scenarios where multiple sessions are managed. - * @param afterSignOut - Callback function to be executed after sign-out is complete. - * @returns A promise that resolves to true if sign-out is successful - */ abstract signOut( options?: SignOutOptions, sessionId?: string, afterSignOut?: (redirectUrl: string) => void, ): Promise; + + abstract signUp(options?: SignUpOptions): Promise; + abstract signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; + abstract signUp(payload?: unknown): Promise | Promise; } export default AsgardeoJavaScriptClient; diff --git a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts index f04ed9e5c..22aabe8da 100644 --- a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts +++ b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts @@ -17,17 +17,11 @@ */ import AsgardeoAPIError from '../errors/AsgardeoAPIError'; -import {EmbeddedFlowType, EmbeddedFlowExecuteResponse} from '../models/embedded-flow'; - -/** - * Represents the embedded signup flow execution request payload. - */ -export interface EmbeddedSignUpFlowExecuteRequestPayload { - /** - * The type of flow to execute. - */ - flowType: EmbeddedFlowType; -} +import { + EmbeddedFlowType, + EmbeddedFlowExecuteResponse, + EmbeddedFlowExecuteRequestPayload, +} from '../models/embedded-flow'; /** * Request configuration for the embedded signup flow execution function. @@ -41,7 +35,7 @@ export interface EmbeddedSignUpFlowExecuteRequestConfig extends Partial /** * The embedded signup flow execution request payload. */ - payload: EmbeddedSignUpFlowExecuteRequestPayload; + payload?: EmbeddedFlowExecuteRequestPayload; } /** @@ -74,23 +68,13 @@ const executeEmbeddedSignUpFlow = async ({ payload, ...requestConfig }: EmbeddedSignUpFlowExecuteRequestConfig): Promise => { - if (!payload) { + if (!baseUrl || !url) { throw new AsgardeoAPIError( - 'Embedded SignUp flow payload is required', + 'Embedded SignUp flow execution failed: Base URL or URL is not provided.', 'javascript-executeEmbeddedSignUpFlow-ValidationError-001', 'javascript', 400, - 'If an embedded signup flow payload is not provided, the request cannot be constructed correctly.', - ); - } - - if (!payload.flowType || payload.flowType !== 'REGISTRATION') { - throw new AsgardeoAPIError( - 'Invalid flowType. Must be "REGISTRATION"', - 'javascript-executeEmbeddedSignUpFlow-ValidationError-002', - 'javascript', - 400, - 'The flowType must be set to "REGISTRATION" for embedded signup flows.', + 'At least one of the baseUrl or url must be provided to execute the embedded sign up flow.', ); } @@ -102,7 +86,10 @@ const executeEmbeddedSignUpFlow = async ({ Accept: 'application/json', ...customHeaders, }, - body: JSON.stringify(payload), + body: JSON.stringify({ + ...(payload ?? {}), + flowType: EmbeddedFlowType.Registration, + }), ...otherConfig, }); diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index d293297c9..cc15d5ec9 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -46,7 +46,7 @@ export { ApplicationNativeAuthenticationHandleResponse, ApplicationNativeAuthenticationAuthenticatorParamType, ApplicationNativeAuthenticationAuthenticatorPromptType, - ApplicationNativeAuthenticationAuthenticatorKnownIdPType + ApplicationNativeAuthenticationAuthenticatorKnownIdPType, } from './models/application-native-authentication'; export { EmbeddedFlowType, @@ -55,8 +55,9 @@ export { EmbeddedSignUpFlowData, EmbeddedFlowComponent, EmbeddedFlowComponentType, + EmbeddedFlowExecuteRequestPayload, } from './models/embedded-flow'; -export {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; +export {AsgardeoClient, SignInOptions, SignOutOptions, SignUpOptions} from './models/client'; export {BaseConfig, Config, Preferences, ThemePreferences, I18nPreferences, WithPreferences} from './models/config'; export {TokenResponse, IdTokenPayload, TokenExchangeRequestConfig} from './models/token'; export {Crypto, JWKInterface} from './models/crypto'; diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index bc7545b6c..f9fd45a35 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -16,10 +16,12 @@ * under the License. */ +import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse} from './embedded-flow'; import {User, UserProfile} from './user'; export type SignInOptions = Record; export type SignOutOptions = Record; +export type SignUpOptions = Record; /** * Interface defining the core functionality for Asgardeo authentication clients. @@ -78,6 +80,22 @@ export interface AsgardeoClient { */ signIn(options?: SignInOptions): Promise; + /** + * Initiates a redirection-based sign-up process for the user. + * + * @param options - Optional sign-up options like additional parameters to be sent in the sign-up request, etc. + * @returns Promise resolving to the user upon successful sign up. + */ + signUp(options?: SignUpOptions): Promise; + + /** + * Initiates an embedded (App-Native) sign-up flow for the user. + * + * @param payload - The payload containing the necessary information to execute the embedded sign-up flow. + * @returns A promise that resolves to an EmbeddedFlowExecuteResponse containing the flow execution details. + */ + signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; + /** * Signs out the currently signed-in user. * diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index 95dd1f41d..7bf982b27 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -20,6 +20,12 @@ export enum EmbeddedFlowType { Registration = 'REGISTRATION', } +export interface EmbeddedFlowExecuteRequestPayload { + actionId?: string; + flowType: EmbeddedFlowType; + inputs?: Record; +} + export interface EmbeddedFlowExecuteResponse { flowId: string; flowStatus: EmbeddedFlowStatus; diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 5dae4ecde..674162298 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -25,6 +25,11 @@ import { SignOutOptions, User, generateUserProfile, + EmbeddedFlowExecuteResponse, + SignUpOptions, + EmbeddedFlowExecuteRequestPayload, + AsgardeoRuntimeError, + executeEmbeddedSignUpFlow, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import {AsgardeoReactConfig} from './models/config'; @@ -71,11 +76,11 @@ class AsgardeoReactClient e const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); - + console.log('Raw Schemas:', JSON.stringify(schemas, null, 2)); const processedSchemas = flattenUserSchema(schemas); - + console.log('Processed Schemas:', JSON.stringify(processedSchemas, null, 2)); return { @@ -112,6 +117,37 @@ class AsgardeoReactClient e return Promise.resolve(String(response)); } + + override async signUp(options?: SignUpOptions): Promise; + override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; + override async signUp(...args: any[]): Promise { + if (args.length === 0) { + throw new AsgardeoRuntimeError( + 'No arguments provided for signUp method.', + 'react-AsgardeoReactClient-ValidationError-001', + 'react', + 'The signUp method requires at least one argument, either a SignUpOptions object or an EmbeddedFlowExecuteRequestPayload.', + ); + } + + const firstArg = args[0]; + + if (typeof firstArg === 'object' && 'flowType' in firstArg) { + const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; + + return executeEmbeddedSignUpFlow({ + baseUrl, + payload: firstArg as EmbeddedFlowExecuteRequestPayload, + }); + } else { + throw new AsgardeoRuntimeError( + 'Not implemented', + 'react-AsgardeoReactClient-ValidationError-002', + 'react', + 'The signUp method with SignUpOptions is not implemented in the React client.', + ); + } + } } export default AsgardeoReactClient; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 92197a40f..73961d609 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -16,7 +16,14 @@ * under the License. */ -import {SignInOptions, SignOutOptions, User, UserProfile} from '@asgardeo/browser'; +import { + AsgardeoRuntimeError, + EmbeddedFlowExecuteRequestPayload, + SignInOptions, + SignOutOptions, + User, + UserProfile, +} from '@asgardeo/browser'; import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, use} from 'react'; import AsgardeoReactClient from '../../AsgardeoReactClient'; import AsgardeoContext from './AsgardeoContext'; @@ -154,8 +161,17 @@ const AsgardeoProvider: FC> = ({ } }; - const signUp = (): void => { - throw new Error('Not implemented'); + const signUp = async (payload?: EmbeddedFlowExecuteRequestPayload): Promise => { + try { + await asgardeo.signUp(payload); + } catch (error) { + throw new AsgardeoRuntimeError( + `Error while signing up: ${error.message || error}`, + 'asgardeo-signUp-Error', + 'react', + 'An error occurred while trying to sign up.', + ); + } }; const signOut = async (options?: SignOutOptions, afterSignOut?: () => void): Promise => @@ -175,10 +191,7 @@ const AsgardeoProvider: FC> = ({ isSignedIn: isSignedInSync, signIn, signOut, - signUp: () => { - // TODO: Implement signUp functionality - throw new Error('Sign up functionality not implemented yet'); - }, + signUp, user, baseUrl, afterSignInUrl, From 33f1cf59452e1161da1686b8d547ad2b3b49c33a Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 18 Jun 2025 00:19:47 +0530 Subject: [PATCH 11/96] feat(react): add BaseSignUp and SignUp components for embedded sign-up flow, enhance AsgardeoProvider with response handling, and update routing in the sample app --- .gitignore | 4 +- .vscode/settings.json | 31 +- .../src/api/executeEmbeddedSignUpFlow.ts | 2 +- packages/javascript/src/index.ts | 1 + packages/react/src/AsgardeoReactClient.ts | 2 +- .../presentation/SignUp/BaseSignUp.tsx | 545 ++++++++++++++++++ .../components/presentation/SignUp/SignUp.tsx | 144 +++++ .../contexts/Asgardeo/AsgardeoProvider.tsx | 7 +- packages/react/src/index.ts | 6 + samples/teamspace-react/src/App.tsx | 9 + .../teamspace-react/src/pages/SignUpPage.tsx | 30 + 11 files changed, 755 insertions(+), 26 deletions(-) create mode 100644 packages/react/src/components/presentation/SignUp/BaseSignUp.tsx create mode 100644 packages/react/src/components/presentation/SignUp/SignUp.tsx create mode 100644 samples/teamspace-react/src/pages/SignUpPage.tsx diff --git a/.gitignore b/.gitignore index cf3dd4a4b..9b89913dd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ Thumbs.db # Environment files *.env -.env* \ No newline at end of file +.env* +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ed1b3fab..8c6c7b13e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,13 @@ { - "conventionalCommits.scopes": [ - "workspace", - "core", - "react", - "auth-components", - "sample-app", - "docs" - ], - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "css.validate": false, - "less.validate": false, - "scss.validate": false, - "stylelint.validate": [ - "css", - "scss" - ], - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true + "conventionalCommits.scopes": ["workspace", "core", "react", "auth-components", "sample-app", "docs"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "css.validate": false, + "less.validate": false, + "scss.validate": false, + "stylelint.validate": ["css", "scss"], + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "nxConsole.generateAiAgentRules": true } diff --git a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts index 22aabe8da..ced448de8 100644 --- a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts +++ b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts @@ -68,7 +68,7 @@ const executeEmbeddedSignUpFlow = async ({ payload, ...requestConfig }: EmbeddedSignUpFlowExecuteRequestConfig): Promise => { - if (!baseUrl || !url) { + if (!baseUrl && !url) { throw new AsgardeoAPIError( 'Embedded SignUp flow execution failed: Base URL or URL is not provided.', 'javascript-executeEmbeddedSignUpFlow-ValidationError-001', diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index cc15d5ec9..b7b70e981 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -50,6 +50,7 @@ export { } from './models/application-native-authentication'; export { EmbeddedFlowType, + EmbeddedFlowStatus, EmbeddedFlowExecuteResponse, EmbeddedFlowResponseType, EmbeddedSignUpFlowData, diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 674162298..12fc48d18 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -133,7 +133,7 @@ class AsgardeoReactClient e const firstArg = args[0]; if (typeof firstArg === 'object' && 'flowType' in firstArg) { - const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; + const baseUrl: string = (await this.asgardeo.getConfigData())?.baseUrl; return executeEmbeddedSignUpFlow({ baseUrl, diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx new file mode 100644 index 000000000..0c7f7b6ad --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -0,0 +1,545 @@ +/** + * 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 { + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, + EmbeddedFlowStatus, + EmbeddedFlowComponentType, + withVendorCSSClassPrefix, + AsgardeoAPIError, +} from '@asgardeo/browser'; +import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; +import {clsx} from 'clsx'; +import Card from '../../primitives/Card/Card'; +import Alert from '../../primitives/Alert/Alert'; +import Divider from '../../primitives/Divider/Divider'; +import Typography from '../../primitives/Typography/Typography'; +import Spinner from '../../primitives/Spinner/Spinner'; +import useTranslation from '../../../hooks/useTranslation'; +import {useForm, FormField} from '../../../hooks/useForm'; +import FlowProvider from '../../../contexts/Flow/FlowProvider'; +import useFlow from '../../../contexts/Flow/useFlow'; +import {createField} from '../../factories/FieldFactory'; +import Button from '../../primitives/Button/Button'; + +/** + * Props for the BaseSignUp component. + */ +export interface BaseSignUpProps { + /** + * Function to initialize sign-up flow. + * @returns Promise resolving to the initial sign-up response. + */ + onInitialize: (payload?: EmbeddedFlowExecuteRequestPayload) => Promise; + + /** + * Function to handle sign-up steps. + * @param payload - The sign-up payload. + * @returns Promise resolving to the sign-up response. + */ + onSubmit: (payload: EmbeddedFlowExecuteRequestPayload) => Promise; + + /** + * Callback function called when sign-up is successful. + * @param response - The sign-up response data returned upon successful completion. + */ + onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; + + /** + * Callback function called when sign-up fails. + * @param error - The error that occurred during sign-up. + */ + onError?: (error: Error) => void; + + /** + * Callback function called when sign-up flow status changes. + * @param response - The current sign-up response. + */ + onFlowChange?: (response: EmbeddedFlowExecuteResponse) => void; + + /** + * Custom CSS class name for the form container. + */ + className?: string; + + /** + * Custom CSS class name for form inputs. + */ + inputClassName?: string; + + /** + * Custom CSS class name for the submit button. + */ + buttonClassName?: string; + + /** + * Custom CSS class name for error messages. + */ + errorClassName?: string; + + /** + * Custom CSS class name for info messages. + */ + messageClassName?: string; + + /** + * Size variant for the component. + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Theme variant for the component. + */ + variant?: 'default' | 'outlined' | 'filled'; + + /** + * URL to redirect after successful sign-up. + */ + afterSignUpUrl?: string; +} + +/** + * Base SignUp component that provides embedded sign-up flow. + * This component handles both the presentation layer and sign-up flow logic. + * It accepts API functions as props to maintain framework independence. + * + * @example + * ```tsx + * import { BaseSignUp } from '@asgardeo/react'; + * + * const MySignUp = () => { + * return ( + * { + * // Your API call to initialize sign-up + * return await initializeSignUp(payload); + * }} + * onSubmit={async (payload) => { + * // Your API call to handle sign-up + * return await handleSignUp(payload); + * }} + * onSuccess={(response) => { + * console.log('Success:', response); + * }} + * onError={(error) => { + * console.error('Error:', error); + * }} + * className="max-w-md mx-auto" + * /> + * ); + * }; + * ``` + */ +const BaseSignUp: FC = props => { + return ( + + + + ); +}; + +/** + * Internal component that consumes FlowContext and renders the sign-up UI. + */ +const BaseSignUpContent: FC = ({ + afterSignUpUrl, + onInitialize, + onSubmit, + onSuccess, + onError, + onFlowChange, + className = '', + inputClassName = '', + buttonClassName = '', + errorClassName = '', + messageClassName = '', + size = 'medium', + variant = 'default', +}) => { + const {t} = useTranslation(); + const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); + + const [isLoading, setIsLoading] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); + const [currentFlow, setCurrentFlow] = useState(null); + const [error, setError] = useState(null); + const [formData, setFormData] = useState>({}); + + // Ref to track if initialization has been attempted to prevent multiple calls + const initializationAttemptedRef = useRef(false); + + /** + * Extract form fields from flow components + */ + const extractFormFields = useCallback( + (components: any[]): FormField[] => { + const fields: FormField[] = []; + + const processComponents = (comps: any[]) => { + comps.forEach(component => { + if (component.type === EmbeddedFlowComponentType.Input) { + const config = component.config || {}; + fields.push({ + name: config.name || component.id, + required: config.required || false, + initialValue: config.defaultValue || '', + validator: (value: string) => { + if (config.required && (!value || value.trim() === '')) { + return t('field.required'); + } + // Add email validation if it's an email field + if (config.type === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + return t('field.email.invalid'); + } + // Add password strength validation if it's a password field + if (config.type === 'password' && value && value.length < 8) { + return t('field.password.weak'); + } + return null; + }, + }); + } + + if (component.components && Array.isArray(component.components)) { + processComponents(component.components); + } + }); + }; + + processComponents(components); + return fields; + }, + [t], + ); + + const formFields = currentFlow?.data?.components ? extractFormFields(currentFlow.data.components) : []; + + const form = useForm>({ + initialValues: {}, + fields: formFields, + validateOnBlur: true, + validateOnChange: false, + requiredMessage: t('field.required'), + }); + + const { + values: formValues, + touched: touchedFields, + errors: formErrors, + isValid: isFormValid, + setValue: setFormValue, + setTouched: setFormTouched, + clearErrors: clearFormErrors, + validateField: validateFormField, + validateForm, + touchAllFields, + reset: resetForm, + } = form; + + /** + * Setup form fields based on the current flow. + */ + const setupFormFields = useCallback( + (flowResponse: EmbeddedFlowExecuteResponse) => { + const fields = extractFormFields(flowResponse.data?.components || []); + const initialValues: Record = {}; + + fields.forEach(field => { + initialValues[field.name] = field.initialValue || ''; + }); + + // Reset form with new values + resetForm(); + + // Set initial values for all fields + Object.keys(initialValues).forEach(key => { + setFormValue(key, initialValues[key]); + }); + }, + [extractFormFields, resetForm, setFormValue], + ); + + /** + * Handle form submission. + */ + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (!currentFlow) { + return; + } + + // Mark all fields as touched before validation + touchAllFields(); + + const validation = validateForm(); + if (!validation.isValid) { + return; + } + + setIsLoading(true); + setError(null); + + try { + const payload: EmbeddedFlowExecuteRequestPayload = { + flowType: currentFlow.data ? (currentFlow.data as any) : undefined, + inputs: formValues, + actionId: 'submit', // This might need to be dynamic based on the flow + }; + + const response = await onSubmit(payload); + onFlowChange?.(response); + + if (response.flowStatus === EmbeddedFlowStatus.Complete) { + onSuccess?.(response); + + // Handle redirect if afterSignUpUrl is provided + if (afterSignUpUrl) { + window.location.href = afterSignUpUrl; + } + return; + } + + if (response.flowStatus === EmbeddedFlowStatus.Incomplete) { + // Continue with the next step + setCurrentFlow(response); + setupFormFields(response); + } + } catch (err) { + const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.up.flow.failure'); + setError(errorMessage); + onError?.(err as Error); + } finally { + setIsLoading(false); + } + }; + + /** + * Handle input value changes. + */ + const handleInputChange = (name: string, value: string) => { + setFormValue(name, value); + setFormTouched(name, true); + }; + + /** + * Render form components based on flow data + */ + const renderComponents = useCallback( + (components: any[]): ReactElement[] => { + return components + .map((component, index) => { + const config = component.config || {}; + + switch (component.type) { + case EmbeddedFlowComponentType.Typography: + return ( + + {config.text || config.content} + + ); + + case EmbeddedFlowComponentType.Input: + const fieldName = config.name || component.id; + return createField({ + type: config.type || 'text', + name: fieldName, + label: config.label || fieldName, + placeholder: config.placeholder, + required: config.required || false, + value: formValues[fieldName] || '', + error: touchedFields[fieldName] ? formErrors[fieldName] : undefined, + onChange: (value: string) => handleInputChange(fieldName, value), + className: inputClasses, + }); + + case EmbeddedFlowComponentType.Button: + return ( + + ); + + case EmbeddedFlowComponentType.Form: + return ( +
{component.components && renderComponents(component.components)}
+ ); + + default: + if (component.components && Array.isArray(component.components)) { + return
{renderComponents(component.components)}
; + } + return null; + } + }) + .filter(Boolean) as ReactElement[]; + }, + [formValues, touchedFields, formErrors, isFormValid, isLoading, size], + ); + + // Generate CSS classes + const containerClasses = clsx( + [ + withVendorCSSClassPrefix('signup'), + withVendorCSSClassPrefix(`signup--${size}`), + withVendorCSSClassPrefix(`signup--${variant}`), + ], + className, + ); + + const inputClasses = clsx( + [ + withVendorCSSClassPrefix('signup__input'), + size === 'small' && withVendorCSSClassPrefix('signup__input--small'), + size === 'large' && withVendorCSSClassPrefix('signup__input--large'), + ], + inputClassName, + ); + + const buttonClasses = clsx( + [ + withVendorCSSClassPrefix('signup__button'), + size === 'small' && withVendorCSSClassPrefix('signup__button--small'), + size === 'large' && withVendorCSSClassPrefix('signup__button--large'), + variant === 'outlined' && withVendorCSSClassPrefix('signup__button--outlined'), + variant === 'filled' && withVendorCSSClassPrefix('signup__button--filled'), + ], + buttonClassName, + ); + + const errorClasses = clsx([withVendorCSSClassPrefix('signup__error')], errorClassName); + + const messageClasses = clsx([withVendorCSSClassPrefix('signup__messages')], messageClassName); + + // Initialize the flow on component mount + useEffect(() => { + if (!isInitialized && !initializationAttemptedRef.current) { + initializationAttemptedRef.current = true; + + // Inline initialization to avoid dependency issues + const performInitialization = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await onInitialize(); + + setCurrentFlow(response); + setIsInitialized(true); + onFlowChange?.(response); + + debugger; + + if (response.flowStatus === EmbeddedFlowStatus.Complete) { + onSuccess?.(response); + + if (afterSignUpUrl) { + window.location.href = afterSignUpUrl; + } + return; + } + + if (response.flowStatus === EmbeddedFlowStatus.Incomplete) { + setupFormFields(response); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : t('errors.sign.up.flow.initialization.failure'); + setError(errorMessage); + onError?.(err as Error); + } finally { + setIsLoading(false); + } + }; + + performInitialization(); + } + }, [isInitialized, onInitialize, onSuccess, onError, onFlowChange, setupFormFields, afterSignUpUrl, t]); + + if (!isInitialized && isLoading) { + return ( + +
+ +
+
+ ); + } + + if (!currentFlow) { + return ( + + + {error || t('errors.sign.up.flow.initialization.failure')} + + + ); + } + + return ( + + {(flowTitle || flowSubtitle) && ( +
+ {flowTitle && ( + + {flowTitle} + + )} + {flowSubtitle && ( + + {flowSubtitle} + + )} +
+ )} + + {error && ( + + {error} + + )} + + {flowMessages && flowMessages.length > 0 && ( +
+ {flowMessages.map((message, index) => ( + + {message.message} + + ))} +
+ )} + +
+ {currentFlow.data?.components && renderComponents(currentFlow.data.components)} +
+
+ ); +}; + +export default BaseSignUp; diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx new file mode 100644 index 000000000..b23067629 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx @@ -0,0 +1,144 @@ +/** + * 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 {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse, EmbeddedFlowType} from '@asgardeo/browser'; +import {FC} from 'react'; +import BaseSignUp from './BaseSignUp'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the SignUp component. + */ +export interface SignUpProps { + /** + * Additional CSS class names for customization. + */ + className?: string; + + /** + * Size variant for the component. + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Theme variant for the component. + */ + variant?: 'default' | 'outlined' | 'filled'; + + /** + * URL to redirect after successful sign-up. + */ + afterSignUpUrl?: string; + + /** + * Callback function called when sign-up is successful. + * @param response - The sign-up response data returned upon successful completion. + */ + onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; + + /** + * Callback function called when sign-up fails. + * @param error - The error that occurred during sign-up. + */ + onError?: (error: Error) => void; +} + +/** + * A styled SignUp component that provides embedded sign-up flow with pre-built styling. + * This component handles the API calls for sign-up and delegates UI logic to BaseSignUp. + * + * @example + * ```tsx + * import { SignUp } from '@asgardeo/react'; + * + * const App = () => { + * return ( + * { + * console.log('Sign-up successful:', response); + * // Handle successful sign-up (e.g., redirect, show confirmation) + * }} + * onError={(error) => { + * console.error('Sign-up failed:', error); + * }} + * size="medium" + * variant="outlined" + * afterSignUpUrl="/welcome" + * /> + * ); + * }; + * ``` + */ +const SignUp: FC = ({ + className, + size = 'medium', + variant = 'default', + afterSignUpUrl, + onSuccess, + onError, +}) => { + const {signUp} = useAsgardeo(); + + /** + * Initialize the sign-up flow. + */ + const handleInitialize = async ( + payload?: EmbeddedFlowExecuteRequestPayload, + ): Promise => { + return await signUp( + payload || { + flowType: EmbeddedFlowType.Registration, + }, + ); + }; + + /** + * Handle sign-up steps. + */ + const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise => { + return await signUp(payload); + }; + + /** + * Handle successful sign-up and redirect. + */ + const handleSuccess = (response: EmbeddedFlowExecuteResponse) => { + // Call the provided onSuccess callback first + onSuccess?.(response); + + // Handle redirect if afterSignUpUrl is provided + if (afterSignUpUrl) { + window.location.href = afterSignUpUrl; + } + }; + + return ( + + ); +}; + +export default SignUp; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 73961d609..dfc1c2329 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -19,6 +19,7 @@ import { AsgardeoRuntimeError, EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, SignInOptions, SignOutOptions, User, @@ -161,9 +162,9 @@ const AsgardeoProvider: FC> = ({ } }; - const signUp = async (payload?: EmbeddedFlowExecuteRequestPayload): Promise => { + const signUp = async (payload?: EmbeddedFlowExecuteRequestPayload): Promise => { try { - await asgardeo.signUp(payload); + return await asgardeo.signUp(payload); } catch (error) { throw new AsgardeoRuntimeError( `Error while signing up: ${error.message || error}`, @@ -187,7 +188,7 @@ const AsgardeoProvider: FC> = ({ return ( } /> + + + + } + /> {/* Dashboard/Protected Routes */} +
+ {/* Header */} + + + + + +
+
+ ); +} From 53e5d76ba62403309e1750b2c715fcb44d24a555 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 18 Jun 2025 02:27:45 +0530 Subject: [PATCH 12/96] feat(react): implement SignIn component for native authentication flow, enhance AsgardeoProvider with sign-in logic, and update Dashboard to display user information --- .../src/components/header/UserDropdown.tsx | 2 +- samples/teamspace-react/src/pages/Dashboard.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/samples/teamspace-react/src/components/header/UserDropdown.tsx b/samples/teamspace-react/src/components/header/UserDropdown.tsx index 2aecf33d6..50b7a314a 100644 --- a/samples/teamspace-react/src/components/header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/header/UserDropdown.tsx @@ -10,7 +10,7 @@ export type UserDropdownProps = { mode?: 'custom' | 'default'; }; -export default function UserDropdown({mode = 'custom'}: UserDropdownProps) { +export default function UserDropdown({mode = 'default'}: UserDropdownProps) { const [showUserDropdown, setShowUserDropdown] = useState(false); const [showUserProfile, setShowUserProfile] = useState(false); const userDropdownRef = useRef(null); diff --git a/samples/teamspace-react/src/pages/Dashboard.tsx b/samples/teamspace-react/src/pages/Dashboard.tsx index 5104232e0..91d9ad9fa 100644 --- a/samples/teamspace-react/src/pages/Dashboard.tsx +++ b/samples/teamspace-react/src/pages/Dashboard.tsx @@ -1,8 +1,9 @@ +import {User} from '@asgardeo/react'; import {useApp} from '../App'; import {Users, MessageSquare, Calendar, FileText, TrendingUp, Clock, CheckCircle2, AlertCircle} from 'lucide-react'; export default function Dashboard() { - const {user, currentOrg} = useApp(); + const {currentOrg} = useApp(); const stats = [ { @@ -94,7 +95,18 @@ export default function Dashboard() {
{/* Header */}
-

Welcome back, {user?.name}!

+

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

+

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

From ecdc6d68b474b30c7c56e1f140e6cc189b5dee92 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 18 Jun 2025 02:57:16 +0530 Subject: [PATCH 13/96] chore: add `afterSignOutUrl` --- samples/teamspace-react/src/main.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx index c7be31c44..9e6dd5214 100644 --- a/samples/teamspace-react/src/main.tsx +++ b/samples/teamspace-react/src/main.tsx @@ -9,6 +9,7 @@ createRoot(document.getElementById('root')!).render( Date: Wed, 18 Jun 2025 03:00:10 +0530 Subject: [PATCH 14/96] feat(react): add afterSignOutUrl to configuration and update related components for sign-out handling --- packages/javascript/src/models/config.ts | 36 ++++++++++++------- packages/react/src/AsgardeoReactClient.ts | 21 +++++------ .../UserDropdown/UserDropdown.tsx | 30 ++++++++-------- .../contexts/Asgardeo/AsgardeoProvider.tsx | 10 ++++-- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index cbf3652da..c24106148 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -16,9 +16,9 @@ * under the License. */ -import {ThemeConfig, ThemeMode} from '../theme/types'; import {I18nBundle} from './i18n'; import {RecursivePartial} from './utility-types'; +import {ThemeConfig, ThemeMode} from '../theme/types'; export interface BaseConfig extends WithPreferences { /** @@ -32,6 +32,18 @@ export interface BaseConfig extends WithPreferences { */ afterSignInUrl?: string | undefined; + /** + * Optional URL where the authorization server should redirect after sign out. + * This must match one of the allowed post logout redirect URIs configured in your IdP + * and is used to redirect the user after they have signed out. + * If not provided, the framework layer will use the default sign out URL based on the + * + * @example + * For development: "http://localhost:3000/api/auth/signout" + * For production: "https://your-app.com/api/auth/signout" + */ + afterSignOutUrl?: string | undefined; + /** * The base URL of the Asgardeo identity server. * Example: "https://api.asgardeo.io/t/{org_name}" @@ -88,30 +100,30 @@ export interface ThemePreferences { export interface I18nPreferences { /** - * The language to use for translations. - * Defaults to the browser's default language. + * Custom translations to override default ones. */ - language?: string; + bundles?: { + [key: string]: I18nBundle; + }; /** * The fallback language to use if translations are not available in the specified language. * Defaults to 'en-US'. */ fallbackLanguage?: string; /** - * Custom translations to override default ones. + * The language to use for translations. + * Defaults to the browser's default language. */ - bundles?: { - [key: string]: I18nBundle; - }; + language?: string; } export interface Preferences { - /** - * Theme preferences for the Asgardeo UI components - */ - theme?: ThemePreferences; /** * Internationalization preferences for the Asgardeo UI components */ i18n?: I18nPreferences; + /** + * Theme preferences for the Asgardeo UI components + */ + theme?: ThemePreferences; } diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 12fc48d18..4b584dfc5 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -32,9 +32,9 @@ import { executeEmbeddedSignUpFlow, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; -import {AsgardeoReactConfig} from './models/config'; import getMeProfile from './api/scim2/getMeProfile'; import getSchemas from './api/scim2/getSchemas'; +import {AsgardeoReactConfig} from './models/config'; /** * Client for mplementing Asgardeo in React applications. @@ -56,10 +56,12 @@ class AsgardeoReactClient e const scopes: string[] = Array.isArray(config.scopes) ? config.scopes : config.scopes.split(' '); return this.asgardeo.init({ + afterSignInUrl: config.afterSignInUrl, + afterSignOutUrl: config.afterSignOutUrl, baseUrl: config.baseUrl, clientId: config.clientId, - afterSignInUrl: config.afterSignInUrl, scopes: [...scopes, 'internal_login'], + ...config, }); } @@ -72,7 +74,7 @@ class AsgardeoReactClient e } async getUserProfile(): Promise { - const baseUrl: string = (await this.asgardeo.getConfigData()).baseUrl; + const {baseUrl} = await this.asgardeo.getConfigData(); const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); @@ -139,14 +141,13 @@ class AsgardeoReactClient e baseUrl, payload: firstArg as EmbeddedFlowExecuteRequestPayload, }); - } else { - throw new AsgardeoRuntimeError( - 'Not implemented', - 'react-AsgardeoReactClient-ValidationError-002', - 'react', - 'The signUp method with SignUpOptions is not implemented in the React client.', - ); } + throw new AsgardeoRuntimeError( + 'Not implemented', + 'react-AsgardeoReactClient-ValidationError-002', + 'react', + 'The signUp method with SignUpOptions is not implemented in the React client.', + ); } } diff --git a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx index 1fcd7da50..3f8f898d2 100644 --- a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx @@ -17,49 +17,49 @@ */ import {FC, ReactElement, ReactNode, useState} from 'react'; -import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import BaseUserDropdown, {BaseUserDropdownProps} from './BaseUserDropdown'; -import UserProfile from '../UserProfile/UserProfile'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; +import UserProfile from '../UserProfile/UserProfile'; /** * Render props data passed to the children function */ export interface UserDropdownRenderProps { - /** The authenticated user object */ - user: any; + /** Function to close the profile dialog */ + closeProfile: () => void; /** Whether user data is currently loading */ isLoading: boolean; + /** Whether the profile dialog is currently open */ + isProfileOpen: boolean; /** Function to open the user profile dialog */ openProfile: () => void; /** Function to sign out the user */ signOut: () => void; - /** Whether the profile dialog is currently open */ - isProfileOpen: boolean; - /** Function to close the profile dialog */ - closeProfile: () => void; + /** The authenticated user object */ + user: any; } /** * Props for the UserDropdown component. * Extends BaseUserDropdownProps but excludes user, onManageProfile, and onSignOut since they're handled internally */ -export type UserDropdownProps = Omit & { +export type UserDropdownProps = Omit & { /** * Render prop function that receives user state and actions. * When provided, this completely replaces the default dropdown rendering. */ children?: (props: UserDropdownRenderProps) => ReactNode; - /** - * Custom render function for the trigger button. - * When provided, this replaces just the trigger button while keeping the dropdown. - */ - renderTrigger?: (props: UserDropdownRenderProps) => ReactNode; /** * Custom render function for the dropdown content. * When provided, this replaces just the dropdown content while keeping the trigger. */ renderDropdown?: (props: UserDropdownRenderProps) => ReactNode; + /** + * Custom render function for the trigger button. + * When provided, this replaces just the trigger button while keeping the dropdown. + */ + renderTrigger?: (props: UserDropdownRenderProps) => ReactNode; }; /** @@ -112,6 +112,7 @@ const UserDropdown: FC = ({ children, renderTrigger, renderDropdown, + onSignOut, ...rest }: UserDropdownProps): ReactElement => { const {user, isLoading, signOut} = useAsgardeo(); @@ -123,6 +124,7 @@ const UserDropdown: FC = ({ const handleSignOut = () => { signOut(); + onSignOut(); }; const closeProfile = () => { diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index dfc1c2329..c9166d817 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -26,13 +26,13 @@ import { UserProfile, } from '@asgardeo/browser'; import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, use} from 'react'; -import AsgardeoReactClient from '../../AsgardeoReactClient'; import AsgardeoContext from './AsgardeoContext'; +import AsgardeoReactClient from '../../AsgardeoReactClient'; import useBrowserUrl from '../../hooks/useBrowserUrl'; import {AsgardeoReactConfig} from '../../models/config'; -import ThemeProvider from '../Theme/ThemeProvider'; -import I18nProvider from '../I18n/I18nProvider'; import FlowProvider from '../Flow/FlowProvider'; +import I18nProvider from '../I18n/I18nProvider'; +import ThemeProvider from '../Theme/ThemeProvider'; import UserProvider from '../User/UserProvider'; /** @@ -42,11 +42,13 @@ export type AsgardeoProviderProps = AsgardeoReactConfig; const AsgardeoProvider: FC> = ({ afterSignInUrl = window.location.origin, + afterSignOutUrl = window.location.origin, baseUrl, clientId, children, scopes, preferences, + ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(), []); @@ -61,9 +63,11 @@ const AsgardeoProvider: FC> = ({ (async (): Promise => { await asgardeo.initialize({ afterSignInUrl, + afterSignOutUrl, baseUrl, clientId, scopes, + ...rest, }); })(); }, []); From cf2d066cefcb2d74dedd63f65fbb8130d25c1e90 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 18 Jun 2025 03:04:45 +0530 Subject: [PATCH 15/96] feat(react): update scopes in AsgardeoProvider for enhanced user permissions --- samples/teamspace-react/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx index 9e6dd5214..e28444a79 100644 --- a/samples/teamspace-react/src/main.tsx +++ b/samples/teamspace-react/src/main.tsx @@ -11,7 +11,7 @@ createRoot(document.getElementById('root')!).render( afterSignInUrl={import.meta.env.VITE_ASGARDEO_AFTER_SIGN_IN_URL} afterSignOutUrl={import.meta.env.VITE_ASGARDEO_AFTER_SIGN_OUT_URL} clientId={import.meta.env.VITE_ASGARDEO_CLIENT_ID} - scopes={['openid', 'address', 'email', 'profile']} + scopes={['openid', 'address', 'email', 'profile', 'user:email', 'read:user']} preferences={{ theme: { mode: 'light', From ed2eb72a098548a0dcbeb526b443d9d9afd384f2 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 18 Jun 2025 03:30:43 +0530 Subject: [PATCH 16/96] feat(react): enhance user profile handling and improve attribute mapping in BaseUserDropdown and BaseUserProfile components --- packages/react/src/AsgardeoReactClient.ts | 52 +++++---- .../UserDropdown/BaseUserDropdown.tsx | 92 ++++++++-------- .../UserProfile/BaseUserProfile.tsx | 101 +++++++++--------- .../teamspace-react/src/pages/Dashboard.tsx | 5 +- 4 files changed, 131 insertions(+), 119 deletions(-) diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 4b584dfc5..354f0688b 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -60,36 +60,44 @@ class AsgardeoReactClient e afterSignOutUrl: config.afterSignOutUrl, baseUrl: config.baseUrl, clientId: config.clientId, - scopes: [...scopes, 'internal_login'], ...config, + scopes: [...scopes, 'internal_login'], }); } override async getUser(): Promise { - const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; - const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); - const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); - - return generateUserProfile(profile, flattenUserSchema(schemas)); + try { + const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; + const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + + return generateUserProfile(profile, flattenUserSchema(schemas)); + } catch (error) { + return this.asgardeo.getDecodedIdToken(); + } } async getUserProfile(): Promise { - const {baseUrl} = await this.asgardeo.getConfigData(); - - const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); - const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); - - console.log('Raw Schemas:', JSON.stringify(schemas, null, 2)); - - const processedSchemas = flattenUserSchema(schemas); - - console.log('Processed Schemas:', JSON.stringify(processedSchemas, null, 2)); - - return { - schemas: processedSchemas, - flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), - profile, - }; + try { + const {baseUrl} = await this.asgardeo.getConfigData(); + + const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + + const processedSchemas = flattenUserSchema(schemas); + + return { + schemas: processedSchemas, + flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), + profile, + }; + } catch (error) { + return { + schemas: [], + flattenedProfile: await this.asgardeo.getDecodedIdToken(), + profile: await this.asgardeo.getDecodedIdToken(), + }; + } } override isLoading(): boolean { diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 024ba143a..876281e6d 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -16,9 +16,7 @@ * under the License. */ -import {CSSProperties, FC, ReactElement, ReactNode, useMemo, useRef, useState} from 'react'; import {withVendorCSSClassPrefix} from '@asgardeo/browser'; -import clsx from 'clsx'; import { useFloating, autoUpdate, @@ -32,13 +30,15 @@ import { FloatingFocusManager, FloatingPortal, } from '@floating-ui/react'; +import clsx from 'clsx'; +import {CSSProperties, FC, ReactElement, ReactNode, useMemo, useRef, useState} from 'react'; import useTheme from '../../../contexts/Theme/useTheme'; +import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; -import Typography from '../../primitives/Typography/Typography'; -import User from '../../primitives/Icons/User'; import LogOut from '../../primitives/Icons/LogOut'; -import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; +import User from '../../primitives/Icons/User'; +import Typography from '../../primitives/Typography/Typography'; const useStyles = () => { const {theme, colorScheme} = useTheme(); @@ -48,8 +48,8 @@ const useStyles = () => { trigger: { display: 'inline-flex', alignItems: 'center', - gap: theme.spacing.unit + 'px', - padding: theme.spacing.unit * 0.5 + 'px', + gap: `${theme.spacing.unit}px`, + padding: `${theme.spacing.unit * 0.5}px`, border: 'none', background: 'none', cursor: 'pointer', @@ -87,7 +87,7 @@ const useStyles = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, width: '100%', color: theme.colors.text.primary, @@ -104,7 +104,7 @@ const useStyles = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, width: '100%', color: theme.colors.text.primary, @@ -124,14 +124,14 @@ const useStyles = () => { dropdownHeader: { display: 'flex', alignItems: 'center', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, padding: `${theme.spacing.unit * 1.5}px`, borderBottom: `1px solid ${theme.colors.border}`, } as CSSProperties, headerInfo: { display: 'flex', flexDirection: 'column', - gap: theme.spacing.unit / 4 + 'px', + gap: `${theme.spacing.unit / 4}px`, flex: 1, minWidth: 0, overflow: 'hidden', @@ -160,7 +160,7 @@ const useStyles = () => { alignItems: 'center', justifyContent: 'center', minHeight: '80px', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, } as CSSProperties, loadingText: { color: theme.colors.text.secondary, @@ -172,68 +172,68 @@ const useStyles = () => { }; export interface MenuItem { - label: ReactNode; + href?: string; icon?: ReactNode; + label: ReactNode; onClick?: () => void; - href?: string; } export interface BaseUserDropdownProps { /** - * Optional element to render when no user is signed in. + * Mapping of component attribute names to identity provider field names. + * Allows customizing which user profile fields should be used for each attribute. */ - fallback?: ReactElement; + attributeMapping?: { + [key: string]: string | string[] | undefined; + firstName?: string | string[]; + lastName?: string | string[]; + picture?: string | string[]; + username?: string | string[]; + }; + /** + * Optional size for the avatar + */ + avatarSize?: number; /** * Optional className for the dropdown container. */ className?: string; /** - * The user object containing profile information + * Optional element to render when no user is signed in. */ - user: any; + fallback?: ReactElement; /** * Whether the user data is currently loading */ isLoading?: boolean; - /** - * The HTML element ID where the portal should be mounted - */ - portalId?: string; /** * Menu items to display in the dropdown */ menuItems?: MenuItem[]; /** - * Show user's display name next to avatar in the trigger button + * Callback function for "Manage Profile" action */ - showTriggerLabel?: boolean; + onManageProfile?: () => void; /** - * Show dropdown header with user information + * Callback function for "Sign Out" action */ - showDropdownHeader?: boolean; + onSignOut?: () => void; /** - * Optional size for the avatar + * The HTML element ID where the portal should be mounted */ - avatarSize?: number; + portalId?: string; /** - * Callback function for "Manage Profile" action + * Show dropdown header with user information */ - onManageProfile?: () => void; + showDropdownHeader?: boolean; /** - * Callback function for "Sign Out" action + * Show user's display name next to avatar in the trigger button */ - onSignOut?: () => void; + showTriggerLabel?: boolean; /** - * Mapping of component attribute names to identity provider field names. - * Allows customizing which user profile fields should be used for each attribute. + * The user object containing profile information */ - attributeMapping?: { - picture?: string | string[]; - firstName?: string | string[]; - lastName?: string | string[]; - username?: string | string[]; - [key: string]: string | string[] | undefined; - }; + user: any; } /** @@ -276,11 +276,11 @@ export const BaseUserDropdown: FC = ({ const {getReferenceProps, getFloatingProps} = useInteractions([click, dismiss, role]); const defaultAttributeMappings = { - picture: ['profile', 'profileUrl'], - firstName: 'name.givenName', - lastName: 'name.familyName', - email: 'emails', - username: 'userName', + picture: ['profile', 'profileUrl', 'picture', 'URL'], + firstName: ['name.givenName', 'given_name'], + lastName: ['name.familyName', 'family_name'], + email: ['emails'], + username: ['userName', 'username', 'user_name'], }; const mergedMappings = {...defaultAttributeMappings, ...attributeMapping}; diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index e31b51a8d..af8e73927 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -16,21 +16,21 @@ * under the License. */ -import {CSSProperties, FC, ReactElement, useMemo, useState, useCallback, useRef} from 'react'; -import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; -import {Avatar} from '../../primitives/Avatar/Avatar'; -import TextField from '../../primitives/TextField/TextField'; -import DatePicker from '../../primitives/DatePicker/DatePicker'; -import Checkbox from '../../primitives/Checkbox/Checkbox'; -import Button from '../../primitives/Button/Button'; -import useTheme from '../../../contexts/Theme/useTheme'; import {User, withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; +import {CSSProperties, FC, ReactElement, useMemo, useState, useCallback, useRef} from 'react'; +import useTheme from '../../../contexts/Theme/useTheme'; import getMappedUserProfileValue from '../../../utils/getMappedUserProfileValue'; +import {Avatar} from '../../primitives/Avatar/Avatar'; +import Button from '../../primitives/Button/Button'; +import Checkbox from '../../primitives/Checkbox/Checkbox'; +import DatePicker from '../../primitives/DatePicker/DatePicker'; +import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; +import TextField from '../../primitives/TextField/TextField'; interface ExtendedFlatSchema { - schemaId?: string; path?: string; + schemaId?: string; } interface Schema extends ExtendedFlatSchema { @@ -43,34 +43,34 @@ interface Schema extends ExtendedFlatSchema { name?: string; required?: boolean; returned?: string; + subAttributes?: Schema[]; type?: string; uniqueness?: string; value?: any; - subAttributes?: Schema[]; } export interface BaseUserProfileProps { - fallback?: ReactElement; - className?: string; - cardLayout?: boolean; - profile?: User; - flattenedProfile?: User; - schemas?: Schema[]; - mode?: 'inline' | 'popup'; - title?: string; attributeMapping?: { - picture?: string | string[]; + [key: string]: string | string[] | undefined; firstName?: string | string[]; lastName?: string | string[]; + picture?: string | string[]; username?: string | string[]; - [key: string]: string | string[] | undefined; }; + cancelButtonText?: string; + cardLayout?: boolean; + className?: string; editable?: boolean; + fallback?: ReactElement; + flattenedProfile?: User; + mode?: 'inline' | 'popup'; onChange?: (field: string, value: any) => void; onSubmit?: (data: any) => void; - saveButtonText?: string; - cancelButtonText?: string; onUpdate?: (payload: any) => Promise; + profile?: User; + saveButtonText?: string; + schemas?: Schema[]; + title?: string; } const BaseUserProfile: FC = ({ @@ -142,7 +142,7 @@ const BaseUserProfile: FC = ({ (schema: Schema) => { if (!onUpdate || !schema.name) return; - let payload = {}; + const payload = {}; const fieldName = schema.name; const fieldValue = editedUser && fieldName && editedUser[fieldName] !== undefined @@ -172,12 +172,14 @@ const BaseUserProfile: FC = ({ [flattenedProfile, profile, toggleFieldEdit], ); - const formatLabel = useCallback((key: string): string => { - return key - .split(/(?=[A-Z])|_/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); - }, []); + const formatLabel = useCallback( + (key: string): string => + key + .split(/(?=[A-Z])|_/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '), + [], + ); const styles = useStyles(); const buttonStyle = useMemo( @@ -212,10 +214,11 @@ const BaseUserProfile: FC = ({ ); const defaultAttributeMappings = { - picture: 'profileUrl', - firstName: 'name.givenName', - lastName: 'name.familyName', - username: 'userName', + picture: ['profile', 'profileUrl', 'picture', 'URL'], + firstName: ['name.givenName', 'given_name'], + lastName: ['name.familyName', 'family_name'], + email: ['emails'], + username: ['userName', 'username', 'user_name'], }; const mergedMappings = {...defaultAttributeMappings, ...attributeMapping}; @@ -278,7 +281,7 @@ const BaseUserProfile: FC = ({ const commonProps = { label: undefined, // Don't show label in field, we render it outside - required: required, + required, value: fieldValue, onChange: (e: any) => onEditValue(e.target ? e.target.value : e), style: { @@ -353,12 +356,12 @@ const BaseUserProfile: FC = ({ ...styles.field, display: 'flex', alignItems: 'center', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, }; return (
-
+
{renderSchemaField(schema, isFieldEditing, value => { const tempEditedUser = {...editedUser}; tempEditedUser[schema.name!] = value; @@ -369,9 +372,9 @@ const BaseUserProfile: FC = ({
{isFieldEditing ? ( @@ -396,7 +399,7 @@ const BaseUserProfile: FC = ({ onClick={() => toggleFieldEdit(schema.name!)} title="Edit" style={{ - padding: theme.spacing.unit / 2 + 'px', + padding: `${theme.spacing.unit / 2}px`, }} > @@ -416,10 +419,10 @@ const BaseUserProfile: FC = ({ {Object.entries(data).map(([key, value]) => ( - + {formatLabel(key)}: - + {typeof value === 'object' ? : String(value)} @@ -488,7 +491,7 @@ const BaseUserProfile: FC = ({ const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; const schemaWithValue = { ...schema, - value: value, + value, }; return
{renderUserInfo(schemaWithValue)}
; })} @@ -516,7 +519,7 @@ const useStyles = () => { return useMemo( () => ({ root: { - padding: theme.spacing.unit * 4 + 'px', + padding: `${theme.spacing.unit * 4}px`, minWidth: '600px', margin: '0 auto', } as CSSProperties, @@ -527,8 +530,8 @@ const useStyles = () => { header: { display: 'flex', alignItems: 'center', - gap: theme.spacing.unit * 1.5 + 'px', - marginBottom: theme.spacing.unit * 1.5 + 'px', + gap: `${theme.spacing.unit * 1.5}px`, + marginBottom: `${theme.spacing.unit * 1.5}px`, } as CSSProperties, profileInfo: { flex: 1, @@ -542,12 +545,12 @@ const useStyles = () => { infoContainer: { display: 'flex', flexDirection: 'column' as const, - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, } as CSSProperties, field: { display: 'flex', alignItems: 'center', - padding: theme.spacing.unit + 'px 0', + padding: `${theme.spacing.unit}px 0`, borderBottom: `1px solid ${theme.colors.border}`, minHeight: '32px', } as CSSProperties, @@ -567,7 +570,7 @@ const useStyles = () => { flex: 1, display: 'flex', alignItems: 'center', - gap: theme.spacing.unit + 'px', + gap: `${theme.spacing.unit}px`, overflow: 'hidden', minHeight: '32px', '& input, & .MuiInputBase-root': { @@ -585,7 +588,7 @@ const useStyles = () => { }, } as CSSProperties, popup: { - padding: theme.spacing.unit * 2 + 'px', + padding: `${theme.spacing.unit * 2}px`, } as CSSProperties, }), [theme, colorScheme], diff --git a/samples/teamspace-react/src/pages/Dashboard.tsx b/samples/teamspace-react/src/pages/Dashboard.tsx index 91d9ad9fa..13ed46408 100644 --- a/samples/teamspace-react/src/pages/Dashboard.tsx +++ b/samples/teamspace-react/src/pages/Dashboard.tsx @@ -98,9 +98,10 @@ export default function Dashboard() {

Welcome back{' '} - {(user) => ( + {user => ( - {user?.name?.givenName} {user?.name?.familyName} + {user?.givenName || user?.name?.givenName || user?.given_name}{' '} + {user?.name?.familyName || user?.familyName || user?.family_name} )} From 48c311be49687ba3a7526a3e8c5a32979a4b6fe9 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 08:09:56 +0530 Subject: [PATCH 17/96] feat(react): implement SignIn component for native authentication flow and update AsgardeoProvider for sign-in handling --- ...itializeApplicationNativeAuthentication.ts | 42 ++--- packages/nextjs/src/AsgardeoNextClient.ts | 50 +++++- .../components/presentation/SignIn/SignIn.tsx | 144 ++++++++++++++++++ .../contexts/Asgardeo/AsgardeoProvider.tsx | 14 +- packages/nextjs/src/index.ts | 3 + packages/node/src/__legacy__/client.ts | 14 +- samples/teamspace-nextjs/app/page.tsx | 8 +- samples/teamspace-nextjs/app/signin/page.tsx | 7 + 8 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx create mode 100644 samples/teamspace-nextjs/app/signin/page.tsx diff --git a/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts b/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts index 6f093be0e..0e563ee4d 100644 --- a/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts +++ b/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts @@ -24,60 +24,59 @@ import {ApplicationNativeAuthenticationInitiateResponse} from '../models/applica */ export interface AuthorizationRequest { /** - * The response type (e.g., 'code', 'token', 'id_token'). + * Additional authorization parameters. */ - response_type?: string; + [key: string]: any; /** * The client identifier. */ client_id?: string; /** - * The redirection URI after authorization. + * PKCE code challenge. */ - redirect_uri?: string; + code_challenge?: string; /** - * The scope of the access request. + * PKCE code challenge method. */ - scope?: string; + code_challenge_method?: string; /** - * An unguessable random string to prevent CSRF attacks. + * The allowable elapsed time in seconds since the last time the End-User was actively authenticated. */ - state?: string; + max_age?: number; /** * String value used to associate a Client session with an ID Token. */ nonce?: string; - /** - * How the authorization response should be returned. - */ - response_mode?: string; /** * Space delimited, case sensitive list of ASCII string values. */ prompt?: string; /** - * The allowable elapsed time in seconds since the last time the End-User was actively authenticated. + * The redirection URI after authorization. */ - max_age?: number; + redirect_uri?: string; /** - * PKCE code challenge. + * How the authorization response should be returned. */ - code_challenge?: string; + response_mode?: string; /** - * PKCE code challenge method. + * The response type (e.g., 'code', 'token', 'id_token'). */ - code_challenge_method?: string; + response_type?: string; /** - * Additional authorization parameters. + * The scope of the access request. */ - [key: string]: any; + scope?: string; + /** + * An unguessable random string to prevent CSRF attacks. + */ + state?: string; } /** * Request configuration for the authorize function. */ export interface AuthorizeRequestConfig extends Partial { - url?: string; /** * The base URL of the Asgardeo server. */ @@ -86,6 +85,7 @@ export interface AuthorizeRequestConfig extends Partial { * The authorization request payload. */ payload: AuthorizationRequest; + url?: string; } /** diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 77479de32..68646f293 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -18,20 +18,25 @@ import { AsgardeoNodeClient, + AsgardeoRuntimeError, + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, LegacyAsgardeoNodeClient, SignInOptions, SignOutOptions, + SignUpOptions, User, UserProfile, + initializeApplicationNativeAuthentication, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; +import InternalAuthAPIRoutesConfig from './configs/InternalAuthAPIRoutesConfig'; import {AsgardeoNextConfig} from './models/config'; import deleteSessionId from './server/actions/deleteSessionId'; import getSessionId from './server/actions/getSessionId'; import getIsSignedIn from './server/actions/isSignedIn'; import setSessionId from './server/actions/setSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; -import InternalAuthAPIRoutesConfig from './configs/InternalAuthAPIRoutesConfig'; const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path.slice(0, -1) : path); /** @@ -54,15 +59,15 @@ class AsgardeoNextClient exte return this.asgardeo.initialize({ baseUrl, - clientId: clientId, + clientId, clientSecret, - afterSignInUrl: afterSignInUrl, + afterSignInUrl, ...rest, } as any); } override async getUser(userId?: string): Promise { - let resolvedSessionId: string = userId || ((await getSessionId()) as string); + const resolvedSessionId: string = userId || ((await getSessionId()) as string); return this.asgardeo.getUser(resolvedSessionId); } @@ -118,20 +123,49 @@ class AsgardeoNextClient exte return Promise.resolve(await this.asgardeo.signOut(resolvedSessionId)); } + override async signUp(options?: SignUpOptions): Promise; + override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; + override async signUp(...args: any[]): Promise { + throw new AsgardeoRuntimeError( + 'Not implemented', + 'react-AsgardeoReactClient-ValidationError-002', + 'react', + 'The signUp method with SignUpOptions is not implemented in the React client.', + ); + } + async handler(req: NextRequest): Promise { const {pathname, searchParams} = req.nextUrl; const sanitizedPathname: string = removeTrailingSlash(pathname); const {method} = req; + if ((method === 'POST' && sanitizedPathname === InternalAuthAPIRoutesConfig.signIn) || searchParams.get('code')) { + let response; + + const userId: string | undefined = await getSessionId(); + + const signInUrl: URL = new URL(await this.asgardeo.getSignInUrl({response_mode: 'direct'}, userId)); + const {pathname, origin, searchParams} = signInUrl; + + try { + response = await initializeApplicationNativeAuthentication({ + url: `${origin}${pathname}`, + payload: Object.fromEntries(searchParams.entries()), + }); + } catch (error) { + throw new Error(`Failed to initialize application native authentication`); + } + + return NextResponse.json(response); + } + if ((method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.signIn) || searchParams.get('code')) { let response: NextResponse | undefined; await this.signIn( {}, undefined, - (redirectUrl: string) => { - return (response = NextResponse.redirect(redirectUrl, 302)); - }, + (redirectUrl: string) => (response = NextResponse.redirect(redirectUrl, 302)), searchParams.get('code') as string, searchParams.get('session_state') as string, searchParams.get('state') as string, @@ -158,7 +192,7 @@ class AsgardeoNextClient exte try { const isSignedIn: boolean = await getIsSignedIn(); - return NextResponse.json({isSignedIn: isSignedIn}); + return NextResponse.json({isSignedIn}); } catch (error) { return NextResponse.json({error: 'Failed to check session'}, {status: 500}); } diff --git a/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx b/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx new file mode 100644 index 000000000..9a3fa00fc --- /dev/null +++ b/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx @@ -0,0 +1,144 @@ +/** + * 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. + */ + +'use client'; + +import {ApplicationNativeAuthenticationInitiateResponse} from '@asgardeo/node'; +import {BaseSignIn, BaseSignInProps} from '@asgardeo/react'; +import {FC} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the SignIn component. + * Extends BaseSignInProps for full compatibility with the React BaseSignIn component + */ +export interface SignInProps extends BaseSignInProps { + /** + * URL to redirect to after successful sign-in. + * If not provided, will use the current window location. + */ + afterSignInUrl?: string; + + /** + * Additional CSS class names for customization. + */ + className?: string; + + /** + * Size variant for the component. + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Theme variant for the component. + */ + variant?: 'default' | 'outlined' | 'filled'; +} + +/** + * A SignIn component for Next.js that provides native authentication flow. + * This component delegates to the BaseSignIn from @asgardeo/react and requires + * the API functions to be provided as props. + * + * @remarks This component requires the authentication API functions to be provided + * as props. For a complete working example, you'll need to implement the server-side + * authentication endpoints or use the traditional OAuth flow with SignInButton. + * + * @example + * ```tsx + * import { SignIn } from '@asgardeo/nextjs'; + * import { handleApplicationNativeAuthentication } from '@asgardeo/browser'; + * + * const LoginPage = () => { + * const handleInitialize = async () => { + * return await handleApplicationNativeAuthentication({ + * response_mode: 'direct', + * }); + * }; + * + * const handleSubmit = async (flow) => { + * return await handleApplicationNativeAuthentication({ flow }); + * }; + * + * return ( + * { + * console.log('Authentication successful:', authData); + * }} + * onError={(error) => { + * console.error('Authentication failed:', error); + * }} + * size="medium" + * variant="outlined" + * afterSignInUrl="/dashboard" + * /> + * ); + * }; + * ``` + */ +const SignIn: FC = ({ + afterSignInUrl, + className, + onError, + onFlowChange, + onInitialize, + onSubmit, + onSuccess, + size = 'medium', + variant = 'default', + ...rest +}: SignInProps) => { + const {signIn} = useAsgardeo(); + + /** + * Handle successful authentication and redirect with query params. + */ + /** + * Initialize the authentication flow. + */ + const handleInitialize = async (): Promise => + await signIn({response_mode: 'direct'}); + + /** + * Handle authentication errors + */ + const handleError = (error: Error): void => { + onError?.(error); + }; + + return ( + + ); +}; + +SignIn.displayName = 'SignIn'; + +export default SignIn; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 0d4c79768..a9de91783 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -18,9 +18,9 @@ 'use client'; -import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; -import {I18nProvider, FlowProvider, UserProvider, ThemeProvider} from '@asgardeo/react'; import {User} from '@asgardeo/node'; +import {I18nProvider, FlowProvider, UserProvider, ThemeProvider} from '@asgardeo/react'; +import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; import AsgardeoContext from './AsgardeoContext'; import InternalAuthAPIRoutesConfig from '../../../configs/InternalAuthAPIRoutesConfig'; @@ -81,12 +81,20 @@ const AsgardeoClientProvider: FC> fetchUserData(); }, []); + const signIn = async () => { + const response = await fetch(InternalAuthAPIRoutesConfig.signIn, { + method: 'POST', + }); + + return response.json(); + }; + const contextValue = useMemo( () => ({ user, isSignedIn, isLoading, - signIn: () => (window.location.href = InternalAuthAPIRoutesConfig.signIn), + signIn, signOut: () => (window.location.href = InternalAuthAPIRoutesConfig.signOut), }), [user, isSignedIn, isLoading], diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 2ba4b280f..4cbfbb733 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -30,6 +30,9 @@ export {SignedOutProps} from './client/components/control/SignedOut/SignedOut'; export {default as SignInButton} from './client/components/actions/SignInButton/SignInButton'; export type {SignInButtonProps} from './client/components/actions/SignInButton/SignInButton'; +export {default as SignIn} from './client/components/presentation/SignIn/SignIn'; +export type {SignInProps} from './client/components/presentation/SignIn/SignIn'; + export {default as SignOutButton} from './client/components/actions/SignOutButton/SignOutButton'; export type {SignOutButtonProps} from './client/components/actions/SignOutButton/SignOutButton'; diff --git a/packages/node/src/__legacy__/client.ts b/packages/node/src/__legacy__/client.ts index 00406486a..eb76cf02c 100644 --- a/packages/node/src/__legacy__/client.ts +++ b/packages/node/src/__legacy__/client.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * Copyright (c) {{year}}, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -25,6 +25,7 @@ import { Storage, TokenResponse, User, + ExtendedAuthorizeRequestUrlParams, } from '@asgardeo/javascript'; import {AsgardeoNodeCore} from './core'; import {AuthURLCallback} from './models'; @@ -298,10 +299,7 @@ export class AsgardeoNodeClient { * @memberof AsgardeoNodeClient * */ - public async exchangeToken( - config: TokenExchangeRequestConfig, - userId?: string, - ): Promise { + public async exchangeToken(config: TokenExchangeRequestConfig, userId?: string): Promise { return this._authCore.exchangeToken(config, userId); } @@ -328,6 +326,10 @@ export class AsgardeoNodeClient { return this._authCore.reInitialize(config); } + public async getSignInUrl(requestConfig?: ExtendedAuthorizeRequestUrlParams, userId?: string): Promise { + return this._authCore.getAuthURL(userId, requestConfig); + } + /** * This method returns a Promise that resolves with the response returned by the server. * @param {string} userId - The userId of the user. diff --git a/samples/teamspace-nextjs/app/page.tsx b/samples/teamspace-nextjs/app/page.tsx index 5f97c435f..78ef68c25 100644 --- a/samples/teamspace-nextjs/app/page.tsx +++ b/samples/teamspace-nextjs/app/page.tsx @@ -10,13 +10,7 @@ export default function Home() {
- - {({signIn, isLoading}) => ( - - )} - + Sign In with Redirect diff --git a/samples/teamspace-nextjs/app/signin/page.tsx b/samples/teamspace-nextjs/app/signin/page.tsx new file mode 100644 index 000000000..3fca357fd --- /dev/null +++ b/samples/teamspace-nextjs/app/signin/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import {SignIn} from '@asgardeo/nextjs'; + +export default function SignInPage() { + return ; +} From 925ced104f051e3cb5702be4cee83022dbb3a718 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 08:31:59 +0530 Subject: [PATCH 18/96] fix: reorder url property in EmbeddedSignUpFlowExecuteRequestConfig for clarity --- packages/javascript/src/api/executeEmbeddedSignUpFlow.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts index ced448de8..4c25a9dfc 100644 --- a/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts +++ b/packages/javascript/src/api/executeEmbeddedSignUpFlow.ts @@ -27,7 +27,6 @@ import { * Request configuration for the embedded signup flow execution function. */ export interface EmbeddedSignUpFlowExecuteRequestConfig extends Partial { - url?: string; /** * The base URL of the Asgardeo server. */ @@ -36,6 +35,10 @@ export interface EmbeddedSignUpFlowExecuteRequestConfig extends Partial * The embedded signup flow execution request payload. */ payload?: EmbeddedFlowExecuteRequestPayload; + /** + * The URL to which the request should be sent. + */ + url?: string; } /** From 02952d5617fc38308f0a37231f1b0853fbf325b4 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 08:32:53 +0530 Subject: [PATCH 19/96] feat(react): make onInitialize and onSubmit props optional in BaseSignInProps interface --- .../react/src/components/presentation/SignIn/BaseSignIn.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index ef6161cdb..6d24b3806 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -218,14 +218,14 @@ export interface BaseSignInProps { * Function to initialize authentication flow. * @returns Promise resolving to the initial authentication response. */ - onInitialize: () => Promise; + onInitialize?: () => Promise; /** * Function to handle authentication steps. * @param payload - The authentication payload. * @returns Promise resolving to the authentication response. */ - onSubmit: (flow: { + onSubmit?: (flow: { requestConfig?: { method: string; url: string; From 56421256e5dd58bfc4014553e39b5561cf516d6e Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 08:59:29 +0530 Subject: [PATCH 20/96] feat(react): enhance Asgardeo context and components with initialization handling and improved prop management --- packages/react/src/AsgardeoReactClient.ts | 14 +- packages/react/src/__temp__/api.ts | 38 +++--- .../presentation/SignUp/BaseSignUp.tsx | 125 +++++++++--------- .../components/presentation/SignUp/SignUp.tsx | 43 +++--- .../src/contexts/Asgardeo/AsgardeoContext.ts | 8 +- .../contexts/Asgardeo/AsgardeoProvider.tsx | 14 +- 6 files changed, 134 insertions(+), 108 deletions(-) diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 354f0688b..d67d880c2 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -67,7 +67,9 @@ class AsgardeoReactClient e override async getUser(): Promise { try { - const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; + const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); @@ -79,7 +81,8 @@ class AsgardeoReactClient e async getUserProfile(): Promise { try { - const {baseUrl} = await this.asgardeo.getConfigData(); + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); @@ -104,6 +107,10 @@ class AsgardeoReactClient e return this.asgardeo.isLoading(); } + async isInitialized(): Promise { + return this.asgardeo.isInitialized(); + } + override isSignedIn(): Promise { return this.asgardeo.isSignedIn(); } @@ -143,7 +150,8 @@ class AsgardeoReactClient e const firstArg = args[0]; if (typeof firstArg === 'object' && 'flowType' in firstArg) { - const baseUrl: string = (await this.asgardeo.getConfigData())?.baseUrl; + const configData = await this.asgardeo.getConfigData(); + const baseUrl: string = configData?.baseUrl; return executeEmbeddedSignUpFlow({ baseUrl, diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts index 279ebcb81..5e972d35b 100644 --- a/packages/react/src/__temp__/api.ts +++ b/packages/react/src/__temp__/api.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2025, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -79,7 +79,7 @@ class AuthAPI { * @param {Config} config - `dispatch` function from React Auth Context. */ public async init(config: AuthClientConfig): Promise { - return await this._client.initialize(config); + return this._client.initialize(config); } /** @@ -88,7 +88,17 @@ class AuthAPI { * @returns {Promise>} - A promise that resolves with the configuration data. */ public async getConfigData(): Promise> { - return await this._client.getConfigData(); + return this._client.getConfigData(); + } + + /** + * Method to get the configuration data. + * + * @returns {Promise>} - A promise that resolves with the configuration data. + */ + public async isInitialized(): Promise { + // Wait for initialization to complete + return this._client.isInitialized(); } /** @@ -139,9 +149,7 @@ class AuthAPI { return response; }) - .catch(error => { - return Promise.reject(error); - }); + .catch(error => Promise.reject(error)); } /** @@ -161,9 +169,7 @@ class AuthAPI { return response; }) - .catch(error => { - return Promise.reject(error); - }); + .catch(error => Promise.reject(error)); } /** @@ -247,9 +253,7 @@ class AuthAPI { return response; }) - .catch(error => { - return Promise.reject(error); - }); + .catch(error => Promise.reject(error)); } /** @@ -265,9 +269,7 @@ class AuthAPI { dispatch(AuthAPI.DEFAULT_STATE); return true; }) - .catch(error => { - return Promise.reject(error); - }); + .catch(error => Promise.reject(error)); } /** @@ -461,9 +463,7 @@ class AuthAPI { return response; }) - .catch(error => { - return Promise.reject(error); - }); + .catch(error => Promise.reject(error)); } } diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 0c7f7b6ad..80fb4bc70 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -24,94 +24,95 @@ import { withVendorCSSClassPrefix, AsgardeoAPIError, } from '@asgardeo/browser'; -import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; import {clsx} from 'clsx'; -import Card from '../../primitives/Card/Card'; -import Alert from '../../primitives/Alert/Alert'; -import Divider from '../../primitives/Divider/Divider'; -import Typography from '../../primitives/Typography/Typography'; -import Spinner from '../../primitives/Spinner/Spinner'; -import useTranslation from '../../../hooks/useTranslation'; -import {useForm, FormField} from '../../../hooks/useForm'; +import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; +import {useForm, FormField} from '../../../hooks/useForm'; +import useTranslation from '../../../hooks/useTranslation'; import {createField} from '../../factories/FieldFactory'; +import Alert from '../../primitives/Alert/Alert'; import Button from '../../primitives/Button/Button'; +import Card from '../../primitives/Card/Card'; +import Divider from '../../primitives/Divider/Divider'; +import Spinner from '../../primitives/Spinner/Spinner'; +import Typography from '../../primitives/Typography/Typography'; /** * Props for the BaseSignUp component. */ export interface BaseSignUpProps { /** - * Function to initialize sign-up flow. - * @returns Promise resolving to the initial sign-up response. + * URL to redirect after successful sign-up. */ - onInitialize: (payload?: EmbeddedFlowExecuteRequestPayload) => Promise; + afterSignUpUrl?: string; /** - * Function to handle sign-up steps. - * @param payload - The sign-up payload. - * @returns Promise resolving to the sign-up response. + * Custom CSS class name for the submit button. */ - onSubmit: (payload: EmbeddedFlowExecuteRequestPayload) => Promise; + buttonClassName?: string; /** - * Callback function called when sign-up is successful. - * @param response - The sign-up response data returned upon successful completion. + * Custom CSS class name for the form container. */ - onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; + className?: string; /** - * Callback function called when sign-up fails. - * @param error - The error that occurred during sign-up. + * Custom CSS class name for error messages. */ - onError?: (error: Error) => void; + errorClassName?: string; /** - * Callback function called when sign-up flow status changes. - * @param response - The current sign-up response. + * Custom CSS class name for form inputs. */ - onFlowChange?: (response: EmbeddedFlowExecuteResponse) => void; + inputClassName?: string; + + isInitialized?: boolean; /** - * Custom CSS class name for the form container. + * Custom CSS class name for info messages. */ - className?: string; + messageClassName?: string; /** - * Custom CSS class name for form inputs. + * Callback function called when sign-up fails. + * @param error - The error that occurred during sign-up. */ - inputClassName?: string; + onError?: (error: Error) => void; /** - * Custom CSS class name for the submit button. + * Callback function called when sign-up flow status changes. + * @param response - The current sign-up response. */ - buttonClassName?: string; + onFlowChange?: (response: EmbeddedFlowExecuteResponse) => void; /** - * Custom CSS class name for error messages. + * Function to initialize sign-up flow. + * @returns Promise resolving to the initial sign-up response. */ - errorClassName?: string; + onInitialize: (payload?: EmbeddedFlowExecuteRequestPayload) => Promise; /** - * Custom CSS class name for info messages. + * Function to handle sign-up steps. + * @param payload - The sign-up payload. + * @returns Promise resolving to the sign-up response. */ - messageClassName?: string; + onSubmit: (payload: EmbeddedFlowExecuteRequestPayload) => Promise; + + /** + * Callback function called when sign-up is successful. + * @param response - The sign-up response data returned upon successful completion. + */ + onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; /** * Size variant for the component. */ size?: 'small' | 'medium' | 'large'; - /** * Theme variant for the component. */ variant?: 'default' | 'outlined' | 'filled'; - - /** - * URL to redirect after successful sign-up. - */ - afterSignUpUrl?: string; } /** @@ -146,13 +147,11 @@ export interface BaseSignUpProps { * }; * ``` */ -const BaseSignUp: FC = props => { - return ( - - - - ); -}; +const BaseSignUp: FC = props => ( + + + +); /** * Internal component that consumes FlowContext and renders the sign-up UI. @@ -171,12 +170,13 @@ const BaseSignUpContent: FC = ({ messageClassName = '', size = 'medium', variant = 'default', + isInitialized, }) => { const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); const [isLoading, setIsLoading] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); + const [isFlowInitialized, setIsFlowInitialized] = useState(false); const [currentFlow, setCurrentFlow] = useState(null); const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); @@ -342,8 +342,8 @@ const BaseSignUpContent: FC = ({ * Render form components based on flow data */ const renderComponents = useCallback( - (components: any[]): ReactElement[] => { - return components + (components: any[]): ReactElement[] => + components .map((component, index) => { const config = component.config || {}; @@ -395,8 +395,7 @@ const BaseSignUpContent: FC = ({ return null; } }) - .filter(Boolean) as ReactElement[]; - }, + .filter(Boolean) as ReactElement[], [formValues, touchedFields, formErrors, isFormValid, isLoading, size], ); @@ -436,7 +435,7 @@ const BaseSignUpContent: FC = ({ // Initialize the flow on component mount useEffect(() => { - if (!isInitialized && !initializationAttemptedRef.current) { + if (isInitialized && !isFlowInitialized && !initializationAttemptedRef.current) { initializationAttemptedRef.current = true; // Inline initialization to avoid dependency issues @@ -448,11 +447,9 @@ const BaseSignUpContent: FC = ({ const response = await onInitialize(); setCurrentFlow(response); - setIsInitialized(true); + setIsFlowInitialized(true); onFlowChange?.(response); - debugger; - if (response.flowStatus === EmbeddedFlowStatus.Complete) { onSuccess?.(response); @@ -476,9 +473,19 @@ const BaseSignUpContent: FC = ({ performInitialization(); } - }, [isInitialized, onInitialize, onSuccess, onError, onFlowChange, setupFormFields, afterSignUpUrl, t]); - - if (!isInitialized && isLoading) { + }, [ + isInitialized, + isFlowInitialized, + onInitialize, + onSuccess, + onError, + onFlowChange, + setupFormFields, + afterSignUpUrl, + t, + ]); + + if (!isFlowInitialized && isLoading) { return (
diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx index b23067629..26c025d2a 100644 --- a/packages/react/src/components/presentation/SignUp/SignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx @@ -26,24 +26,20 @@ import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; */ export interface SignUpProps { /** - * Additional CSS class names for customization. - */ - className?: string; - - /** - * Size variant for the component. + * URL to redirect after successful sign-up. */ - size?: 'small' | 'medium' | 'large'; + afterSignUpUrl?: string; /** - * Theme variant for the component. + * Additional CSS class names for customization. */ - variant?: 'default' | 'outlined' | 'filled'; + className?: string; /** - * URL to redirect after successful sign-up. + * Callback function called when sign-up fails. + * @param error - The error that occurred during sign-up. */ - afterSignUpUrl?: string; + onError?: (error: Error) => void; /** * Callback function called when sign-up is successful. @@ -52,10 +48,14 @@ export interface SignUpProps { onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; /** - * Callback function called when sign-up fails. - * @param error - The error that occurred during sign-up. + * Size variant for the component. */ - onError?: (error: Error) => void; + size?: 'small' | 'medium' | 'large'; + + /** + * Theme variant for the component. + */ + variant?: 'default' | 'outlined' | 'filled'; } /** @@ -92,27 +92,23 @@ const SignUp: FC = ({ onSuccess, onError, }) => { - const {signUp} = useAsgardeo(); + const {signUp, isInitialized} = useAsgardeo(); /** * Initialize the sign-up flow. */ - const handleInitialize = async ( - payload?: EmbeddedFlowExecuteRequestPayload, - ): Promise => { - return await signUp( + const handleInitialize = async (payload?: EmbeddedFlowExecuteRequestPayload): Promise => + await signUp( payload || { flowType: EmbeddedFlowType.Registration, }, ); - }; /** * Handle sign-up steps. */ - const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise => { - return await signUp(payload); - }; + const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise => + await signUp(payload); /** * Handle successful sign-up and redirect. @@ -137,6 +133,7 @@ const SignUp: FC = ({ className={className} size={size} variant={variant} + isInitialized={isInitialized} /> ); }; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index 0d2f0e418..c70a8c66b 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -16,13 +16,16 @@ * under the License. */ -import {Context, createContext} from 'react'; import {User} from '@asgardeo/browser'; +import {Context, createContext} from 'react'; /** * Props interface of {@link AsgardeoContext} */ export type AsgardeoContextProps = { + afterSignInUrl: string; + baseUrl: string; + isInitialized: boolean; /** * Flag indicating whether the SDK is working in the background. */ @@ -50,8 +53,6 @@ export type AsgardeoContextProps = { */ signUp: any; user: any; - baseUrl: string; - afterSignInUrl: string; }; /** @@ -66,6 +67,7 @@ const AsgardeoContext: Context = createContext> = ({ const [user, setUser] = useState(null); const [isSignedInSync, setIsSignedInSync] = useState(false); + const [isInitializedSync, setIsInitializedSync] = useState(false); const [userProfile, setUserProfile] = useState(null); @@ -108,7 +109,6 @@ const AsgardeoProvider: FC> = ({ // setError(null); } catch (error) { - debugger; if (error && Object.prototype.hasOwnProperty.call(error, 'code')) { // setError(error); } @@ -151,6 +151,17 @@ const AsgardeoProvider: FC> = ({ }; }, [asgardeo]); + useEffect(() => { + (async () => { + try { + const status = await asgardeo.isInitialized(); + setIsInitializedSync(status); + } catch (error) { + setIsInitializedSync(false); + } + })(); + }, [asgardeo]); + const signIn = async (options?: SignInOptions): Promise => { try { const response = await asgardeo.signIn(options); @@ -200,6 +211,7 @@ const AsgardeoProvider: FC> = ({ user, baseUrl, afterSignInUrl, + isInitialized: isInitializedSync, }} > From 3d91761f4058b4dd817aff52850c2c1f2940fd12 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:26:22 +0530 Subject: [PATCH 21/96] feat(react): implement sign-up flow components including EmailInput, PasswordInput, and FormContainer; enhance translations and add new UI elements --- packages/javascript/src/i18n/en-US.ts | 11 +- packages/javascript/src/models/i18n.ts | 7 + .../presentation/SignUp/BaseSignUp.tsx | 179 +++++++--------- .../SignUp/options/EmailInput.tsx | 53 +++++ .../SignUp/options/FormContainer.tsx | 46 +++++ .../SignUp/options/PasswordInput.tsx | 89 ++++++++ .../SignUp/options/SignUpOptionFactory.tsx | 194 ++++++++++++++++++ .../SignUp/options/SubmitButton.tsx | 55 +++++ .../presentation/SignUp/options/TextInput.tsx | 53 +++++ .../SignUp/options/Typography.tsx | 76 +++++++ .../presentation/SignUp/options/index.ts | 31 +++ 11 files changed, 684 insertions(+), 110 deletions(-) create mode 100644 packages/react/src/components/presentation/SignUp/options/EmailInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/FormContainer.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/TextInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/Typography.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/index.ts diff --git a/packages/javascript/src/i18n/en-US.ts b/packages/javascript/src/i18n/en-US.ts index 163f5f405..666826064 100644 --- a/packages/javascript/src/i18n/en-US.ts +++ b/packages/javascript/src/i18n/en-US.ts @@ -45,6 +45,11 @@ const translations: I18nTranslations = { /* Base Sign In */ 'signin.title': 'Sign In', + 'signin.subtitle': 'Enter your credentials to continue.', + + /* Base Sign Up */ + 'signup.title': 'Sign Up', + 'signup.subtitle': 'Create a new account to get started.', /* Email OTP */ 'email.otp.title': 'OTP Verification', @@ -84,9 +89,11 @@ const translations: I18nTranslations = { 'errors.title': 'Error', 'errors.sign.in.initialization': 'An error occurred while initializing. Please try again later.', 'errors.sign.in.flow.failure': 'An error occurred during the sign-in flow. Please try again later.', - 'errors.sign.in.flow.completion.failure': 'An error occurred while completing the sign-in flow. Please try again later.', + 'errors.sign.in.flow.completion.failure': + 'An error occurred while completing the sign-in flow. Please try again later.', 'errors.sign.in.flow.passkeys.failure': 'An error occurred while signing in with passkeys. Please try again later.', - 'errors.sign.in.flow.passkeys.completion.failure': 'An error occurred while completing the passkeys sign-in flow. Please try again later.', + 'errors.sign.in.flow.passkeys.completion.failure': + 'An error occurred while completing the passkeys sign-in flow. Please try again later.', }; const metadata: I18nMetadata = { diff --git a/packages/javascript/src/models/i18n.ts b/packages/javascript/src/models/i18n.ts index 321f0767e..9fc22a833 100644 --- a/packages/javascript/src/models/i18n.ts +++ b/packages/javascript/src/models/i18n.ts @@ -16,6 +16,8 @@ * under the License. */ +/* eslint-disable typescript-sort-keys/interface */ + export interface I18nTranslations { /* |---------------------------------------------------------------| */ /* | Elements | */ @@ -43,6 +45,11 @@ export interface I18nTranslations { /* Base Sign In */ 'signin.title': string; + 'signin.subtitle': string; + + /* Base Sign Up */ + 'signup.title': string; + 'signup.subtitle': string; /* Email OTP */ 'email.otp.title': string; diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 80fb4bc70..5ea1aa4f4 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -26,15 +26,13 @@ import { } from '@asgardeo/browser'; import {clsx} from 'clsx'; import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; +import {renderSignUpComponents} from './options/SignUpOptionFactory'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; import {useForm, FormField} from '../../../hooks/useForm'; import useTranslation from '../../../hooks/useTranslation'; -import {createField} from '../../factories/FieldFactory'; import Alert from '../../primitives/Alert/Alert'; -import Button from '../../primitives/Button/Button'; import Card from '../../primitives/Card/Card'; -import Divider from '../../primitives/Divider/Divider'; import Spinner from '../../primitives/Spinner/Spinner'; import Typography from '../../primitives/Typography/Typography'; @@ -338,67 +336,6 @@ const BaseSignUpContent: FC = ({ setFormTouched(name, true); }; - /** - * Render form components based on flow data - */ - const renderComponents = useCallback( - (components: any[]): ReactElement[] => - components - .map((component, index) => { - const config = component.config || {}; - - switch (component.type) { - case EmbeddedFlowComponentType.Typography: - return ( - - {config.text || config.content} - - ); - - case EmbeddedFlowComponentType.Input: - const fieldName = config.name || component.id; - return createField({ - type: config.type || 'text', - name: fieldName, - label: config.label || fieldName, - placeholder: config.placeholder, - required: config.required || false, - value: formValues[fieldName] || '', - error: touchedFields[fieldName] ? formErrors[fieldName] : undefined, - onChange: (value: string) => handleInputChange(fieldName, value), - className: inputClasses, - }); - - case EmbeddedFlowComponentType.Button: - return ( - - ); - - case EmbeddedFlowComponentType.Form: - return ( -
{component.components && renderComponents(component.components)}
- ); - - default: - if (component.components && Array.isArray(component.components)) { - return
{renderComponents(component.components)}
; - } - return null; - } - }) - .filter(Boolean) as ReactElement[], - [formValues, touchedFields, formErrors, isFormValid, isLoading, size], - ); - // Generate CSS classes const containerClasses = clsx( [ @@ -433,6 +370,30 @@ const BaseSignUpContent: FC = ({ const messageClasses = clsx([withVendorCSSClassPrefix('signup__messages')], messageClassName); + /** + * Render form components based on flow data using the factory + */ + const renderComponents = useCallback( + (components: any[]): ReactElement[] => + renderSignUpComponents( + components, + formValues, + touchedFields, + formErrors, + isLoading, + isFormValid, + handleInputChange, + { + buttonClassName: buttonClasses, + error, + inputClassName: inputClasses, + size, + variant, + }, + ), + [formValues, touchedFields, formErrors, isFormValid, isLoading, size, variant, error, inputClasses, buttonClasses], + ); + // Initialize the flow on component mount useEffect(() => { if (isInitialized && !isFlowInitialized && !initializationAttemptedRef.current) { @@ -488,9 +449,11 @@ const BaseSignUpContent: FC = ({ if (!isFlowInitialized && isLoading) { return ( -
- -
+ +
+ +
+
); } @@ -498,53 +461,53 @@ const BaseSignUpContent: FC = ({ if (!currentFlow) { return ( - - {error || t('errors.sign.up.flow.initialization.failure')} - + + + {t('errors.title') || 'Error'} + {error || t('errors.sign.up.flow.initialization.failure')} + + ); } return ( - {(flowTitle || flowSubtitle) && ( -
- {flowTitle && ( - - {flowTitle} - - )} - {flowSubtitle && ( - - {flowSubtitle} - - )} -
- )} - - {error && ( - - {error} - - )} - - {flowMessages && flowMessages.length > 0 && ( -
- {flowMessages.map((message, index) => ( - - {message.message} - - ))} -
- )} - -
- {currentFlow.data?.components && renderComponents(currentFlow.data.components)} -
+ + {flowTitle || t('signup.title')} + {flowSubtitle && ( + + {flowSubtitle || t('signup.subtitle')} + + )} + {flowMessages && flowMessages.length > 0 && ( +
+ {flowMessages.map((message: any, index: number) => ( + + {message.message} + + ))} +
+ )} +
+ + + {error && ( + + {t('errors.title') || 'Error'} + {error} + + )} + +
+ {currentFlow.data?.components && renderComponents(currentFlow.data.components)} +
+
); }; diff --git a/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx b/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx new file mode 100644 index 000000000..1fe24075b --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx @@ -0,0 +1,53 @@ +/** + * 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 {FC} from 'react'; +import {FieldType} from '@asgardeo/browser'; +import {createField} from '../../../factories/FieldFactory'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; + +/** + * Email input component for sign-up forms. + */ +const EmailInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return createField({ + type: FieldType.Email, + name: fieldName, + label: config['label'] || 'Email', + placeholder: config['placeholder'] || 'Enter your email', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default EmailInput; diff --git a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx new file mode 100644 index 000000000..066a376cc --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx @@ -0,0 +1,46 @@ +/** + * 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 {FC} from 'react'; +import {createSignUpComponent, BaseSignUpOptionProps} from './SignUpOptionFactory'; + +/** + * Form container component that renders child components. + */ +const FormContainer: FC = props => { + const {component} = props; + + // If the form has child components, render them + if (component.components && component.components.length > 0) { + return ( +
+ {component.components.map((childComponent, index) => + createSignUpComponent({ + ...props, + component: childComponent, + }), + )} +
+ ); + } + + // Empty form container + return
; +}; + +export default FormContainer; diff --git a/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx b/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx new file mode 100644 index 000000000..725152252 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx @@ -0,0 +1,89 @@ +/** + * 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 {FC} from 'react'; +import {FieldType} from '@asgardeo/browser'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; + +/** + * Password input component for sign-up forms. + */ +const PasswordInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + // Extract validation rules from the component config if available + const validations = config['validations'] || []; + const validationHints: string[] = []; + + validations.forEach((validation: any) => { + if (validation.name === 'LengthValidator') { + const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const maxLength = validation.conditions?.find((c: any) => c.key === 'max.length')?.value; + if (minLength || maxLength) { + validationHints.push(`Length: ${minLength || '0'}-${maxLength || '∞'} characters`); + } + } else if (validation.name === 'UpperCaseValidator') { + const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + if (minLength && parseInt(minLength, 10) > 0) { + validationHints.push('Must contain uppercase letter(s)'); + } + } else if (validation.name === 'LowerCaseValidator') { + const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + if (minLength && parseInt(minLength, 10) > 0) { + validationHints.push('Must contain lowercase letter(s)'); + } + } else if (validation.name === 'NumeralValidator') { + const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + if (minLength && parseInt(minLength, 10) > 0) { + validationHints.push('Must contain number(s)'); + } + } else if (validation.name === 'SpecialCharacterValidator') { + const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + if (minLength && parseInt(minLength, 10) > 0) { + validationHints.push('Must contain special character(s)'); + } + } + }); + + const hint = validationHints.length > 0 ? validationHints.join(', ') : config['hint'] || ''; + + return createField({ + type: FieldType.Password, + name: fieldName, + label: config['label'] || 'Password', + placeholder: config['placeholder'] || 'Enter your password', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default PasswordInput; diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx new file mode 100644 index 000000000..0adc483c5 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -0,0 +1,194 @@ +/** + * 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 {EmbeddedFlowComponent, EmbeddedFlowComponentType, WithPreferences} from '@asgardeo/browser'; +import {ReactElement} from 'react'; +import EmailInput from './EmailInput'; +import FormContainer from './FormContainer'; +import PasswordInput from './PasswordInput'; +import SubmitButton from './SubmitButton'; +import TextInput from './TextInput'; +import Typography from './Typography'; + +/** + * Base props that all sign-up option components share. + */ +export interface BaseSignUpOptionProps extends WithPreferences { + /** + * Custom CSS class name for buttons. + */ + buttonClassName?: string; + + /** + * The component configuration from the flow response. + */ + component: EmbeddedFlowComponent; + + /** + * Global error message to display. + */ + error?: string | null; + + /** + * Form validation errors. + */ + formErrors: Record; + + /** + * Current form values. + */ + formValues: Record; + + /** + * Custom CSS class name for form inputs. + */ + inputClassName?: string; + + /** + * Whether the form is valid. + */ + isFormValid: boolean; + + /** + * Whether the component is in loading state. + */ + isLoading: boolean; + + /** + * Callback function called when input values change. + */ + onInputChange: (name: string, value: string) => void; + + /** + * Component size variant. + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Touched state for form fields. + */ + touchedFields: Record; + + /** + * Component theme variant. + */ + variant?: 'default' | 'outlined' | 'filled'; +} + +/** + * Creates the appropriate sign-up component based on the component type. + */ +export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElement => { + const {component} = props; + + switch (component.type) { + case EmbeddedFlowComponentType.Typography: + return ; + + case EmbeddedFlowComponentType.Input: + // Determine input type based on variant or config + const inputVariant = component.variant?.toUpperCase(); + const inputType = component.config['type']?.toLowerCase(); + + if (inputVariant === 'EMAIL' || inputType === 'email') { + return ; + } + if (inputVariant === 'PASSWORD' || inputType === 'password') { + return ; + } + return ; + + case EmbeddedFlowComponentType.Button: + return ; + + case EmbeddedFlowComponentType.Form: + return ; + + default: + return
; + } +}; + +/** + * Convenience function that creates the appropriate sign-up component from flow component data. + */ +export const createSignUpOptionFromComponent = ( + component: EmbeddedFlowComponent, + formValues: Record, + touchedFields: Record, + formErrors: Record, + isLoading: boolean, + isFormValid: boolean, + onInputChange: (name: string, value: string) => void, + options?: { + buttonClassName?: string; + error?: string | null; + inputClassName?: string; + key?: string | number; + size?: 'small' | 'medium' | 'large'; + variant?: 'default' | 'outlined' | 'filled'; + }, +): ReactElement => + createSignUpComponent({ + component, + formValues, + touchedFields, + formErrors, + isLoading, + isFormValid, + onInputChange, + ...options, + }); + +/** + * Processes an array of components and renders them as React elements. + */ +export const renderSignUpComponents = ( + components: EmbeddedFlowComponent[], + formValues: Record, + touchedFields: Record, + formErrors: Record, + isLoading: boolean, + isFormValid: boolean, + onInputChange: (name: string, value: string) => void, + options?: { + buttonClassName?: string; + error?: string | null; + inputClassName?: string; + size?: 'small' | 'medium' | 'large'; + variant?: 'default' | 'outlined' | 'filled'; + }, +): ReactElement[] => + components + .map((component, index) => + createSignUpOptionFromComponent( + component, + formValues, + touchedFields, + formErrors, + isLoading, + isFormValid, + onInputChange, + { + ...options, + // Use component id as key, fallback to index + key: component.id || index, + }, + ), + ) + .filter(Boolean); diff --git a/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx b/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx new file mode 100644 index 000000000..95809beb8 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx @@ -0,0 +1,55 @@ +/** + * 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 {FC} from 'react'; + +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import Button from '../../../primitives/Button/Button'; +import Spinner from '../../../primitives/Spinner/Spinner'; + +/** + * Submit button component for sign-up forms. + */ +const SubmitButton: FC = ({ + component, + isLoading, + isFormValid, + buttonClassName, + size = 'medium', +}) => { + const config = component.config || {}; + const buttonText = config['text'] || config['label'] || 'Continue'; + const buttonType = config['type'] || 'submit'; + const buttonVariant = config['variant']?.toLowerCase() === 'primary' ? 'solid' : 'outline'; + + return ( + + ); +}; + +export default SubmitButton; diff --git a/packages/react/src/components/presentation/SignUp/options/TextInput.tsx b/packages/react/src/components/presentation/SignUp/options/TextInput.tsx new file mode 100644 index 000000000..e8e792362 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/TextInput.tsx @@ -0,0 +1,53 @@ +/** + * 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 {FC} from 'react'; +import {FieldType} from '@asgardeo/browser'; +import {createField} from '../../../factories/FieldFactory'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; + +/** + * Text input component for sign-up forms. + */ +const TextInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return createField({ + type: FieldType.Text, + name: fieldName, + label: config['label'] || '', + placeholder: config['placeholder'] || '', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default TextInput; diff --git a/packages/react/src/components/presentation/SignUp/options/Typography.tsx b/packages/react/src/components/presentation/SignUp/options/Typography.tsx new file mode 100644 index 000000000..566648d56 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/Typography.tsx @@ -0,0 +1,76 @@ +/** + * 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 {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import Typography from '../../../primitives/Typography/Typography'; + +/** + * Typography component for sign-up forms (titles, descriptions, etc.). + */ +const TypographyComponent: FC = ({component}) => { + const config = component.config || {}; + const text = config['text'] || config['content'] || ''; + const variant = component.variant?.toLowerCase() || 'body1'; + + // Map component variants to Typography variants + let typographyVariant: any = 'body1'; + + switch (variant) { + case 'h1': + typographyVariant = 'h1'; + break; + case 'h2': + typographyVariant = 'h2'; + break; + case 'h3': + typographyVariant = 'h3'; + break; + case 'h4': + typographyVariant = 'h4'; + break; + case 'h5': + typographyVariant = 'h5'; + break; + case 'h6': + typographyVariant = 'h6'; + break; + case 'subtitle1': + typographyVariant = 'subtitle1'; + break; + case 'subtitle2': + typographyVariant = 'subtitle2'; + break; + case 'body2': + typographyVariant = 'body2'; + break; + case 'caption': + typographyVariant = 'caption'; + break; + default: + typographyVariant = 'body1'; + } + + return ( + + {text} + + ); +}; + +export default TypographyComponent; diff --git a/packages/react/src/components/presentation/SignUp/options/index.ts b/packages/react/src/components/presentation/SignUp/options/index.ts new file mode 100644 index 000000000..ae1c85f37 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/index.ts @@ -0,0 +1,31 @@ +/** + * 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. + */ + +export {default as TextInput} from './TextInput'; +export {default as EmailInput} from './EmailInput'; +export {default as PasswordInput} from './PasswordInput'; +export {default as SubmitButton} from './SubmitButton'; +export {default as Typography} from './Typography'; +export {default as FormContainer} from './FormContainer'; + +export { + createSignUpComponent, + createSignUpOptionFromComponent, + renderSignUpComponents, + type BaseSignUpOptionProps, +} from './SignUpOptionFactory'; From aa88400a5104b6114e183c4b34562b09f71fc9b0 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:26:57 +0530 Subject: [PATCH 22/96] fix: reorder import statements in EmailInput, PasswordInput, and TextInput components for consistency --- .../src/components/presentation/SignUp/options/EmailInput.tsx | 4 ++-- .../components/presentation/SignUp/options/PasswordInput.tsx | 2 +- .../src/components/presentation/SignUp/options/TextInput.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx b/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx index 1fe24075b..3f49117a3 100644 --- a/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx +++ b/packages/react/src/components/presentation/SignUp/options/EmailInput.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC} from 'react'; import {FieldType} from '@asgardeo/browser'; -import {createField} from '../../../factories/FieldFactory'; +import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; /** * Email input component for sign-up forms. diff --git a/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx b/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx index 725152252..ee4424d2c 100644 --- a/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx +++ b/packages/react/src/components/presentation/SignUp/options/PasswordInput.tsx @@ -16,8 +16,8 @@ * under the License. */ -import {FC} from 'react'; import {FieldType} from '@asgardeo/browser'; +import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; import {createField} from '../../../factories/FieldFactory'; diff --git a/packages/react/src/components/presentation/SignUp/options/TextInput.tsx b/packages/react/src/components/presentation/SignUp/options/TextInput.tsx index e8e792362..43e5887d8 100644 --- a/packages/react/src/components/presentation/SignUp/options/TextInput.tsx +++ b/packages/react/src/components/presentation/SignUp/options/TextInput.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC} from 'react'; import {FieldType} from '@asgardeo/browser'; -import {createField} from '../../../factories/FieldFactory'; +import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; /** * Text input component for sign-up forms. From dc23c0eae27df765b3a7647d52f04c397c0622ca Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:34:15 +0530 Subject: [PATCH 23/96] feat(react): add SocialButton component for sign-up options and update exports --- .../SignUp/options/SignUpOptionFactory.tsx | 11 ++- .../SignUp/options/SocialButton.tsx | 69 +++++++++++++++++++ .../presentation/SignUp/options/index.ts | 1 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/components/presentation/SignUp/options/SocialButton.tsx diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx index 0adc483c5..9f33fb723 100644 --- a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -21,6 +21,7 @@ import {ReactElement} from 'react'; import EmailInput from './EmailInput'; import FormContainer from './FormContainer'; import PasswordInput from './PasswordInput'; +import SocialButton from './SocialButton'; import SubmitButton from './SubmitButton'; import TextInput from './TextInput'; import Typography from './Typography'; @@ -74,6 +75,8 @@ export interface BaseSignUpOptionProps extends WithPreferences { */ onInputChange: (name: string, value: string) => void; + onSubmit?: (payload) => void; + /** * Component size variant. */ @@ -113,8 +116,14 @@ export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElemen } return ; - case EmbeddedFlowComponentType.Button: + case EmbeddedFlowComponentType.Button: { + const buttonVariant: string | undefined = component.variant?.toUpperCase(); + + if (buttonVariant === 'SOCIAL') { + return ; + } return ; + } case EmbeddedFlowComponentType.Form: return ; diff --git a/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx new file mode 100644 index 000000000..c51968d7a --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx @@ -0,0 +1,69 @@ +/** + * 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 {FC} from 'react'; + +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import Button from '../../../primitives/Button/Button'; + +/** + * Social button component for sign-up forms. + */ +const SocialButton: FC = ({ + component, + isLoading, + buttonClassName, + size = 'medium', + onSubmit, +}) => { + const config = component.config || {}; + const buttonText: string = config['text'] || config['label'] || 'Continue with Social'; + + const handleClick = (): void => { + if (onSubmit) { + onSubmit({ + actionId: component.id, + }); + } + }; + + return ( + + ); +}; + +export default SocialButton; diff --git a/packages/react/src/components/presentation/SignUp/options/index.ts b/packages/react/src/components/presentation/SignUp/options/index.ts index ae1c85f37..325d7e3ff 100644 --- a/packages/react/src/components/presentation/SignUp/options/index.ts +++ b/packages/react/src/components/presentation/SignUp/options/index.ts @@ -20,6 +20,7 @@ export {default as TextInput} from './TextInput'; export {default as EmailInput} from './EmailInput'; export {default as PasswordInput} from './PasswordInput'; export {default as SubmitButton} from './SubmitButton'; +export {default as SocialButton} from './SocialButton'; export {default as Typography} from './Typography'; export {default as FormContainer} from './FormContainer'; From b4225940310422c813ebdbd6ba3fca6cac8d8ddb Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:38:44 +0530 Subject: [PATCH 24/96] feat(react): add GoogleButton component and integrate it into sign-up options --- .../SignUp/options/GoogleButton.tsx | 90 +++++++++++++++++++ .../SignUp/options/SignUpOptionFactory.tsx | 6 ++ .../presentation/SignUp/options/index.ts | 1 + 3 files changed, 97 insertions(+) create mode 100644 packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx diff --git a/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx b/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx new file mode 100644 index 000000000..6ffc97705 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx @@ -0,0 +1,90 @@ +/** + * 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 {FC} from 'react'; + +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import Button from '../../../primitives/Button/Button'; + +/** + * Google Sign-Up Button Component. + * Handles registration with Google identity provider. + */ +const GoogleButton: FC = ({ + component, + isLoading, + buttonClassName = '', + size = 'medium', + onSubmit, +}) => { + const config = component.config || {}; + const buttonText: string = config['text'] || config['label'] || 'Continue with Google'; + + /** + * Handle button click. + */ + const handleClick = (): void => { + if (onSubmit) { + onSubmit({ + actionId: component.id, + }); + } + }; + + return ( + + ); +}; + +export default GoogleButton; diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx index 9f33fb723..4d31aaed5 100644 --- a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -20,6 +20,7 @@ import {EmbeddedFlowComponent, EmbeddedFlowComponentType, WithPreferences} from import {ReactElement} from 'react'; import EmailInput from './EmailInput'; import FormContainer from './FormContainer'; +import GoogleButton from './GoogleButton'; import PasswordInput from './PasswordInput'; import SocialButton from './SocialButton'; import SubmitButton from './SubmitButton'; @@ -118,8 +119,13 @@ export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElemen case EmbeddedFlowComponentType.Button: { const buttonVariant: string | undefined = component.variant?.toUpperCase(); + const buttonText: string = component.config['text'] || component.config['label'] || ''; if (buttonVariant === 'SOCIAL') { + // Check if it's a Google button based on text content + if (buttonText.toLowerCase().includes('google')) { + return ; + } return ; } return ; diff --git a/packages/react/src/components/presentation/SignUp/options/index.ts b/packages/react/src/components/presentation/SignUp/options/index.ts index 325d7e3ff..361077b89 100644 --- a/packages/react/src/components/presentation/SignUp/options/index.ts +++ b/packages/react/src/components/presentation/SignUp/options/index.ts @@ -21,6 +21,7 @@ export {default as EmailInput} from './EmailInput'; export {default as PasswordInput} from './PasswordInput'; export {default as SubmitButton} from './SubmitButton'; export {default as SocialButton} from './SocialButton'; +export {default as GoogleButton} from './GoogleButton'; export {default as Typography} from './Typography'; export {default as FormContainer} from './FormContainer'; From aa6a0596c433ef22471a4a9eb1405002ca707c10 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:51:16 +0530 Subject: [PATCH 25/96] feat(react): refactor form submission handling in sign-up components for improved structure and clarity --- .../presentation/SignUp/BaseSignUp.tsx | 53 ++++++++++--------- .../SignUp/options/FormContainer.tsx | 24 +++++++-- .../SignUp/options/GoogleButton.tsx | 4 +- .../SignUp/options/SignUpOptionFactory.tsx | 10 ++-- .../SignUp/options/SocialButton.tsx | 4 +- 5 files changed, 56 insertions(+), 39 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 5ea1aa4f4..373d52ce3 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -274,31 +274,29 @@ const BaseSignUpContent: FC = ({ ); /** - * Handle form submission. + * Handle input value changes. */ - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); + const handleInputChange = (name: string, value: string) => { + setFormValue(name, value); + setFormTouched(name, true); + }; + /** + * Handle component submission (for buttons outside forms). + */ + const handleSubmit = async (component: any, data?: Record) => { if (!currentFlow) { return; } - // Mark all fields as touched before validation - touchAllFields(); - - const validation = validateForm(); - if (!validation.isValid) { - return; - } - setIsLoading(true); setError(null); try { const payload: EmbeddedFlowExecuteRequestPayload = { flowType: currentFlow.data ? (currentFlow.data as any) : undefined, - inputs: formValues, - actionId: 'submit', // This might need to be dynamic based on the flow + inputs: data || {}, + actionId: component.id, }; const response = await onSubmit(payload); @@ -307,7 +305,6 @@ const BaseSignUpContent: FC = ({ if (response.flowStatus === EmbeddedFlowStatus.Complete) { onSuccess?.(response); - // Handle redirect if afterSignUpUrl is provided if (afterSignUpUrl) { window.location.href = afterSignUpUrl; } @@ -315,7 +312,6 @@ const BaseSignUpContent: FC = ({ } if (response.flowStatus === EmbeddedFlowStatus.Incomplete) { - // Continue with the next step setCurrentFlow(response); setupFormFields(response); } @@ -328,14 +324,6 @@ const BaseSignUpContent: FC = ({ } }; - /** - * Handle input value changes. - */ - const handleInputChange = (name: string, value: string) => { - setFormValue(name, value); - setFormTouched(name, true); - }; - // Generate CSS classes const containerClasses = clsx( [ @@ -387,11 +375,24 @@ const BaseSignUpContent: FC = ({ buttonClassName: buttonClasses, error, inputClassName: inputClasses, + onSubmit: handleSubmit, size, variant, }, ), - [formValues, touchedFields, formErrors, isFormValid, isLoading, size, variant, error, inputClasses, buttonClasses], + [ + formValues, + touchedFields, + formErrors, + isFormValid, + isLoading, + size, + variant, + error, + inputClasses, + buttonClasses, + handleSubmit, + ], ); // Initialize the flow on component mount @@ -504,9 +505,9 @@ const BaseSignUpContent: FC = ({ )} -
+
{currentFlow.data?.components && renderComponents(currentFlow.data.components)} - +
); diff --git a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx index 066a376cc..5f56aa1b8 100644 --- a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx +++ b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx @@ -25,17 +25,35 @@ import {createSignUpComponent, BaseSignUpOptionProps} from './SignUpOptionFactor const FormContainer: FC = props => { const {component} = props; - // If the form has child components, render them + // If the form has child components, render them wrapped in a form element if (component.components && component.components.length > 0) { + const handleFormSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + + // Find submit button in child components and trigger its submission + const submitButton = component.components?.find( + child => child.type === 'BUTTON' && + (child.variant === 'PRIMARY' || child.config?.['type'] === 'submit') + ); + + if (submitButton && props.onSubmit) { + props.onSubmit(submitButton, props.formValues); + } + }; + return ( -
+
{component.components.map((childComponent, index) => createSignUpComponent({ ...props, component: childComponent, }), )} -
+ ); } diff --git a/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx b/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx index 6ffc97705..c0390083a 100644 --- a/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx +++ b/packages/react/src/components/presentation/SignUp/options/GoogleButton.tsx @@ -40,9 +40,7 @@ const GoogleButton: FC = ({ */ const handleClick = (): void => { if (onSubmit) { - onSubmit({ - actionId: component.id, - }); + onSubmit(component, {}); } }; diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx index 4d31aaed5..e59a8f67b 100644 --- a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -76,7 +76,7 @@ export interface BaseSignUpOptionProps extends WithPreferences { */ onInputChange: (name: string, value: string) => void; - onSubmit?: (payload) => void; + onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; /** * Component size variant. @@ -155,18 +155,19 @@ export const createSignUpOptionFromComponent = ( error?: string | null; inputClassName?: string; key?: string | number; + onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; size?: 'small' | 'medium' | 'large'; variant?: 'default' | 'outlined' | 'filled'; }, ): ReactElement => createSignUpComponent({ component, - formValues, - touchedFields, formErrors, - isLoading, + formValues, isFormValid, + isLoading, onInputChange, + touchedFields, ...options, }); @@ -185,6 +186,7 @@ export const renderSignUpComponents = ( buttonClassName?: string; error?: string | null; inputClassName?: string; + onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; size?: 'small' | 'medium' | 'large'; variant?: 'default' | 'outlined' | 'filled'; }, diff --git a/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx index c51968d7a..80038530f 100644 --- a/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx @@ -36,9 +36,7 @@ const SocialButton: FC = ({ const handleClick = (): void => { if (onSubmit) { - onSubmit({ - actionId: component.id, - }); + onSubmit(component, {}); } }; From a9ee1661feb0f52eab10b2fe70b9c9d919566865 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 09:52:53 +0530 Subject: [PATCH 26/96] fix: simplify formatting of submit button search logic in FormContainer component --- .../components/presentation/SignUp/options/FormContainer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx index 5f56aa1b8..f24fccb1a 100644 --- a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx +++ b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx @@ -32,8 +32,7 @@ const FormContainer: FC = props => { // Find submit button in child components and trigger its submission const submitButton = component.components?.find( - child => child.type === 'BUTTON' && - (child.variant === 'PRIMARY' || child.config?.['type'] === 'submit') + child => child.type === 'BUTTON' && (child.variant === 'PRIMARY' || child.config?.['type'] === 'submit'), ); if (submitButton && props.onSubmit) { From f7f3d6f85f25f0a64bbc988c0ffe1d76d646b399 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 10:43:07 +0530 Subject: [PATCH 27/96] feat(react): update typography styles and simplify form layout in sign-up components --- .../src/components/presentation/SignIn/BaseSignIn.tsx | 2 +- .../src/components/presentation/SignUp/BaseSignUp.tsx | 6 ------ .../presentation/SignUp/options/FormContainer.tsx | 2 +- .../components/primitives/Typography/Typography.tsx | 10 +++++----- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index 6d24b3806..14aa2d5d9 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -1162,7 +1162,7 @@ const BaseSignInContent: FC = ({ return ( - {flowTitle || t('signin.title')} + {flowTitle || t('signin.title')} {flowSubtitle && ( {flowSubtitle || t('signin.subtitle')} diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 373d52ce3..d2132e938 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -475,12 +475,6 @@ const BaseSignUpContent: FC = ({ return ( - {flowTitle || t('signup.title')} - {flowSubtitle && ( - - {flowSubtitle || t('signup.subtitle')} - - )} {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message: any, index: number) => ( diff --git a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx index f24fccb1a..fbfb21380 100644 --- a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx +++ b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx @@ -44,7 +44,7 @@ const FormContainer: FC = props => {
{component.components.map((childComponent, index) => createSignUpComponent({ diff --git a/packages/react/src/components/primitives/Typography/Typography.tsx b/packages/react/src/components/primitives/Typography/Typography.tsx index bdb799e60..f386e0383 100644 --- a/packages/react/src/components/primitives/Typography/Typography.tsx +++ b/packages/react/src/components/primitives/Typography/Typography.tsx @@ -174,35 +174,35 @@ const Typography: FC = ({ case 'h1': return { fontSize: '2.125rem', // 34px - fontWeight: 400, + fontWeight: 600, lineHeight: 1.235, letterSpacing: '-0.00735em', }; case 'h2': return { fontSize: '1.5rem', // 24px - fontWeight: 400, + fontWeight: 600, lineHeight: 1.334, letterSpacing: '0em', }; case 'h3': return { fontSize: '1.25rem', // 20px - fontWeight: 400, + fontWeight: 600, lineHeight: 1.6, letterSpacing: '0.0075em', }; case 'h4': return { fontSize: '1.125rem', // 18px - fontWeight: 400, + fontWeight: 600, lineHeight: 1.5, letterSpacing: '0.00938em', }; case 'h5': return { fontSize: '1rem', // 16px - fontWeight: 400, + fontWeight: 600, lineHeight: 1.334, letterSpacing: '0em', }; From ac2d6924639b5b55af4dcd59ef7266d87580e9b0 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 10:57:32 +0530 Subject: [PATCH 28/96] feat(react): add new input components (Checkbox, Date, Number, Telephone) and update SignUpOptionFactory for integration --- .../javascript/src/models/embedded-flow.ts | 2 + .../src/components/factories/FieldFactory.tsx | 9 +++ .../SignUp/options/CheckboxInput.tsx | 53 +++++++++++++++++ .../presentation/SignUp/options/DateInput.tsx | 53 +++++++++++++++++ .../SignUp/options/DividerComponent.tsx | 42 ++++++++++++++ .../SignUp/options/ImageComponent.tsx | 58 +++++++++++++++++++ .../SignUp/options/NumberInput.tsx | 53 +++++++++++++++++ .../SignUp/options/SignUpOptionFactory.tsx | 24 ++++++++ .../SignUp/options/TelephoneInput.tsx | 56 ++++++++++++++++++ 9 files changed, 350 insertions(+) create mode 100644 packages/react/src/components/presentation/SignUp/options/CheckboxInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/DateInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/NumberInput.tsx create mode 100644 packages/react/src/components/presentation/SignUp/options/TelephoneInput.tsx diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index 7bf982b27..639600770 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -62,4 +62,6 @@ export enum EmbeddedFlowComponentType { Select = 'SELECT', Checkbox = 'CHECKBOX', Radio = 'RADIO', + Divider = 'DIVIDER', + Image = 'IMAGE', } diff --git a/packages/react/src/components/factories/FieldFactory.tsx b/packages/react/src/components/factories/FieldFactory.tsx index 6c2109c73..dbb1c728e 100644 --- a/packages/react/src/components/factories/FieldFactory.tsx +++ b/packages/react/src/components/factories/FieldFactory.tsx @@ -22,6 +22,8 @@ import Select from '../primitives/Select/Select'; import {SelectOption} from '../primitives/Select/Select'; import OtpField from '../primitives/OtpField/OtpField'; import PasswordField from '../primitives/PasswordField/PasswordField'; +import DatePicker from '../primitives/DatePicker/DatePicker'; +import Checkbox from '../primitives/Checkbox/Checkbox'; import {FieldType} from '@asgardeo/browser'; /** @@ -178,6 +180,13 @@ export const createField = (config: FieldConfig): ReactElement => { return ; case FieldType.Text: return onChange(e.target.value)} autoComplete="off" />; + case FieldType.Email: + return onChange(e.target.value)} autoComplete="email" />; + case FieldType.Date: + return onChange(e.target.value)} />; + case FieldType.Checkbox: + const isChecked = value === 'true' || (value as any) === true; + return onChange(e.target.checked.toString())} />; case FieldType.Otp: return onChange(e.target.value)} />; case FieldType.Number: diff --git a/packages/react/src/components/presentation/SignUp/options/CheckboxInput.tsx b/packages/react/src/components/presentation/SignUp/options/CheckboxInput.tsx new file mode 100644 index 000000000..5eb4164d3 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/CheckboxInput.tsx @@ -0,0 +1,53 @@ +/** + * 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 {FieldType} from '@asgardeo/browser'; +import {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; + +/** + * Checkbox input component for sign-up forms. + */ +const CheckboxInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return createField({ + type: FieldType.Checkbox, + name: fieldName, + label: config['label'] || '', + placeholder: config['placeholder'] || '', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default CheckboxInput; diff --git a/packages/react/src/components/presentation/SignUp/options/DateInput.tsx b/packages/react/src/components/presentation/SignUp/options/DateInput.tsx new file mode 100644 index 000000000..781415ce0 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/DateInput.tsx @@ -0,0 +1,53 @@ +/** + * 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 {FieldType} from '@asgardeo/browser'; +import {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; + +/** + * Date input component for sign-up forms. + */ +const DateInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return createField({ + type: FieldType.Date, + name: fieldName, + label: config['label'] || '', + placeholder: config['placeholder'] || '', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default DateInput; diff --git a/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx b/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx new file mode 100644 index 000000000..57c497116 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx @@ -0,0 +1,42 @@ +/** + * 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 {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import Divider from '../../../primitives/Divider/Divider'; + +/** + * Divider component for sign-up forms. + */ +const DividerComponent: FC = ({component}) => { + const config = component.config || {}; + const text = config['text'] || ''; + const variant = component.variant?.toLowerCase() || 'horizontal'; + + return ( + + {text} + + ); +}; + +export default DividerComponent; diff --git a/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx b/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx new file mode 100644 index 000000000..5ed54f86f --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx @@ -0,0 +1,58 @@ +/** + * 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 {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; + +/** + * Image component for sign-up forms. + */ +const ImageComponent: FC = ({component}) => { + const config = component.config || {}; + const src = config['src'] || ''; + const alt = config['alt'] || config['label'] || 'Image'; + const variant = component.variant?.toLowerCase() || 'image_block'; + + const imageStyle: React.CSSProperties = { + maxWidth: '100%', + height: 'auto', + display: 'block', + margin: variant === 'image_block' ? '1rem auto' : '0', + borderRadius: '4px', + }; + + if (!src) { + return null; + } + + return ( +
+ {alt} { + // Hide broken images + e.currentTarget.style.display = 'none'; + }} + /> +
+ ); +}; + +export default ImageComponent; diff --git a/packages/react/src/components/presentation/SignUp/options/NumberInput.tsx b/packages/react/src/components/presentation/SignUp/options/NumberInput.tsx new file mode 100644 index 000000000..f03ddbaa9 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/NumberInput.tsx @@ -0,0 +1,53 @@ +/** + * 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 {FieldType} from '@asgardeo/browser'; +import {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import {createField} from '../../../factories/FieldFactory'; + +/** + * Number input component for sign-up forms. + */ +const NumberInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return createField({ + type: FieldType.Number, + name: fieldName, + label: config['label'] || '', + placeholder: config['placeholder'] || '', + required: config['required'] || false, + value, + error, + onChange: (newValue: string) => onInputChange(fieldName, newValue), + className: inputClassName, + }); +}; + +export default NumberInput; diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx index e59a8f67b..be3119e3b 100644 --- a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -18,12 +18,18 @@ import {EmbeddedFlowComponent, EmbeddedFlowComponentType, WithPreferences} from '@asgardeo/browser'; import {ReactElement} from 'react'; +import CheckboxInput from './CheckboxInput'; +import DateInput from './DateInput'; +import DividerComponent from './DividerComponent'; import EmailInput from './EmailInput'; import FormContainer from './FormContainer'; import GoogleButton from './GoogleButton'; +import ImageComponent from './ImageComponent'; +import NumberInput from './NumberInput'; import PasswordInput from './PasswordInput'; import SocialButton from './SocialButton'; import SubmitButton from './SubmitButton'; +import TelephoneInput from './TelephoneInput'; import TextInput from './TextInput'; import Typography from './Typography'; @@ -115,6 +121,18 @@ export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElemen if (inputVariant === 'PASSWORD' || inputType === 'password') { return ; } + if (inputVariant === 'TELEPHONE' || inputType === 'tel') { + return ; + } + if (inputVariant === 'NUMBER' || inputType === 'number') { + return ; + } + if (inputVariant === 'DATE' || inputType === 'date') { + return ; + } + if (inputVariant === 'CHECKBOX' || inputType === 'checkbox') { + return ; + } return ; case EmbeddedFlowComponentType.Button: { @@ -134,6 +152,12 @@ export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElemen case EmbeddedFlowComponentType.Form: return ; + case EmbeddedFlowComponentType.Divider: + return ; + + case EmbeddedFlowComponentType.Image: + return ; + default: return
; } diff --git a/packages/react/src/components/presentation/SignUp/options/TelephoneInput.tsx b/packages/react/src/components/presentation/SignUp/options/TelephoneInput.tsx new file mode 100644 index 000000000..604241114 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/options/TelephoneInput.tsx @@ -0,0 +1,56 @@ +/** + * 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 {FC} from 'react'; +import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import TextField from '../../../primitives/TextField/TextField'; + +/** + * Telephone input component for sign-up forms. + */ +const TelephoneInput: FC = ({ + component, + formValues, + touchedFields, + formErrors, + onInputChange, + inputClassName, +}) => { + const config = component.config || {}; + const fieldName = config['identifier'] || config['name'] || component.id; + const value = formValues[fieldName] || ''; + const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + + return ( + onInputChange(fieldName, e.target.value)} + className={inputClassName} + helperText={config['hint'] || ''} + /> + ); +}; + +export default TelephoneInput; From c77dd0614b3aad582f52c2b1c7aa2a5bc553c7fa Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 11:08:09 +0530 Subject: [PATCH 29/96] feat(react): refactor button handling in sign-up components to use a generic ButtonComponent for improved variant management --- .../SignUp/options/FormContainer.tsx | 10 ++--- .../SignUp/options/SignUpOptionFactory.tsx | 16 ++++---- .../SignUp/options/SubmitButton.tsx | 39 ++++++++++++++++--- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx index fbfb21380..453a8fe8a 100644 --- a/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx +++ b/packages/react/src/components/presentation/SignUp/options/FormContainer.tsx @@ -32,7 +32,9 @@ const FormContainer: FC = props => { // Find submit button in child components and trigger its submission const submitButton = component.components?.find( - child => child.type === 'BUTTON' && (child.variant === 'PRIMARY' || child.config?.['type'] === 'submit'), + child => + child.type === 'BUTTON' && + (child.variant === 'PRIMARY' || child.variant === 'SECONDARY' || child.config?.['type'] === 'submit'), ); if (submitButton && props.onSubmit) { @@ -41,11 +43,7 @@ const FormContainer: FC = props => { }; return ( - + {component.components.map((childComponent, index) => createSignUpComponent({ ...props, diff --git a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx index be3119e3b..df22078dc 100644 --- a/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SignUpOptionFactory.tsx @@ -28,7 +28,7 @@ import ImageComponent from './ImageComponent'; import NumberInput from './NumberInput'; import PasswordInput from './PasswordInput'; import SocialButton from './SocialButton'; -import SubmitButton from './SubmitButton'; +import ButtonComponent from './SubmitButton'; import TelephoneInput from './TelephoneInput'; import TextInput from './TextInput'; import Typography from './Typography'; @@ -139,14 +139,14 @@ export const createSignUpComponent = (props: BaseSignUpOptionProps): ReactElemen const buttonVariant: string | undefined = component.variant?.toUpperCase(); const buttonText: string = component.config['text'] || component.config['label'] || ''; - if (buttonVariant === 'SOCIAL') { - // Check if it's a Google button based on text content - if (buttonText.toLowerCase().includes('google')) { - return ; - } - return ; + // TODO: The connection type should come as metadata. + if (buttonVariant === 'SOCIAL' && buttonText.toLowerCase().includes('google')) { + return ; } - return ; + + // Use the generic ButtonComponent for all other button variants + // It will handle PRIMARY, SECONDARY, TEXT, SOCIAL mappings internally + return ; } case EmbeddedFlowComponentType.Form: diff --git a/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx b/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx index 95809beb8..610d05987 100644 --- a/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SubmitButton.tsx @@ -23,27 +23,54 @@ import Button from '../../../primitives/Button/Button'; import Spinner from '../../../primitives/Spinner/Spinner'; /** - * Submit button component for sign-up forms. + * Button component for sign-up forms that handles all button variants. */ -const SubmitButton: FC = ({ +const ButtonComponent: FC = ({ component, isLoading, isFormValid, buttonClassName, + onSubmit, size = 'medium', }) => { const config = component.config || {}; const buttonText = config['text'] || config['label'] || 'Continue'; const buttonType = config['type'] || 'submit'; - const buttonVariant = config['variant']?.toLowerCase() === 'primary' ? 'solid' : 'outline'; + const componentVariant = component.variant?.toUpperCase() || 'PRIMARY'; + + // Map component variants to Button primitive props + const getButtonProps = () => { + switch (componentVariant) { + case 'PRIMARY': + return {variant: 'solid' as const, color: 'primary' as const}; + case 'SECONDARY': + return {variant: 'solid' as const, color: 'secondary' as const}; + case 'TEXT': + return {variant: 'text' as const, color: 'primary' as const}; + case 'SOCIAL': + return {variant: 'outline' as const, color: 'primary' as const}; + default: + return {variant: 'solid' as const, color: 'primary' as const}; + } + }; + + const {variant, color} = getButtonProps(); + + const handleClick = () => { + if (onSubmit && buttonType !== 'submit') { + onSubmit(component); + } + }; return ( + )} +
); } @@ -31,15 +39,21 @@ export default function PublicActions({className = '', showMobileActions = false
{/* Desktop CTA */}
- - Sign In - - - Get Started - + + {({isLoading}) => ( + + )} +
{/* Mobile CTA - shown in mobile menu */} diff --git a/samples/teamspace-react/src/pages/LandingPage.tsx b/samples/teamspace-react/src/pages/LandingPage.tsx index aa6be30e6..ff0c7b583 100644 --- a/samples/teamspace-react/src/pages/LandingPage.tsx +++ b/samples/teamspace-react/src/pages/LandingPage.tsx @@ -17,6 +17,7 @@ import { Linkedin, Mail, } from 'lucide-react'; +import {SignUpButton} from '@asgardeo/react'; export default function LandingPage() { const features = [ @@ -347,17 +348,20 @@ export default function LandingPage() { ))} - - - {plan.cta} - + + {isLoading => ( + + {isLoading ? 'Loading...' : plan.cta} + + )} +
))}
diff --git a/samples/teamspace-react/src/pages/SignIn.tsx b/samples/teamspace-react/src/pages/SignInPage.tsx similarity index 100% rename from samples/teamspace-react/src/pages/SignIn.tsx rename to samples/teamspace-react/src/pages/SignInPage.tsx From a859677e3f9d4c4ac39aa9118229723003c1d56e Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 11:36:08 +0530 Subject: [PATCH 31/96] feat(react): enable validation on change in BaseSignUp component and update useForm hook for improved form handling --- .../presentation/SignUp/BaseSignUp.tsx | 2 +- packages/react/src/hooks/useForm.ts | 240 +++++++++--------- 2 files changed, 125 insertions(+), 117 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index d2132e938..66369055a 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -232,7 +232,7 @@ const BaseSignUpContent: FC = ({ initialValues: {}, fields: formFields, validateOnBlur: true, - validateOnChange: false, + validateOnChange: true, requiredMessage: t('field.required'), }); diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index 608c2c784..8cf685092 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -22,9 +22,9 @@ import {useState, useCallback} from 'react'; * Generic form field configuration */ export interface FormField { + initialValue?: string; name: string; required?: boolean; - initialValue?: string; validator?: (value: string) => string | null; } @@ -32,38 +32,38 @@ export interface FormField { * Form validation result */ export interface ValidationResult { - isValid: boolean; errors: Record; + isValid: boolean; } /** * Configuration for the useForm hook */ export interface UseFormConfig> { - /** - * Initial form values - */ - initialValues?: Partial; /** * Form field definitions */ fields?: FormField[]; /** - * Global form validator function + * Initial form values */ - validator?: (values: T) => Record; + initialValues?: Partial; /** - * Whether to validate on change (default: false) + * Custom required field validation message */ - validateOnChange?: boolean; + requiredMessage?: string; /** * Whether to validate on blur (default: true) */ validateOnBlur?: boolean; /** - * Custom required field validation message + * Whether to validate on change (default: false) */ - requiredMessage?: string; + validateOnChange?: boolean; + /** + * Global form validator function + */ + validator?: (values: T) => Record; } /** @@ -71,85 +71,85 @@ export interface UseFormConfig> { */ export interface UseFormReturn> { /** - * Current form values - */ - values: T; - /** - * Fields that have been touched by the user + * Clear all errors */ - touched: Record; + clearErrors: () => void; /** * Current validation errors */ errors: Record; /** - * Whether the form is currently valid + * Get field props for easy integration with form components */ - isValid: boolean; + getFieldProps: (name: keyof T) => { + error: string | undefined; + name: keyof T; + onBlur: () => void; + onChange: (value: string) => void; + required: boolean; + touched: boolean; + value: string; + }; + /** + * Handle form submission + */ + handleSubmit: (onSubmit: (values: T) => void | Promise) => (e?: React.FormEvent) => Promise; /** * Whether the form has been submitted */ isSubmitted: boolean; /** - * Set a single field value + * Whether the form is currently valid */ - setValue: (name: keyof T, value: string) => void; + isValid: boolean; /** - * Set multiple field values + * Reset the form to initial values */ - setValues: (values: Partial) => void; + reset: () => void; /** - * Mark a field as touched + * Set a field error */ - setTouched: (name: keyof T, touched?: boolean) => void; + setError: (name: keyof T, error: string) => void; /** - * Mark multiple fields as touched + * Set multiple field errors */ - setTouchedFields: (touched: Partial>) => void; + setErrors: (errors: Partial>) => void; /** - * Mark all fields as touched + * Mark a field as touched */ - touchAllFields: () => void; + setTouched: (name: keyof T, touched?: boolean) => void; /** - * Set a field error + * Mark multiple fields as touched */ - setError: (name: keyof T, error: string) => void; + setTouchedFields: (touched: Partial>) => void; /** - * Set multiple field errors + * Set a single field value */ - setErrors: (errors: Partial>) => void; + setValue: (name: keyof T, value: string) => void; /** - * Clear all errors + * Set multiple field values */ - clearErrors: () => void; + setValues: (values: Partial) => void; /** - * Validate a single field + * Mark all fields as touched */ - validateField: (name: keyof T) => string | null; + touchAllFields: () => void; /** * Validate all fields */ validateForm: () => ValidationResult; /** - * Reset the form to initial values + * Current form values */ - reset: () => void; + values: T; /** - * Handle form submission + * Validate a single field */ - handleSubmit: (onSubmit: (values: T) => void | Promise) => (e?: React.FormEvent) => Promise; + validateField: (name: keyof T) => string | null; /** - * Get field props for easy integration with form components + * Fields that have been touched by the user */ - getFieldProps: (name: keyof T) => { - name: keyof T; - value: string; - onChange: (value: string) => void; - onBlur: () => void; - error: string | undefined; - touched: boolean; - required: boolean; - }; + touched: Record; } /** @@ -213,9 +213,7 @@ export const useForm = >(config: UseFormConfig< // Get field configuration by name const getFieldConfig = useCallback( - (name: keyof T): FormField | undefined => { - return fields.find(field => field.name === name); - }, + (name: keyof T): FormField | undefined => fields.find(field => field.name === name), [fields], ); @@ -274,8 +272,8 @@ export const useForm = >(config: UseFormConfig< // Set a single field value const setValue = useCallback( - (name: keyof T, value: string) => { - setFormValues(prev => ({ + (name: keyof T, value: string): void => { + setFormValues((prev: T) => ({ ...prev, [name]: value, })); @@ -283,18 +281,23 @@ export const useForm = >(config: UseFormConfig< // Validate on change if enabled if (validateOnChange) { const error = validateField(name); - setFormErrors(prev => ({ - ...prev, - [name]: error || '', - })); + setFormErrors((prev: Record) => { + const newErrors: Record = {...prev}; + if (error) { + newErrors[name] = error; + } else { + delete newErrors[name]; + } + return newErrors; + }); } }, [validateField, validateOnChange], ); // Set multiple field values - const setValues = useCallback((newValues: Partial) => { - setFormValues(prev => ({ + const setValues = useCallback((newValues: Partial): void => { + setFormValues((prev: T) => ({ ...prev, ...newValues, })); @@ -302,8 +305,8 @@ export const useForm = >(config: UseFormConfig< // Mark a field as touched const setTouched = useCallback( - (name: keyof T, isTouched: boolean = true) => { - setFormTouched(prev => ({ + (name: keyof T, isTouched: boolean = true): void => { + setFormTouched((prev: Record) => ({ ...prev, [name]: isTouched, })); @@ -311,26 +314,31 @@ export const useForm = >(config: UseFormConfig< // Validate on blur if enabled and field is touched if (validateOnBlur && isTouched) { const error = validateField(name); - setFormErrors(prev => ({ - ...prev, - [name]: error || '', - })); + setFormErrors((prev: Record) => { + const newErrors: Record = {...prev}; + if (error) { + newErrors[name] = error; + } else { + delete newErrors[name]; + } + return newErrors; + }); } }, [validateField, validateOnBlur], ); // Set multiple touched fields - const setTouchedFields = useCallback((touchedFields: Partial>) => { - setFormTouched(prev => ({ + const setTouchedFields = useCallback((touchedFields: Partial>): void => { + setFormTouched((prev: Record) => ({ ...prev, ...touchedFields, })); }, []); // Mark all fields as touched - const touchAllFields = useCallback(() => { - const allTouched = fields.reduce((acc, field) => { + const touchAllFields = useCallback((): void => { + const allTouched: Record = fields.reduce((acc: Record, field: FormField) => { acc[field.name as keyof T] = true; return acc; }, {} as Record); @@ -338,33 +346,33 @@ export const useForm = >(config: UseFormConfig< setFormTouched(allTouched); // Validate all fields - const validation = validateForm(); + const validation: ValidationResult = validateForm(); setFormErrors(validation.errors as Record); }, [fields, validateForm]); // Set a field error - const setError = useCallback((name: keyof T, error: string) => { - setFormErrors(prev => ({ + const setError = useCallback((name: keyof T, error: string): void => { + setFormErrors((prev: Record) => ({ ...prev, [name]: error, })); }, []); // Set multiple field errors - const setErrors = useCallback((newErrors: Partial>) => { - setFormErrors(prev => ({ + const setErrors = useCallback((newErrors: Partial>): void => { + setFormErrors((prev: Record) => ({ ...prev, ...newErrors, })); }, []); // Clear all errors - const clearErrors = useCallback(() => { + const clearErrors = useCallback((): void => { setFormErrors({} as Record); }, []); // Reset form to initial state - const reset = useCallback(() => { + const reset = useCallback((): void => { setFormValues({...initialValues} as T); setFormTouched({} as Record); setFormErrors({} as Record); @@ -372,63 +380,63 @@ export const useForm = >(config: UseFormConfig< }, [initialValues]); // Handle form submission - const handleSubmit = useCallback( - (onSubmit: (values: T) => void | Promise) => { - return async (e?: React.FormEvent) => { - if (e) { - e.preventDefault(); - } - - setIsSubmitted(true); - touchAllFields(); - - const validation = validateForm(); - - if (validation.isValid) { - await onSubmit(values); - } - }; - }, - [values, touchAllFields, validateForm], - ); + const handleSubmit: (onSubmit: (values: T) => void | Promise) => (e?: React.FormEvent) => Promise = + useCallback( + (onSubmit: (values: T) => void | Promise) => + async (e?: React.FormEvent): Promise => { + if (e) { + e.preventDefault(); + } + + setIsSubmitted(true); + touchAllFields(); + + const validation: ValidationResult = validateForm(); + + if (validation.isValid) { + await onSubmit(values); + } + }, + [values, touchAllFields, validateForm], + ); // Get field props for easy integration const getFieldProps = useCallback( (name: keyof T) => { - const fieldConfig = getFieldConfig(name); + const fieldConfig: FormField | undefined = getFieldConfig(name); return { - name, - value: values[name] || '', - onChange: (value: string) => setValue(name, value), - onBlur: () => setTouched(name, true), error: touched[name] ? errors[name] : undefined, - touched: touched[name] || false, + name, + onBlur: (): void => setTouched(name, true), + onChange: (value: string): void => setValue(name, value), required: fieldConfig?.required || false, + touched: touched[name] || false, + value: values[name] || '', }; }, [values, errors, touched, setValue, setTouched, getFieldConfig], ); return { - values, - touched, + clearErrors, errors, - isValid, + getFieldProps, + handleSubmit, isSubmitted, - setValue, - setValues, + isValid, + reset, + setError, + setErrors, setTouched, setTouchedFields, + setValue, + setValues, touchAllFields, - setError, - setErrors, - clearErrors, + touched, validateField, validateForm, - reset, - handleSubmit, - getFieldProps, + values, }; }; From 1f479440086fa8768e26b38b9abf48779d67d809 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 11:40:57 +0530 Subject: [PATCH 32/96] feat(react): filter out empty or undefined input values in BaseSignUp component before submission --- .../presentation/SignUp/BaseSignUp.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 66369055a..91172d2ab 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -293,11 +293,22 @@ const BaseSignUpContent: FC = ({ setError(null); try { + // Filter out empty or undefined input values + const filteredInputs: Record = {}; + if (data) { + Object.entries(data).forEach(([key, value]) => { + if (value !== null && value !== undefined && value !== '') { + filteredInputs[key] = value; + } + }); + } + const payload: EmbeddedFlowExecuteRequestPayload = { - flowType: currentFlow.data ? (currentFlow.data as any) : undefined, - inputs: data || {}, + ...(currentFlow.flowId && {flowId: currentFlow.flowId}), + flowType: (currentFlow as any).flowType || 'REGISTRATION', + inputs: filteredInputs, actionId: component.id, - }; + } as any; const response = await onSubmit(payload); onFlowChange?.(response); From bd65662820d9ee2f2cca5affc51b1f328d7a8c23 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 12:05:13 +0530 Subject: [PATCH 33/96] chore(react): update documentation comments in SignUp and BaseSignUp components for clarity --- .../javascript/src/models/embedded-flow.ts | 20 ++++++------ .../presentation/SignUp/BaseSignUp.tsx | 28 ++++++++-------- .../components/presentation/SignUp/SignUp.tsx | 32 ++++++++++++------- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index 639600770..64d67a9dc 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -27,10 +27,10 @@ export interface EmbeddedFlowExecuteRequestPayload { } export interface EmbeddedFlowExecuteResponse { + data: EmbeddedSignUpFlowData; flowId: string; flowStatus: EmbeddedFlowStatus; type: EmbeddedFlowResponseType; - data: EmbeddedSignUpFlowData; } export enum EmbeddedFlowStatus { @@ -39,29 +39,31 @@ export enum EmbeddedFlowStatus { } export enum EmbeddedFlowResponseType { + Redirection = 'REDIRECTION', View = 'VIEW', } export interface EmbeddedSignUpFlowData { - components: EmbeddedFlowComponent[]; + components?: EmbeddedFlowComponent[]; + redirectURL?: string; } export interface EmbeddedFlowComponent { + components: EmbeddedFlowComponent[]; + config: Record; id: string; type: EmbeddedFlowComponentType; variant?: string; - components: EmbeddedFlowComponent[]; - config: Record; } export enum EmbeddedFlowComponentType { - Typography = 'TYPOGRAPHY', - Form = 'FORM', Button = 'BUTTON', - Input = 'INPUT', - Select = 'SELECT', Checkbox = 'CHECKBOX', - Radio = 'RADIO', Divider = 'DIVIDER', + Form = 'FORM', Image = 'IMAGE', + Input = 'INPUT', + Radio = 'RADIO', + Select = 'SELECT', + Typography = 'TYPOGRAPHY', } diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 91172d2ab..37dbf71f8 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -98,10 +98,11 @@ export interface BaseSignUpProps { onSubmit: (payload: EmbeddedFlowExecuteRequestPayload) => Promise; /** - * Callback function called when sign-up is successful. - * @param response - The sign-up response data returned upon successful completion. + * Callback function called when the sign-up flow completes and requires redirection. + * This allows platform-specific handling of redirects (e.g., Next.js router.push). + * @param response - The response from the sign-up flow containing the redirect URL, etc. */ - onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; + onComplete?: (response: EmbeddedFlowExecuteResponse) => void; /** * Size variant for the component. @@ -135,10 +136,13 @@ export interface BaseSignUpProps { * }} * onSuccess={(response) => { * console.log('Success:', response); - * }} - * onError={(error) => { + * }} * onError={(error) => { * console.error('Error:', error); * }} + * onComplete={(redirectUrl) => { + * // Platform-specific redirect handling (e.g., Next.js router.push) + * router.push(redirectUrl); // or window.location.href = redirectUrl + * }} * className="max-w-md mx-auto" * /> * ); @@ -158,9 +162,9 @@ const BaseSignUpContent: FC = ({ afterSignUpUrl, onInitialize, onSubmit, - onSuccess, onError, onFlowChange, + onComplete, className = '', inputClassName = '', buttonClassName = '', @@ -314,11 +318,8 @@ const BaseSignUpContent: FC = ({ onFlowChange?.(response); if (response.flowStatus === EmbeddedFlowStatus.Complete) { - onSuccess?.(response); + onComplete?.(response); - if (afterSignUpUrl) { - window.location.href = afterSignUpUrl; - } return; } @@ -424,11 +425,8 @@ const BaseSignUpContent: FC = ({ onFlowChange?.(response); if (response.flowStatus === EmbeddedFlowStatus.Complete) { - onSuccess?.(response); + onComplete?.(response); - if (afterSignUpUrl) { - window.location.href = afterSignUpUrl; - } return; } @@ -450,7 +448,7 @@ const BaseSignUpContent: FC = ({ isInitialized, isFlowInitialized, onInitialize, - onSuccess, + onComplete, onError, onFlowChange, setupFormFields, diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx index 26c025d2a..90f9e4320 100644 --- a/packages/react/src/components/presentation/SignUp/SignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx @@ -16,7 +16,12 @@ * under the License. */ -import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse, EmbeddedFlowType} from '@asgardeo/browser'; +import { + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, + EmbeddedFlowResponseType, + EmbeddedFlowType, +} from '@asgardeo/browser'; import {FC} from 'react'; import BaseSignUp from './BaseSignUp'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; @@ -42,10 +47,11 @@ export interface SignUpProps { onError?: (error: Error) => void; /** - * Callback function called when sign-up is successful. - * @param response - The sign-up response data returned upon successful completion. + * Callback function called when the sign-up flow completes and requires redirection. + * This allows platform-specific handling of redirects (e.g., Next.js router.push). + * @param response - The response from the sign-up flow containing the redirect URL, etc. */ - onSuccess?: (response: EmbeddedFlowExecuteResponse) => void; + onComplete?: (response: EmbeddedFlowExecuteResponse) => void; /** * Size variant for the component. @@ -76,6 +82,10 @@ export interface SignUpProps { * onError={(error) => { * console.error('Sign-up failed:', error); * }} + * onComplete={(redirectUrl) => { + * // Platform-specific redirect handling (e.g., Next.js router.push) + * router.push(redirectUrl); // or window.location.href = redirectUrl + * }} * size="medium" * variant="outlined" * afterSignUpUrl="/welcome" @@ -89,8 +99,8 @@ const SignUp: FC = ({ size = 'medium', variant = 'default', afterSignUpUrl, - onSuccess, onError, + onComplete, }) => { const {signUp, isInitialized} = useAsgardeo(); @@ -113,13 +123,11 @@ const SignUp: FC = ({ /** * Handle successful sign-up and redirect. */ - const handleSuccess = (response: EmbeddedFlowExecuteResponse) => { - // Call the provided onSuccess callback first - onSuccess?.(response); + const handleComplete = (response: EmbeddedFlowExecuteResponse) => { + onComplete?.(response); - // Handle redirect if afterSignUpUrl is provided - if (afterSignUpUrl) { - window.location.href = afterSignUpUrl; + if (response?.type === EmbeddedFlowResponseType.Redirection && response?.data?.redirectURL) { + window.location.href = response.data.redirectURL; } }; @@ -128,8 +136,8 @@ const SignUp: FC = ({ afterSignUpUrl={afterSignUpUrl} onInitialize={handleInitialize} onSubmit={handleOnSubmit} - onSuccess={handleSuccess} onError={onError} + onComplete={handleComplete} className={className} size={size} variant={variant} From a47c831343a63819df67b36e52f0ae7a6a2d6900 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 12:20:47 +0530 Subject: [PATCH 34/96] chore(react): remove unused SignUpButton import in PublicActions component --- .../teamspace-react/src/components/header/PublicActions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/teamspace-react/src/components/header/PublicActions.tsx b/samples/teamspace-react/src/components/header/PublicActions.tsx index 115b3bbd7..bb005b6ba 100644 --- a/samples/teamspace-react/src/components/header/PublicActions.tsx +++ b/samples/teamspace-react/src/components/header/PublicActions.tsx @@ -1,7 +1,7 @@ 'use client'; -import {Link, useNavigate} from 'react-router-dom'; -import {SignInButton, SignUpButton} from '@asgardeo/react'; +import {useNavigate} from 'react-router-dom'; +import {SignInButton} from '@asgardeo/react'; import {Button} from '../ui/button'; interface PublicActionsProps { From b1ec9c024b2ead2c00b88cc254d0a3784686475e Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 13:16:36 +0530 Subject: [PATCH 35/96] feat(react): add X icon component and integrate it into Popover for close functionality --- .../src/components/primitives/Icons/X.tsx | 42 +++++++++++++++++++ .../components/primitives/Popover/Popover.tsx | 5 ++- packages/react/src/index.ts | 6 +++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/components/primitives/Icons/X.tsx diff --git a/packages/react/src/components/primitives/Icons/X.tsx b/packages/react/src/components/primitives/Icons/X.tsx new file mode 100644 index 000000000..e5d154123 --- /dev/null +++ b/packages/react/src/components/primitives/Icons/X.tsx @@ -0,0 +1,42 @@ +/** + * 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 {FC, SVGProps} from 'react'; + +/** + * X (close) icon component. + */ +const X: FC> = props => ( + + + + +); + +export default X; diff --git a/packages/react/src/components/primitives/Popover/Popover.tsx b/packages/react/src/components/primitives/Popover/Popover.tsx index 0b717f6c0..36ae12f43 100644 --- a/packages/react/src/components/primitives/Popover/Popover.tsx +++ b/packages/react/src/components/primitives/Popover/Popover.tsx @@ -39,6 +39,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import {withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; import Button from '../Button/Button'; +import {X} from '../Icons'; const useStyles = () => { const {theme, colorScheme} = useTheme(); @@ -400,7 +401,7 @@ export const PopoverHeading = React.forwardRef
); @@ -424,7 +425,7 @@ export const DialogHeading = React.forwardRef
); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0c00aac73..6f0fb1a46 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -94,6 +94,12 @@ export * from './components/control/SignedIn'; export {default as SignedOut} from './components/control/SignedOut'; export * from './components/control/SignedOut'; +export {default as Loading} from './components/control/Loading'; +export * from './components/control/Loading'; + +export {default as Loaded} from './components/control/Loaded'; +export * from './components/control/Loaded'; + export {default as BaseSignIn} from './components/presentation/SignIn/BaseSignIn'; export * from './components/presentation/SignIn/BaseSignIn'; From 2fee1d0225ba605b41dbf29ff532a12183c59afe Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 19 Jun 2025 13:16:47 +0530 Subject: [PATCH 36/96] feat(react): add Loaded and Loading components for handling loading states; update GoogleButton variant to solid; enhance DividerProps interface; add User and X icons to exports --- .../react/src/components/control/Loaded.tsx | 60 +++++++++++++++++++ .../react/src/components/control/Loading.tsx | 60 +++++++++++++++++++ .../SignIn/options/GoogleButton.tsx | 4 +- .../SignUp/options/GoogleButton.tsx | 3 +- .../components/primitives/Divider/Divider.tsx | 20 +++---- .../src/components/primitives/Icons/index.ts | 27 +++++++++ 6 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 packages/react/src/components/control/Loaded.tsx create mode 100644 packages/react/src/components/control/Loading.tsx create mode 100644 packages/react/src/components/primitives/Icons/index.ts diff --git a/packages/react/src/components/control/Loaded.tsx b/packages/react/src/components/control/Loaded.tsx new file mode 100644 index 000000000..d7e7406f6 --- /dev/null +++ b/packages/react/src/components/control/Loaded.tsx @@ -0,0 +1,60 @@ +/** + * 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 {FC, PropsWithChildren, ReactNode} from 'react'; +import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the Loaded component. + */ +export interface LoadedProps { + /** + * Content to show when the user is not signed in. + */ + fallback?: ReactNode; +} + +/** + * A component that only renders its children when the Asgardeo has finished Loading. + * + * @example + * ```tsx + * import { Loaded } from '@asgardeo/auth-react'; + * + * const App = () => { + * return ( + * Loading...

}> + *

Loaded

+ *
+ * ); + * } + * ``` + */ +const Loaded: FC> = ({children, fallback = null}: PropsWithChildren) => { + const {isLoading} = useAsgardeo(); + + if (isLoading) { + return <>{fallback}; + } + + return <>{children}; +}; + +Loaded.displayName = 'Loaded'; + +export default Loaded; diff --git a/packages/react/src/components/control/Loading.tsx b/packages/react/src/components/control/Loading.tsx new file mode 100644 index 000000000..31374c3d0 --- /dev/null +++ b/packages/react/src/components/control/Loading.tsx @@ -0,0 +1,60 @@ +/** + * 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 {FC, PropsWithChildren, ReactNode} from 'react'; +import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the Loading component. + */ +export interface LoadingProps { + /** + * Content to show when the user is not signed in. + */ + fallback?: ReactNode; +} + +/** + * A component that only renders its children when the Asgardeo is loading. + * + * @example + * ```tsx + * import { Loading } from '@asgardeo/auth-react'; + * + * const App = () => { + * return ( + * Finished Loading...

}> + *

Loading...

+ *
+ * ); + * } + * ``` + */ +const Loading: FC> = ({children, fallback = null}: PropsWithChildren) => { + const {isLoading} = useAsgardeo(); + + if (!isLoading) { + return <>{fallback}; + } + + return <>{children}; +}; + +Loading.displayName = 'Loading'; + +export default Loading; diff --git a/packages/react/src/components/presentation/SignIn/options/GoogleButton.tsx b/packages/react/src/components/presentation/SignIn/options/GoogleButton.tsx index e242e3423..ca4f60d0c 100644 --- a/packages/react/src/components/presentation/SignIn/options/GoogleButton.tsx +++ b/packages/react/src/components/presentation/SignIn/options/GoogleButton.tsx @@ -17,9 +17,9 @@ */ import {FC} from 'react'; -import Button from '../../../primitives/Button/Button'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; +import Button from '../../../primitives/Button/Button'; /** * Google Sign-In Button Component. @@ -43,7 +43,7 @@ const GoogleButton: FC = ({ return ( + ) : ( + displayValue + )} +
); } @@ -332,11 +404,44 @@ const BaseUserProfile: FC = ({ ); } // Default: view mode - const displayValue = value !== undefined && value !== null && value !== '' ? String(value) : '-'; + const hasValue = value !== undefined && value !== null && value !== ''; + const isEditable = editable && mutability !== 'READ_ONLY'; + + let displayValue: string; + if (hasValue) { + displayValue = String(value); + } else if (isEditable) { + displayValue = getFieldPlaceholder(schema); + } else { + displayValue = '-'; + } + return ( <> {label} -
{displayValue}
+
+ {!hasValue && isEditable && onStartEdit ? ( + + ) : ( + displayValue + )} +
); }; @@ -369,7 +474,7 @@ const BaseUserProfile: FC = ({ const tempEditedUser = {...editedUser}; tempEditedUser[schema.name!] = value; setEditedUser(tempEditedUser); - })} + }, () => toggleFieldEdit(schema.name!))}
{editable && schema.mutability !== 'READ_ONLY' && (
= ({ marginLeft: `${theme.spacing.unit}px`, }} > - {isFieldEditing ? ( + {isFieldEditing && ( <> - ) : ( + )} + {!isFieldEditing && hasValue && (