Skip to content

Commit 5c87542

Browse files
desiprisgSokratisVidros
authored andcommitted
feat(clerk-js): Prefill SignIn/Up components with initial values
feat(types): Remove web3WalletAddress from SignUpInitialValues chore(repo): Changeset feat(clerk-js): Add initial values to SignUpContinue chore(clerk-js): Address PR comments chore(clerk-js): Avoid initialValues optional chaining feat(clerk-js): Include initialValues from query params in sign in/up context feat(clerk-js,clerk-react,types): Add initialValues support to redirectToSignIn/Up methods feat(clerk-react): Add initialValues to <RedirectToSignIn/> and <RedirectToSignUp/> fix(clerk-js): Use router queryString for initialValues chore(types): Remove unused RedirectToProps type refactor(clerk-js): Extract and reuse query param initial value logic fix(clerk-js): Prioritize initial values from query params fix(clerk-react): Fix initialValues type for <RedirectToSignUp/>
1 parent 01cfcdd commit 5c87542

File tree

14 files changed

+162
-59
lines changed

14 files changed

+162
-59
lines changed

.changeset/fresh-papayas-cheat.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/clerk-react': minor
4+
'@clerk/types': minor
5+
---
6+
7+
`<SignIn/>` and `<SignUp/>` input fields can now be prefilled with the `initialValues` prop.

