Skip to content

Commit eda115b

Browse files
committed
feat: Implement embedded sign-up flow; add SignUp component and related actions; enhance SignUpButton and context management
1 parent 560abda commit eda115b

File tree

15 files changed

+370
-200
lines changed

15 files changed

+370
-200
lines changed

packages/nextjs/src/AsgardeoNextClient.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getSchemas,
4040
generateFlattenedUserProfile,
4141
updateMeProfile,
42+
executeEmbeddedSignUpFlow,
4243
} from '@asgardeo/node';
4344
import {NextRequest, NextResponse} from 'next/server';
4445
import {AsgardeoNextConfig} from './models/config';
@@ -99,18 +100,6 @@ class AsgardeoNextClient<T extends AsgardeoNextConfig = AsgardeoNextConfig> exte
99100

100101
this.isInitialized = true;
101102

102-
console.log('[AsgardeoNextClient] Initializing with decorateConfigWithNextEnv:', {
103-
baseUrl,
104-
clientId,
105-
clientSecret,
106-
signInUrl,
107-
signUpUrl,
108-
afterSignInUrl,
109-
afterSignOutUrl,
110-
enablePKCE: false,
111-
...rest,
112-
});
113-
114103
const origin: string = await getClientOrigin();
115104

116105
return this.asgardeo.initialize({
@@ -314,11 +303,31 @@ class AsgardeoNextClient<T extends AsgardeoNextConfig = AsgardeoNextConfig> exte
314303
override async signUp(options?: SignUpOptions): Promise<void>;
315304
override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise<EmbeddedFlowExecuteResponse>;
316305
override async signUp(...args: any[]): Promise<void | EmbeddedFlowExecuteResponse> {
306+
if (args.length === 0) {
307+
throw new AsgardeoRuntimeError(
308+
'No arguments provided for signUp method.',
309+
'AsgardeoNextClient-ValidationError-001',
310+
'nextjs',
311+
'The signUp method requires at least one argument, either a SignUpOptions object or an EmbeddedFlowExecuteRequestPayload.',
312+
);
313+
}
314+
315+
const firstArg = args[0];
316+
317+
if (typeof firstArg === 'object' && 'flowType' in firstArg) {
318+
const configData = await this.asgardeo.getConfigData();
319+
const baseUrl = configData?.baseUrl;
320+
321+
return executeEmbeddedSignUpFlow({
322+
baseUrl,
323+
payload: firstArg as EmbeddedFlowExecuteRequestPayload,
324+
});
325+
}
317326
throw new AsgardeoRuntimeError(
318327
'Not implemented',
319-
'react-AsgardeoReactClient-ValidationError-002',
320-
'react',
321-
'The signUp method with SignUpOptions is not implemented in the React client.',
328+
'AsgardeoNextClient-ValidationError-002',
329+
'nextjs',
330+
'The signUp method with SignUpOptions is not implemented in the Next.js client.',
322331
);
323332
}
324333

packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const SignInButton = forwardRef<HTMLButtonElement, SignInButtonProps>(
8282
throw new AsgardeoRuntimeError(
8383
`Sign in failed: ${error instanceof Error ? error.message : String(error)}`,
8484
'SignInButton-handleSignIn-RuntimeError-001',
85-
'next',
85+
'nextjs',
8686
'Something went wrong while trying to sign in. Please try again later.',
8787
);
8888
} finally {

packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,108 @@
1818

1919
'use client';
2020

21-
import {FC, forwardRef, PropsWithChildren, ReactElement, Ref} from 'react';
22-
import InternalAuthAPIRoutesConfig from '../../../../configs/InternalAuthAPIRoutesConfig';
23-
import {BaseSignUpButton, BaseSignUpButtonProps} from '@asgardeo/react';
21+
import {AsgardeoRuntimeError} from '@asgardeo/node';
22+
import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react';
23+
import {BaseSignUpButton, BaseSignUpButtonProps, useTranslation} from '@asgardeo/react';
24+
import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
25+
import {useRouter} from 'next/navigation';
2426

2527
/**
26-
* Interface for SignInButton component props.
28+
* Props interface of {@link SignUpButton}
2729
*/
2830
export type SignUpButtonProps = BaseSignUpButtonProps;
2931

3032
/**
31-
* SignInButton component. This button initiates the sign-in process when clicked.
33+
* SignUpButton component that supports both render props and traditional props patterns.
34+
* It redirects the user to the Asgardeo sign-up page configured for the application.
3235
*
33-
* @example
36+
* @remarks This component is only supported in browser based React applications (CSR).
37+
*
38+
* @example Using render props pattern
39+
* ```tsx
40+
* <SignUpButton>
41+
* {({ signUp, isLoading }) => (
42+
* <button onClick={signUp} disabled={isLoading}>
43+
* {isLoading ? 'Creating Account...' : 'Create Account'}
44+
* </button>
45+
* )}
46+
* </SignUpButton>
47+
* ```
48+
*
49+
* @example Using traditional props pattern
3450
* ```tsx
35-
* import { SignInButton } from '@asgardeo/auth-react';
51+
* <SignUpButton className="custom-button">Create Account</SignUpButton>
52+
* ```
3653
*
37-
* const App = () => {
38-
* const buttonRef = useRef<HTMLButtonElement>(null);
39-
* return (
40-
* <SignInButton ref={buttonRef} className="custom-class" style={{ backgroundColor: 'blue' }}>
41-
* Sign In
42-
* </SignInButton>
43-
* );
44-
* }
54+
* @example Using component-level preferences
55+
* ```tsx
56+
* <SignUpButton
57+
* preferences={{
58+
* i18n: {
59+
* bundles: {
60+
* 'en-US': {
61+
* translations: {
62+
* 'buttons.signUp': 'Custom Sign Up Text'
63+
* }
64+
* }
65+
* }
66+
* }
67+
* }}
68+
* >
69+
* Custom Sign Up
70+
* </SignUpButton>
4571
* ```
4672
*/
47-
const SignUpButton: FC<PropsWithChildren<SignUpButtonProps>> = forwardRef<
73+
const SignUpButton: ForwardRefExoticComponent<SignUpButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
4874
HTMLButtonElement,
49-
PropsWithChildren<SignUpButtonProps>
50-
>(
51-
({className, style, ...rest}: PropsWithChildren<SignUpButtonProps>, ref: Ref<HTMLButtonElement>): ReactElement => (
52-
<form action={InternalAuthAPIRoutesConfig.signUp}>
53-
<BaseSignUpButton className={className} style={style} ref={ref} type="submit" {...rest} />
54-
</form>
55-
),
56-
);
75+
SignUpButtonProps
76+
>(({children, onClick, preferences, ...rest}: SignUpButtonProps, ref: Ref<HTMLButtonElement>): ReactElement => {
77+
const {signUp, signUpUrl} = useAsgardeo();
78+
const router = useRouter();
79+
const {t} = useTranslation(preferences?.i18n);
80+
81+
const [isLoading, setIsLoading] = useState(false);
82+
83+
const handleSignUp = async (e?: MouseEvent<HTMLButtonElement>): Promise<void> => {
84+
try {
85+
setIsLoading(true);
86+
87+
// If a custom `signUpUrl` is provided, use it for navigation.
88+
if (signUpUrl) {
89+
router.push(signUpUrl);
90+
} else {
91+
await signUp();
92+
}
93+
94+
if (onClick) {
95+
onClick(e as MouseEvent<HTMLButtonElement>);
96+
}
97+
} catch (error) {
98+
throw new AsgardeoRuntimeError(
99+
`Sign up failed: ${error instanceof Error ? error.message : String(error)}`,
100+
'SignUpButton-handleSignUp-RuntimeError-001',
101+
'nextjs',
102+
'Something went wrong while trying to sign up. Please try again later.',
103+
);
104+
} finally {
105+
setIsLoading(false);
106+
}
107+
};
108+
109+
return (
110+
<BaseSignUpButton
111+
ref={ref}
112+
onClick={handleSignUp}
113+
isLoading={isLoading}
114+
signUp={handleSignUp}
115+
preferences={preferences}
116+
{...rest}
117+
>
118+
{children ?? t('elements.buttons.signUp')}
119+
</BaseSignUpButton>
120+
);
121+
});
122+
123+
SignUpButton.displayName = 'SignUpButton';
57124

58125
export default SignUpButton;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
'use client';
20+
21+
import {
22+
EmbeddedFlowExecuteRequestPayload,
23+
EmbeddedFlowExecuteResponse,
24+
EmbeddedFlowResponseType,
25+
EmbeddedFlowType,
26+
} from '@asgardeo/node';
27+
import {FC} from 'react';
28+
import {BaseSignUp, BaseSignUpProps} from '@asgardeo/react';
29+
import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
30+
31+
/**
32+
* Props for the SignUp component.
33+
*/
34+
export type SignUpProps = BaseSignUpProps;
35+
36+
/**
37+
* A styled SignUp component that provides embedded sign-up flow with pre-built styling.
38+
* This component handles the API calls for sign-up and delegates UI logic to BaseSignUp.
39+
*
40+
* @example
41+
* ```tsx
42+
* import { SignUp } from '@asgardeo/react';
43+
*
44+
* const App = () => {
45+
* return (
46+
* <SignUp
47+
* onSuccess={(response) => {
48+
* console.log('Sign-up successful:', response);
49+
* // Handle successful sign-up (e.g., redirect, show confirmation)
50+
* }}
51+
* onError={(error) => {
52+
* console.error('Sign-up failed:', error);
53+
* }}
54+
* onComplete={(redirectUrl) => {
55+
* // Platform-specific redirect handling (e.g., Next.js router.push)
56+
* router.push(redirectUrl); // or window.location.href = redirectUrl
57+
* }}
58+
* size="medium"
59+
* variant="outlined"
60+
* afterSignUpUrl="/welcome"
61+
* />
62+
* );
63+
* };
64+
* ```
65+
*/
66+
const SignUp: FC<SignUpProps> = ({className, size = 'medium', variant = 'outlined', afterSignUpUrl, onError}) => {
67+
const {signUp, isInitialized} = useAsgardeo();
68+
69+
/**
70+
* Initialize the sign-up flow.
71+
*/
72+
const handleInitialize = async (
73+
payload?: EmbeddedFlowExecuteRequestPayload,
74+
): Promise<EmbeddedFlowExecuteResponse> => {
75+
return await signUp(
76+
payload || {
77+
flowType: EmbeddedFlowType.Registration,
78+
},
79+
);
80+
};
81+
82+
/**
83+
* Handle sign-up steps.
84+
*/
85+
const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise<EmbeddedFlowExecuteResponse> =>
86+
await signUp(payload);
87+
88+
return (
89+
<BaseSignUp
90+
afterSignUpUrl={afterSignUpUrl}
91+
onInitialize={handleInitialize}
92+
onSubmit={handleOnSubmit}
93+
onError={onError}
94+
className={className}
95+
size={size}
96+
variant={variant}
97+
isInitialized={true}
98+
/>
99+
);
100+
};
101+
102+
export default SignUp;

packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type AsgardeoContextProps = Partial<AsgardeoReactContextProps>;
3232
*/
3333
const AsgardeoContext: Context<AsgardeoContextProps | null> = createContext<null | AsgardeoContextProps>({
3434
signInUrl: undefined,
35+
signUpUrl: undefined,
3536
afterSignInUrl: undefined,
3637
baseUrl: undefined,
3738
isInitialized: false,

packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import {
2222
EmbeddedFlowExecuteRequestConfig,
23+
EmbeddedFlowExecuteRequestPayload,
2324
EmbeddedSignInFlowHandleRequestPayload,
2425
User,
2526
UserProfile,
@@ -36,6 +37,7 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
3637
Pick<AsgardeoProviderProps, 'baseUrl' | 'clientId'> & {
3738
signOut: AsgardeoContextProps['signOut'];
3839
signIn: AsgardeoContextProps['signIn'];
40+
signUp: AsgardeoContextProps['signUp'];
3941
isSignedIn: boolean;
4042
userProfile: UserProfile;
4143
user: User | null;
@@ -46,9 +48,11 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
4648
children,
4749
signIn,
4850
signOut,
51+
signUp,
4952
preferences,
5053
isSignedIn,
5154
signInUrl,
55+
signUpUrl,
5256
user,
5357
userProfile,
5458
}: PropsWithChildren<AsgardeoClientProviderProps>) => {
@@ -101,6 +105,38 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
101105
}
102106
};
103107

108+
const handleSignUp = async (
109+
payload: EmbeddedFlowExecuteRequestPayload,
110+
request: EmbeddedFlowExecuteRequestConfig,
111+
) => {
112+
console.log('[AsgardeoClientProvider] Executing sign-up action with payload', payload);
113+
try {
114+
const result = await signUp(payload, request);
115+
116+
// Redirect based flow URL is sent as `signUpUrl` in the response.
117+
if (result?.data?.signUpUrl) {
118+
router.push(result.data.signUpUrl);
119+
120+
return;
121+
}
122+
123+
// After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response.
124+
if (result?.data?.afterSignUpUrl) {
125+
router.push(result.data.afterSignUpUrl);
126+
127+
return;
128+
}
129+
130+
if (result?.error) {
131+
throw new Error(result.error);
132+
}
133+
134+
return result?.data ?? result;
135+
} catch (error) {
136+
throw error;
137+
}
138+
};
139+
104140
const handleSignOut = async () => {
105141
try {
106142
const result = await signOut();
@@ -128,9 +164,11 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
128164
isLoading,
129165
signIn: handleSignIn,
130166
signOut: handleSignOut,
167+
signUp: handleSignUp,
131168
signInUrl,
169+
signUpUrl,
132170
}),
133-
[baseUrl, user, isSignedIn, isLoading, signInUrl],
171+
[baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl],
134172
);
135173

136174
return (

0 commit comments

Comments
 (0)