packages/clerk-js/src/core/clerk.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ import type {
4141
Resources,
4242
SetActiveParams,
4343
SignInProps,
44+
SignInRedirectOptions,
4445
SignInResource,
4546
SignOut,
4647
SignOutCallback,
4748
SignOutOptions,
4849
SignUpField,
4950
SignUpProps,
51+
SignUpRedirectOptions,
5052
SignUpResource,
5153
UnsubscribeCallback,
5254
UserButtonProps,
@@ -58,6 +60,7 @@ import type { MountComponentRenderer } from '../ui/Components';
5860
import { completeSignUpFlow } from '../ui/components/SignUp/util';
5961
import {
6062
appendAsQueryParams,
63+
appendUrlsAsQueryParams,
6164
buildURL,
6265
createBeforeUnloadTracker,
6366
createCookieHandler,
@@ -694,11 +697,11 @@ export default class Clerk implements ClerkInterface {
694697
return setDevBrowserJWTInURL(toURL, devBrowserJwt, asQueryParam).href;
695698
}
696699

697-
public buildSignInUrl(options?: RedirectOptions): string {
700+
public buildSignInUrl(options?: SignInRedirectOptions): string {
698701
return this.#buildUrl('signInUrl', options);
699702
}
700703

701-
public buildSignUpUrl(options?: RedirectOptions): string {
704+
public buildSignUpUrl(options?: SignUpRedirectOptions): string {
702705
return this.#buildUrl('signUpUrl', options);
703706
}
704707

@@ -757,14 +760,14 @@ export default class Clerk implements ClerkInterface {
757760
return;
758761
};
759762

760-
public redirectToSignIn = async (options?: RedirectOptions): Promise<unknown> => {
763+
public redirectToSignIn = async (options?: SignInRedirectOptions): Promise<unknown> => {
761764
if (inBrowser()) {
762765
return this.navigate(this.buildSignInUrl(options));
763766
}
764767
return;
765768
};
766769

767-
public redirectToSignUp = async (options?: RedirectOptions): Promise<unknown> => {
770+
public redirectToSignUp = async (options?: SignUpRedirectOptions): Promise<unknown> => {
768771
if (inBrowser()) {
769772
return this.navigate(this.buildSignUpUrl(options));
770773
}
@@ -1456,7 +1459,7 @@ export default class Clerk implements ClerkInterface {
14561459
});
14571460
};
14581461

1459-
#buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: RedirectOptions): string => {
1462+
#buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: SignInRedirectOptions | SignUpRedirectOptions): string => {
14601463
if (!this.#isReady || !this.#environment || !this.#environment.displayConfig) {
14611464
return '';
14621465
}
@@ -1472,7 +1475,10 @@ export default class Clerk implements ClerkInterface {
14721475
{ options: this.#options, displayConfig: this.#environment.displayConfig },
14731476
false,
14741477
);
1475-
return this.buildUrlWithAuth(appendAsQueryParams(signInOrUpUrl, opts));
1478+
1479+
return this.buildUrlWithAuth(
1480+
appendUrlsAsQueryParams(appendAsQueryParams(signInOrUpUrl, options?.initialValues || {}), opts),
1481+
);
14761482
};
14771483

14781484
assertComponentsReady(controls: unknown): asserts controls is ReturnType<MountComponentRenderer> {

packages/clerk-js/src/core/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export const ERROR_CODES = {
1717
NOT_ALLOWED_ACCESS: 'not_allowed_access',
1818
SAML_USER_ATTRIBUTE_MISSING: 'saml_user_attribute_missing',
1919
};
20+
21+
export const SIGN_IN_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username'];
22+
export const SIGN_UP_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username', 'first_name', 'last_name'];

packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ClerkAPIError, SignInCreateParams } from '@clerk/types';
2-
import React, { useEffect, useMemo, useRef, useState } from 'react';
2+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
33

44
import { ERROR_CODES } from '../../../core/constants';
55
import { clerkInvalidFAPIResponse } from '../../../core/errors';
@@ -64,42 +64,73 @@ export function _SignInStart(): JSX.Element {
6464
placeholder: localizationKeys('formFieldInputPlaceholder__password') as any,
6565
});
6666

67-
const identifierField = useFormControl('identifier', '', {
67+
const ctxInitialValues = ctx.initialValues || {};
68+
const initialValues: Record<SignInStartIdentifier, string | undefined> = useMemo(
69+
() => ({
70+
email_address: ctxInitialValues.emailAddress,
71+
email_address_username: ctxInitialValues.emailAddress || ctxInitialValues.username,
72+
username: ctxInitialValues.username,
73+
phone_number: ctxInitialValues.phoneNumber,
74+
}),
75+
[ctx.initialValues],
76+
);
77+
78+
const hasSocialOrWeb3Buttons = !!authenticatableSocialStrategies.length || !!web3FirstFactors.length;
79+
const [shouldAutofocus, setShouldAutofocus] = useState(!isMobileDevice() && !hasSocialOrWeb3Buttons);
80+
const textIdentifierField = useFormControl('identifier', initialValues[identifierAttribute] || '', {
6881
...currentIdentifier,
6982
isRequired: true,
7083
});
7184

85+
const phoneIdentifierField = useFormControl('identifier', initialValues['phone_number'] || '', {
86+
...currentIdentifier,
87+
isRequired: true,
88+
});
89+
90+
const identifierField = identifierAttribute === 'phone_number' ? phoneIdentifierField : textIdentifierField;
91+
92+
const identifierFieldRef = useRef<HTMLInputElement>(null);
93+
7294
const switchToNextIdentifier = () => {
7395
setIdentifierAttribute(
7496
i => identifierAttributes[(identifierAttributes.indexOf(i) + 1) % identifierAttributes.length],
7597
);
76-
identifierField.setValue('');
98+
setShouldAutofocus(true);
7799
};
78100

79-
const switchToPhoneInput = (value?: string) => {
101+
const switchToPhoneInput = () => {
80102
setIdentifierAttribute('phone_number');
81-
identifierField.setValue(value || '');
103+
setShouldAutofocus(true);
82104
};
83105

84106
// switch to the phone input (if available) if a "+" is entered
85107
// (either by the browser or the user)
86108
// this does not work in chrome as it does not fire the change event and the value is
87109
// not available via js
88-
React.useLayoutEffect(() => {
110+
useLayoutEffect(() => {
89111
if (
90112
identifierField.value.startsWith('+') &&
91113
identifierAttributes.includes('phone_number') &&
92114
identifierAttribute !== 'phone_number' &&
93115
!hasSwitchedByAutofill
94116
) {
95-
switchToPhoneInput(identifierField.value);
117+
switchToPhoneInput();
96118
// do not switch automatically on subsequent autofills
97119
// by the browser to avoid a switch loop
98120
setHasSwitchedByAutofill(true);
99121
}
100122
}, [identifierField.value, identifierAttributes]);
101123

102-
React.useEffect(() => {
124+
useLayoutEffect(() => {
125+
if (identifierAttribute === 'phone_number' && identifierField.value) {
126+
//value should be kept as we have auto-switched to the phone input
127+
return;
128+
}
129+
130+
identifierField.setValue(initialValues[identifierAttribute] || '');
131+
}, [identifierAttribute]);
132+
133+
useEffect(() => {
103134
if (!organizationTicket) {
104135
return;
105136
}
@@ -137,7 +168,7 @@ export function _SignInStart(): JSX.Element {
137168
});
138169
}, []);
139170

140-
React.useEffect(() => {
171+
useEffect(() => {
141172
async function handleOauthError() {
142173
const error = signIn?.firstFactorVerification?.error;
143174
if (error) {
@@ -244,9 +275,6 @@ export function _SignInStart(): JSX.Element {
244275
return <LoadingCard />;
245276
}
246277

247-
const hasSocialOrWeb3Buttons = !!authenticatableSocialStrategies.length || !!web3FirstFactors.length;
248-
const shouldAutofocus = !isMobileDevice() && !hasSocialOrWeb3Buttons;
249-
250278
return (
251279
<Flow.Part part='start'>
252280
<Card>
@@ -271,6 +299,7 @@ export function _SignInStart(): JSX.Element {
271299
<Form.Root onSubmit={handleFirstPartySubmit}>
272300
<Form.ControlRow elementId={identifierField.id}>
273301
<Form.Control
302+
ref={identifierFieldRef}
274303
actionLabel={nextIdentifier?.action}
275304
onActionClicked={switchToNextIdentifier}
276305
{...identifierField.props}
@@ -303,7 +332,7 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> })
303332
const ref = useRef<HTMLInputElement>(null);
304333

305334
// show password if it's autofilled by the browser
306-
React.useLayoutEffect(() => {
335+
useLayoutEffect(() => {
307336
const intervalId = setInterval(() => {
308337
if (ref?.current) {
309338
const autofilled = window.getComputedStyle(ref.current, ':autofill').animationName === 'onAutoFillStart';

packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function _SignUpContinue() {
3232
const { navigate } = useRouter();
3333
const { displayConfig, userSettings } = useEnvironment();
3434
const { attributes } = userSettings;
35-
const { navigateAfterSignUp, signInUrl, unsafeMetadata } = useSignUpContext();
35+
const { navigateAfterSignUp, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext();
3636
const signUp = useCoreSignUp();
3737
const isProgressiveSignUp = userSettings.signUp.progressive;
3838
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState<ActiveIdentifier>(
@@ -47,27 +47,27 @@ function _SignUpContinue() {
4747

4848
// TODO: This form should be shared between SignUpStart and SignUpContinue
4949
const formState = {
50-
firstName: useFormControl('firstName', '', {
50+
firstName: useFormControl('firstName', initialValues.firstName || '', {
5151
type: 'text',
5252
label: localizationKeys('formFieldLabel__firstName'),
5353
placeholder: localizationKeys('formFieldInputPlaceholder__firstName'),
5454
}),
55-
lastName: useFormControl('lastName', '', {
55+
lastName: useFormControl('lastName', initialValues.lastName || '', {
5656
type: 'text',
5757
label: localizationKeys('formFieldLabel__lastName'),
5858
placeholder: localizationKeys('formFieldInputPlaceholder__lastName'),
5959
}),
60-
emailAddress: useFormControl('emailAddress', '', {
60+
emailAddress: useFormControl('emailAddress', initialValues.emailAddress || '', {
6161
type: 'email',
6262
label: localizationKeys('formFieldLabel__emailAddress'),
6363
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'),
6464
}),
65-
username: useFormControl('username', '', {
65+
username: useFormControl('username', initialValues.username || '', {
6666
type: 'text',
6767
label: localizationKeys('formFieldLabel__username'),
6868
placeholder: localizationKeys('formFieldInputPlaceholder__username'),
6969
}),
70-
phoneNumber: useFormControl('phoneNumber', '', {
70+
phoneNumber: useFormControl('phoneNumber', initialValues.phoneNumber || '', {
7171
type: 'tel',
7272
label: localizationKeys('formFieldLabel__phoneNumber'),
7373
placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'),

packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function _SignUpStart(): JSX.Element {
4242
getInitialActiveIdentifier(attributes, userSettings.signUp.progressive),
4343
);
4444
const { t, locale } = useLocalizations();
45+
const initialValues = ctx.initialValues || {};
4546

4647
const [missingRequirementsWithTicket, setMissingRequirementsWithTicket] = React.useState(false);
4748

@@ -51,27 +52,27 @@ function _SignUpStart(): JSX.Element {
5152
const { failedValidationsText } = usePasswordComplexity(passwordSettings);
5253

5354
const formState = {
54-
firstName: useFormControl('firstName', signUp.firstName || '', {
55+
firstName: useFormControl('firstName', signUp.firstName || initialValues.firstName || '', {
5556
type: 'text',
5657
label: localizationKeys('formFieldLabel__firstName'),
5758
placeholder: localizationKeys('formFieldInputPlaceholder__firstName'),
5859
}),
59-
lastName: useFormControl('lastName', signUp.lastName || '', {
60+
lastName: useFormControl('lastName', signUp.lastName || initialValues.lastName || '', {
6061
type: 'text',
6162
label: localizationKeys('formFieldLabel__lastName'),
6263
placeholder: localizationKeys('formFieldInputPlaceholder__lastName'),
6364
}),
64-
emailAddress: useFormControl('emailAddress', signUp.emailAddress || '', {
65+
emailAddress: useFormControl('emailAddress', signUp.emailAddress || initialValues.emailAddress || '', {
6566
type: 'email',
6667
label: localizationKeys('formFieldLabel__emailAddress'),
6768
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'),
6869
}),
69-
username: useFormControl('username', signUp.username || '', {
70+
username: useFormControl('username', signUp.username || initialValues.username || '', {
7071
type: 'text',
7172
label: localizationKeys('formFieldLabel__username'),
7273
placeholder: localizationKeys('formFieldInputPlaceholder__username'),
7374
}),
74-
phoneNumber: useFormControl('phoneNumber', signUp.phoneNumber || '', {
75+
phoneNumber: useFormControl('phoneNumber', signUp.phoneNumber || initialValues.phoneNumber || '', {
7576
type: 'tel',
7677
label: localizationKeys('formFieldLabel__phoneNumber'),
7778
placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'),

packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { snakeToCamel } from '@clerk/shared';
12
import type { OrganizationResource, UserResource } from '@clerk/types';
2-
import React from 'react';
3+
import React, { useMemo } from 'react';
34

5+
import { SIGN_IN_INITIAL_VALUE_KEYS, SIGN_UP_INITIAL_VALUE_KEYS } from '../../core/constants';
46
import { buildAuthQueryString, buildURL, createDynamicParamParser, pickRedirectionProp } from '../../utils';
57
import { useCoreClerk, useEnvironment, useOptions } from '../contexts';
68
import type { ParsedQs } from '../router';
@@ -21,6 +23,18 @@ const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ });
2123

2224
export const ComponentContext = React.createContext<AvailableComponentCtx | null>(null);
2325

26+
const getInitialValuesFromQueryParams = (queryString: string, params: string[]) => {
27+
const props: Record<string, string> = {};
28+
const searchParams = new URLSearchParams(queryString);
29+
searchParams.forEach((value, key) => {
30+
if (params.includes(key) && typeof value === 'string') {
31+
props[snakeToCamel(key)] = value;
32+
}
33+
});
34+
35+
return props;
36+
};
37+
2438
export type SignUpContextType = SignUpCtx & {
2539
navigateAfterSignUp: () => any;
2640
queryParams: ParsedQs;
@@ -33,10 +47,15 @@ export const useSignUpContext = (): SignUpContextType => {
3347
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignUpCtx;
3448
const { navigate } = useRouter();
3549
const { displayConfig } = useEnvironment();
36-
const { queryParams } = useRouter();
50+
const { queryParams, queryString } = useRouter();
3751
const options = useOptions();
3852
const clerk = useCoreClerk();
3953

54+
const initialValuesFromQueryParams = useMemo(
55+
() => getInitialValuesFromQueryParams(queryString, SIGN_UP_INITIAL_VALUE_KEYS),
56+
[],
57+
);
58+
4059
if (componentName !== 'SignUp') {
4160
throw new Error('Clerk: useSignUpContext called outside of the mounted SignUp component.');
4261
}
@@ -87,6 +106,7 @@ export const useSignUpContext = (): SignUpContextType => {
87106
afterSignInUrl,
88107
navigateAfterSignUp,
89108
queryParams,
109+
initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
90110
authQueryString: authQs,
91111
};
92112
};
@@ -103,10 +123,15 @@ export const useSignInContext = (): SignInContextType => {
103123
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignInCtx;
104124
const { navigate } = useRouter();
105125
const { displayConfig } = useEnvironment();
106-
const { queryParams } = useRouter();
126+
const { queryParams, queryString } = useRouter();
107127
const options = useOptions();
108128
const clerk = useCoreClerk();
109129

130+
const initialValuesFromQueryParams = useMemo(
131+
() => getInitialValuesFromQueryParams(queryString, SIGN_IN_INITIAL_VALUE_KEYS),
132+
[],
133+
);
134+
110135
if (componentName !== 'SignIn') {
111136
throw new Error('Clerk: useSignInContext called outside of the mounted SignIn component.');
112137
}
@@ -154,6 +179,7 @@ export const useSignInContext = (): SignInContextType => {
154179
navigateAfterSignIn,
155180
signUpContinueUrl,
156181
queryParams,
182+
initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
157183
authQueryString: authQs,
158184
};
159185
};

0 commit comments

Comments
 (0)