diff --git a/.changeset/fresh-papayas-cheat.md b/.changeset/fresh-papayas-cheat.md
new file mode 100644
index 00000000000..61f3f389556
--- /dev/null
+++ b/.changeset/fresh-papayas-cheat.md
@@ -0,0 +1,7 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/clerk-react': minor
+'@clerk/types': minor
+---
+
+``, ``, ``, ``, `clerk.redirectToSignIn()` and `clerk.redirectToSignUp()` now accept the `initialValues` option, which will prefill the appropriate form fields with the values provided.
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 89727469a14..5e2c652b588 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -41,12 +41,14 @@ import type {
Resources,
SetActiveParams,
SignInProps,
+ SignInRedirectOptions,
SignInResource,
SignOut,
SignOutCallback,
SignOutOptions,
SignUpField,
SignUpProps,
+ SignUpRedirectOptions,
SignUpResource,
UnsubscribeCallback,
UserButtonProps,
@@ -58,6 +60,7 @@ import type { MountComponentRenderer } from '../ui/Components';
import { completeSignUpFlow } from '../ui/components/SignUp/util';
import {
appendAsQueryParams,
+ appendUrlsAsQueryParams,
buildURL,
createBeforeUnloadTracker,
createCookieHandler,
@@ -694,11 +697,11 @@ export default class Clerk implements ClerkInterface {
return setDevBrowserJWTInURL(toURL, devBrowserJwt, asQueryParam).href;
}
- public buildSignInUrl(options?: RedirectOptions): string {
+ public buildSignInUrl(options?: SignInRedirectOptions): string {
return this.#buildUrl('signInUrl', options);
}
- public buildSignUpUrl(options?: RedirectOptions): string {
+ public buildSignUpUrl(options?: SignUpRedirectOptions): string {
return this.#buildUrl('signUpUrl', options);
}
@@ -757,14 +760,14 @@ export default class Clerk implements ClerkInterface {
return;
};
- public redirectToSignIn = async (options?: RedirectOptions): Promise => {
+ public redirectToSignIn = async (options?: SignInRedirectOptions): Promise => {
if (inBrowser()) {
return this.navigate(this.buildSignInUrl(options));
}
return;
};
- public redirectToSignUp = async (options?: RedirectOptions): Promise => {
+ public redirectToSignUp = async (options?: SignUpRedirectOptions): Promise => {
if (inBrowser()) {
return this.navigate(this.buildSignUpUrl(options));
}
@@ -1456,7 +1459,7 @@ export default class Clerk implements ClerkInterface {
});
};
- #buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: RedirectOptions): string => {
+ #buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: SignInRedirectOptions | SignUpRedirectOptions): string => {
if (!this.#isReady || !this.#environment || !this.#environment.displayConfig) {
return '';
}
@@ -1472,7 +1475,10 @@ export default class Clerk implements ClerkInterface {
{ options: this.#options, displayConfig: this.#environment.displayConfig },
false,
);
- return this.buildUrlWithAuth(appendAsQueryParams(signInOrUpUrl, opts));
+
+ return this.buildUrlWithAuth(
+ appendUrlsAsQueryParams(appendAsQueryParams(signInOrUpUrl, options?.initialValues || {}), opts),
+ );
};
assertComponentsReady(controls: unknown): asserts controls is ReturnType {
diff --git a/packages/clerk-js/src/core/constants.ts b/packages/clerk-js/src/core/constants.ts
index 17df8558c34..ffa65e9171c 100644
--- a/packages/clerk-js/src/core/constants.ts
+++ b/packages/clerk-js/src/core/constants.ts
@@ -17,3 +17,6 @@ export const ERROR_CODES = {
NOT_ALLOWED_ACCESS: 'not_allowed_access',
SAML_USER_ATTRIBUTE_MISSING: 'saml_user_attribute_missing',
};
+
+export const SIGN_IN_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username'];
+export const SIGN_UP_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username', 'first_name', 'last_name'];
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
index 865239bab18..5b893c9b6a1 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
@@ -1,5 +1,5 @@
import type { ClerkAPIError, SignInCreateParams } from '@clerk/types';
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ERROR_CODES } from '../../../core/constants';
import { clerkInvalidFAPIResponse } from '../../../core/errors';
@@ -64,42 +64,73 @@ export function _SignInStart(): JSX.Element {
placeholder: localizationKeys('formFieldInputPlaceholder__password') as any,
});
- const identifierField = useFormControl('identifier', '', {
+ const ctxInitialValues = ctx.initialValues || {};
+ const initialValues: Record = useMemo(
+ () => ({
+ email_address: ctxInitialValues.emailAddress,
+ email_address_username: ctxInitialValues.emailAddress || ctxInitialValues.username,
+ username: ctxInitialValues.username,
+ phone_number: ctxInitialValues.phoneNumber,
+ }),
+ [ctx.initialValues],
+ );
+
+ const hasSocialOrWeb3Buttons = !!authenticatableSocialStrategies.length || !!web3FirstFactors.length;
+ const [shouldAutofocus, setShouldAutofocus] = useState(!isMobileDevice() && !hasSocialOrWeb3Buttons);
+ const textIdentifierField = useFormControl('identifier', initialValues[identifierAttribute] || '', {
...currentIdentifier,
isRequired: true,
});
+ const phoneIdentifierField = useFormControl('identifier', initialValues['phone_number'] || '', {
+ ...currentIdentifier,
+ isRequired: true,
+ });
+
+ const identifierField = identifierAttribute === 'phone_number' ? phoneIdentifierField : textIdentifierField;
+
+ const identifierFieldRef = useRef(null);
+
const switchToNextIdentifier = () => {
setIdentifierAttribute(
i => identifierAttributes[(identifierAttributes.indexOf(i) + 1) % identifierAttributes.length],
);
- identifierField.setValue('');
+ setShouldAutofocus(true);
};
- const switchToPhoneInput = (value?: string) => {
+ const switchToPhoneInput = () => {
setIdentifierAttribute('phone_number');
- identifierField.setValue(value || '');
+ setShouldAutofocus(true);
};
// switch to the phone input (if available) if a "+" is entered
// (either by the browser or the user)
// this does not work in chrome as it does not fire the change event and the value is
// not available via js
- React.useLayoutEffect(() => {
+ useLayoutEffect(() => {
if (
identifierField.value.startsWith('+') &&
identifierAttributes.includes('phone_number') &&
identifierAttribute !== 'phone_number' &&
!hasSwitchedByAutofill
) {
- switchToPhoneInput(identifierField.value);
+ switchToPhoneInput();
// do not switch automatically on subsequent autofills
// by the browser to avoid a switch loop
setHasSwitchedByAutofill(true);
}
}, [identifierField.value, identifierAttributes]);
- React.useEffect(() => {
+ useLayoutEffect(() => {
+ if (identifierAttribute === 'phone_number' && identifierField.value) {
+ //value should be kept as we have auto-switched to the phone input
+ return;
+ }
+
+ identifierField.setValue(initialValues[identifierAttribute] || '');
+ }, [identifierAttribute]);
+
+ useEffect(() => {
if (!organizationTicket) {
return;
}
@@ -137,7 +168,7 @@ export function _SignInStart(): JSX.Element {
});
}, []);
- React.useEffect(() => {
+ useEffect(() => {
async function handleOauthError() {
const error = signIn?.firstFactorVerification?.error;
if (error) {
@@ -244,9 +275,6 @@ export function _SignInStart(): JSX.Element {
return ;
}
- const hasSocialOrWeb3Buttons = !!authenticatableSocialStrategies.length || !!web3FirstFactors.length;
- const shouldAutofocus = !isMobileDevice() && !hasSocialOrWeb3Buttons;
-
return (
@@ -271,6 +299,7 @@ export function _SignInStart(): JSX.Element {
})
const ref = useRef(null);
// show password if it's autofilled by the browser
- React.useLayoutEffect(() => {
+ useLayoutEffect(() => {
const intervalId = setInterval(() => {
if (ref?.current) {
const autofilled = window.getComputedStyle(ref.current, ':autofill').animationName === 'onAutoFillStart';
diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx
index fbe391c9902..b728563281d 100644
--- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx
@@ -222,4 +222,36 @@ describe('SignInStart', () => {
expect(screen.getByRole('textbox', { name: /phone number/i })).toHaveAttribute('type', 'tel');
});
});
+
+ describe('initialValues', () => {
+ it('prefills the emailAddress field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withEmailAddress();
+ });
+ props.setProps({ initialValues: { emailAddress: 'foo@clerk.com' } });
+
+ render(, { wrapper });
+ screen.getByDisplayValue(/foo@clerk.com/i);
+ });
+
+ it('prefills the phoneNumber field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withPhoneNumber();
+ });
+ props.setProps({ initialValues: { phoneNumber: '+306911111111' } });
+
+ render(, { wrapper });
+ screen.getByDisplayValue(/691 1111111/i);
+ });
+
+ it('prefills the username field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withUsername();
+ });
+
+ props.setProps({ initialValues: { username: 'foo' } });
+ render(, { wrapper });
+ screen.getByDisplayValue(/foo/i);
+ });
+ });
});
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
index 135f286dd48..ff08f7685b3 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
@@ -32,7 +32,7 @@ function _SignUpContinue() {
const { navigate } = useRouter();
const { displayConfig, userSettings } = useEnvironment();
const { attributes } = userSettings;
- const { navigateAfterSignUp, signInUrl, unsafeMetadata } = useSignUpContext();
+ const { navigateAfterSignUp, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext();
const signUp = useCoreSignUp();
const isProgressiveSignUp = userSettings.signUp.progressive;
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState(
@@ -47,27 +47,27 @@ function _SignUpContinue() {
// TODO: This form should be shared between SignUpStart and SignUpContinue
const formState = {
- firstName: useFormControl('firstName', '', {
+ firstName: useFormControl('firstName', initialValues.firstName || '', {
type: 'text',
label: localizationKeys('formFieldLabel__firstName'),
placeholder: localizationKeys('formFieldInputPlaceholder__firstName'),
}),
- lastName: useFormControl('lastName', '', {
+ lastName: useFormControl('lastName', initialValues.lastName || '', {
type: 'text',
label: localizationKeys('formFieldLabel__lastName'),
placeholder: localizationKeys('formFieldInputPlaceholder__lastName'),
}),
- emailAddress: useFormControl('emailAddress', '', {
+ emailAddress: useFormControl('emailAddress', initialValues.emailAddress || '', {
type: 'email',
label: localizationKeys('formFieldLabel__emailAddress'),
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'),
}),
- username: useFormControl('username', '', {
+ username: useFormControl('username', initialValues.username || '', {
type: 'text',
label: localizationKeys('formFieldLabel__username'),
placeholder: localizationKeys('formFieldInputPlaceholder__username'),
}),
- phoneNumber: useFormControl('phoneNumber', '', {
+ phoneNumber: useFormControl('phoneNumber', initialValues.phoneNumber || '', {
type: 'tel',
label: localizationKeys('formFieldLabel__phoneNumber'),
placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'),
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
index eff57d14705..526e03815be 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
@@ -42,6 +42,7 @@ function _SignUpStart(): JSX.Element {
getInitialActiveIdentifier(attributes, userSettings.signUp.progressive),
);
const { t, locale } = useLocalizations();
+ const initialValues = ctx.initialValues || {};
const [missingRequirementsWithTicket, setMissingRequirementsWithTicket] = React.useState(false);
@@ -51,27 +52,27 @@ function _SignUpStart(): JSX.Element {
const { failedValidationsText } = usePasswordComplexity(passwordSettings);
const formState = {
- firstName: useFormControl('firstName', signUp.firstName || '', {
+ firstName: useFormControl('firstName', signUp.firstName || initialValues.firstName || '', {
type: 'text',
label: localizationKeys('formFieldLabel__firstName'),
placeholder: localizationKeys('formFieldInputPlaceholder__firstName'),
}),
- lastName: useFormControl('lastName', signUp.lastName || '', {
+ lastName: useFormControl('lastName', signUp.lastName || initialValues.lastName || '', {
type: 'text',
label: localizationKeys('formFieldLabel__lastName'),
placeholder: localizationKeys('formFieldInputPlaceholder__lastName'),
}),
- emailAddress: useFormControl('emailAddress', signUp.emailAddress || '', {
+ emailAddress: useFormControl('emailAddress', signUp.emailAddress || initialValues.emailAddress || '', {
type: 'email',
label: localizationKeys('formFieldLabel__emailAddress'),
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'),
}),
- username: useFormControl('username', signUp.username || '', {
+ username: useFormControl('username', signUp.username || initialValues.username || '', {
type: 'text',
label: localizationKeys('formFieldLabel__username'),
placeholder: localizationKeys('formFieldInputPlaceholder__username'),
}),
- phoneNumber: useFormControl('phoneNumber', signUp.phoneNumber || '', {
+ phoneNumber: useFormControl('phoneNumber', signUp.phoneNumber || initialValues.phoneNumber || '', {
type: 'tel',
label: localizationKeys('formFieldLabel__phoneNumber'),
placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'),
diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx
index 5b24d443dba..9a2de2d257e 100644
--- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx
@@ -167,4 +167,36 @@ describe('SignUpStart', () => {
expect(screen.getByRole('textbox', { name: 'Phone number' })).toHaveValue('(123) 456-789');
});
});
+
+ describe('initialValues', () => {
+ it('prefills the emailAddress field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withEmailAddress();
+ });
+ props.setProps({ initialValues: { emailAddress: 'foo@clerk.com' } });
+
+ render(, { wrapper });
+ screen.getByDisplayValue(/foo@clerk.com/i);
+ });
+
+ it('prefills the phoneNumber field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withPhoneNumber();
+ });
+ props.setProps({ initialValues: { phoneNumber: '+306911111111' } });
+
+ render(, { wrapper });
+ screen.getByDisplayValue(/691 1111111/i);
+ });
+
+ it('prefills the username field with the correct initial value', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withUsername();
+ });
+
+ props.setProps({ initialValues: { username: 'foo' } });
+ render(, { wrapper });
+ screen.getByDisplayValue(/foo/i);
+ });
+ });
});
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index e5b9f0c36bd..98ed7863ed2 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -1,6 +1,8 @@
+import { snakeToCamel } from '@clerk/shared';
import type { OrganizationResource, UserResource } from '@clerk/types';
-import React from 'react';
+import React, { useMemo } from 'react';
+import { SIGN_IN_INITIAL_VALUE_KEYS, SIGN_UP_INITIAL_VALUE_KEYS } from '../../core/constants';
import { buildAuthQueryString, buildURL, createDynamicParamParser, pickRedirectionProp } from '../../utils';
import { useCoreClerk, useEnvironment, useOptions } from '../contexts';
import type { ParsedQs } from '../router';
@@ -21,6 +23,18 @@ const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ });
export const ComponentContext = React.createContext(null);
+const getInitialValuesFromQueryParams = (queryString: string, params: string[]) => {
+ const props: Record = {};
+ const searchParams = new URLSearchParams(queryString);
+ searchParams.forEach((value, key) => {
+ if (params.includes(key) && typeof value === 'string') {
+ props[snakeToCamel(key)] = value;
+ }
+ });
+
+ return props;
+};
+
export type SignUpContextType = SignUpCtx & {
navigateAfterSignUp: () => any;
queryParams: ParsedQs;
@@ -33,10 +47,15 @@ export const useSignUpContext = (): SignUpContextType => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignUpCtx;
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();
- const { queryParams } = useRouter();
+ const { queryParams, queryString } = useRouter();
const options = useOptions();
const clerk = useCoreClerk();
+ const initialValuesFromQueryParams = useMemo(
+ () => getInitialValuesFromQueryParams(queryString, SIGN_UP_INITIAL_VALUE_KEYS),
+ [],
+ );
+
if (componentName !== 'SignUp') {
throw new Error('Clerk: useSignUpContext called outside of the mounted SignUp component.');
}
@@ -87,6 +106,7 @@ export const useSignUpContext = (): SignUpContextType => {
afterSignInUrl,
navigateAfterSignUp,
queryParams,
+ initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
authQueryString: authQs,
};
};
@@ -103,10 +123,15 @@ export const useSignInContext = (): SignInContextType => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignInCtx;
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();
- const { queryParams } = useRouter();
+ const { queryParams, queryString } = useRouter();
const options = useOptions();
const clerk = useCoreClerk();
+ const initialValuesFromQueryParams = useMemo(
+ () => getInitialValuesFromQueryParams(queryString, SIGN_IN_INITIAL_VALUE_KEYS),
+ [],
+ );
+
if (componentName !== 'SignIn') {
throw new Error('Clerk: useSignInContext called outside of the mounted SignIn component.');
}
@@ -154,6 +179,7 @@ export const useSignInContext = (): SignInContextType => {
navigateAfterSignIn,
signUpContinueUrl,
queryParams,
+ initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
authQueryString: authQs,
};
};
diff --git a/packages/clerk-js/src/ui/elements/PhoneInput/PhoneInput.tsx b/packages/clerk-js/src/ui/elements/PhoneInput/PhoneInput.tsx
index 5077d0a3ecd..fda71753ac5 100644
--- a/packages/clerk-js/src/ui/elements/PhoneInput/PhoneInput.tsx
+++ b/packages/clerk-js/src/ui/elements/PhoneInput/PhoneInput.tsx
@@ -1,4 +1,4 @@
-import React, { forwardRef, useLayoutEffect, useState } from 'react';
+import { forwardRef, memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCoreClerk } from '../../contexts';
import { descriptors, Flex, Input, Text } from '../../customizables';
@@ -24,7 +24,7 @@ type PhoneInputProps = PropsOfComponent & { locationBasedCountryIs
const PhoneInputBase = forwardRef((props, ref) => {
const { onChange: onChangeProp, value, locationBasedCountryIso, sx, ...rest } = props;
- const phoneInputRef = React.useRef(null);
+ const phoneInputRef = useRef(null);
const { setNumber, setIso, setNumberAndIso, numberWithCode, iso, formattedNumber } = useFormattedPhoneNumber({
initPhoneWithCode: value as string,
locationBasedCountryIso,
@@ -37,11 +37,11 @@ const PhoneInputBase = forwardRef((props, ref
onChangeProp?.({ target: { value: numberWithCode } } as any);
};
- const selectedCountryOption = React.useMemo(() => {
+ const selectedCountryOption = useMemo(() => {
return countryOptions.find(o => o.country.iso === iso) || countryOptions[0];
}, [iso]);
- React.useEffect(callOnChangeProp, [numberWithCode]);
+ useEffect(callOnChangeProp, [numberWithCode]);
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
@@ -153,7 +153,7 @@ const PhoneInputBase = forwardRef((props, ref
type CountryCodeListItemProps = PropsOfComponent & {
country: CountryEntry;
};
-const CountryCodeListItem = React.memo((props: CountryCodeListItemProps) => {
+const CountryCodeListItem = memo((props: CountryCodeListItemProps) => {
const { country, sx, ...rest } = props;
return (
{
describe('appendQueryParams(base,url)', () => {
it('returns the same url if no params provided', () => {
const base = new URL('https://dashboard.clerk.com');
- const res = appendAsQueryParams(base);
+ const res = appendUrlsAsQueryParams(base);
expect(res).toBe('https://dashboard.clerk.com/');
});
it('handles URL objects', () => {
const base = new URL('https://dashboard.clerk.com');
const url = new URL('https://dashboard.clerk.com/applications/appid/instances/');
- const res = appendAsQueryParams(base, { redirect_url: url });
+ const res = appendUrlsAsQueryParams(base, { redirect_url: url });
expect(res).toBe('https://dashboard.clerk.com/#/?redirect_url=%2Fapplications%2Fappid%2Finstances%2F');
});
it('handles plain strings', () => {
const base = 'https://dashboard.clerk.com';
const url = 'https://dashboard.clerk.com/applications/appid/instances/';
- const res = appendAsQueryParams(base, { redirect_url: url });
+ const res = appendUrlsAsQueryParams(base, { redirect_url: url });
expect(res).toBe('https://dashboard.clerk.com/#/?redirect_url=%2Fapplications%2Fappid%2Finstances%2F');
});
it('handles multiple params', () => {
const base = 'https://dashboard.clerk.com';
const url = 'https://dashboard.clerk.com/applications/appid/instances/';
- const res = appendAsQueryParams(base, {
+ const res = appendUrlsAsQueryParams(base, {
redirect_url: url,
after_sign_in_url: url,
});
@@ -274,26 +274,26 @@ describe('appendQueryParams(base,url)', () => {
it('skips falsy values', () => {
const base = new URL('https://dashboard.clerk.com');
- const res = appendAsQueryParams(base, { redirect_url: undefined });
+ const res = appendUrlsAsQueryParams(base, { redirect_url: undefined });
expect(res).toBe('https://dashboard.clerk.com/');
});
it('converts relative to absolute urls', () => {
const base = new URL('https://dashboard.clerk.com');
- const res = appendAsQueryParams(base, { redirect_url: '/test' });
+ const res = appendUrlsAsQueryParams(base, { redirect_url: '/test' });
expect(res).toBe('https://dashboard.clerk.com/#/?redirect_url=http%3A%2F%2Flocalhost%2Ftest');
});
it('converts keys from camel to snake case', () => {
const base = new URL('https://dashboard.clerk.com');
- const res = appendAsQueryParams(base, { redirectUrl: '/test' });
+ const res = appendUrlsAsQueryParams(base, { redirectUrl: '/test' });
expect(res).toBe('https://dashboard.clerk.com/#/?redirect_url=http%3A%2F%2Flocalhost%2Ftest');
});
it('keeps origin before appending if base and url have different origin', () => {
const base = new URL('https://dashboard.clerk.com');
const url = new URL('https://www.google.com/something');
- const res = appendAsQueryParams(base, { redirect_url: url });
+ const res = appendUrlsAsQueryParams(base, { redirect_url: url });
expect(res).toBe('https://dashboard.clerk.com/#/?redirect_url=https%3A%2F%2Fwww.google.com%2Fsomething');
});
});
diff --git a/packages/clerk-js/src/utils/url.ts b/packages/clerk-js/src/utils/url.ts
index 632fcdb8e11..a4e08b0c09b 100644
--- a/packages/clerk-js/src/utils/url.ts
+++ b/packages/clerk-js/src/utils/url.ts
@@ -210,7 +210,7 @@ export const trimTrailingSlash = (path: string): string => {
return (path || '').replace(/\/+$/, '');
};
-export const appendAsQueryParams = (
+export const appendUrlsAsQueryParams = (
baseUrl: string | URL,
urls: Record = {},
): string => {
@@ -231,6 +231,21 @@ export const appendAsQueryParams = (
return `${base}${params.toString() ? '#/?' + params.toString() : ''}`;
};
+export const appendAsQueryParams = (
+ baseUrl: string | URL,
+ values: Record = {},
+): string => {
+ const base = toURL(baseUrl);
+ for (const [key, val] of Object.entries(values)) {
+ if (!val) {
+ continue;
+ }
+ base.searchParams.append(camelToSnake(key), val);
+ }
+
+ return baseUrl.toString() + base.search;
+};
+
export const hasExternalAccountSignUpError = (signUp: SignUpResource): boolean => {
const { externalAccount } = signUp.verifications;
return !!externalAccount.error;
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index 9701835e499..20791102102 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -5,7 +5,7 @@ import { useAuthContext } from '../contexts/AuthContext';
import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useSessionContext } from '../contexts/SessionContext';
import { LoadedGuarantee } from '../contexts/StructureContext';
-import type { RedirectToProps, WithClerkProp } from '../types';
+import type { RedirectToSignInProps, RedirectToSignUpProps, WithClerkProp } from '../types';
import { withClerk } from './withClerk';
export const SignedIn = ({ children }: React.PropsWithChildren): JSX.Element | null => {
@@ -40,7 +40,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JS
return <>{children}>;
};
-export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
+export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
const { client, session } = clerk;
// TODO: Remove temp use of __unstable__environment
const { __unstable__environment } = clerk as any;
@@ -59,7 +59,7 @@ export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
+export const RedirectToSignUp = withClerk(({ clerk, ...props }: WithClerkProp) => {
React.useEffect(() => {
void clerk.redirectToSignUp(props);
}, []);
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index 398e675f9de..7a1200ecf6e 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -14,13 +14,14 @@ import type {
OrganizationListProps,
OrganizationMembershipResource,
OrganizationResource,
- RedirectOptions,
SetActiveParams,
SignInProps,
+ SignInRedirectOptions,
SignOut,
SignOutCallback,
SignOutOptions,
SignUpProps,
+ SignUpRedirectOptions,
UnsubscribeCallback,
UserButtonProps,
UserProfileProps,
@@ -596,7 +597,7 @@ export default class IsomorphicClerk {
}
};
- redirectToSignIn = (opts: RedirectOptions): void => {
+ redirectToSignIn = (opts: SignInRedirectOptions): void => {
const callback = () => this.clerkjs?.redirectToSignIn(opts as any);
if (this.clerkjs && this.#loaded) {
void callback();
@@ -605,7 +606,7 @@ export default class IsomorphicClerk {
}
};
- redirectToSignUp = (opts: RedirectOptions): void => {
+ redirectToSignUp = (opts: SignUpRedirectOptions): void => {
const callback = () => this.clerkjs?.redirectToSignUp(opts as any);
if (this.clerkjs && this.#loaded) {
void callback();
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index b764d7a4bc6..7d8109601cc 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -6,8 +6,9 @@ import type {
LoadedClerk,
MultiDomainAndOrProxy,
PublishableKeyOrFrontendApi,
- RedirectOptions,
SessionResource,
+ SignInRedirectOptions,
+ SignUpRedirectOptions,
UserResource,
} from '@clerk/types';
@@ -83,4 +84,5 @@ export interface SignUpButtonProps extends ButtonProps {
export type SignInWithMetamaskButtonProps = Pick;
-export type RedirectToProps = RedirectOptions;
+export type RedirectToSignInProps = SignInRedirectOptions;
+export type RedirectToSignUpProps = SignUpRedirectOptions;
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index d91e33c5573..bdfbc653045 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -358,14 +358,14 @@ export interface Clerk {
*
* @param opts A {@link RedirectOptions} object
*/
- redirectToSignIn(opts?: RedirectOptions): Promise;
+ redirectToSignIn(opts?: SignInRedirectOptions): Promise;
/**
* Redirects to the configured URL where is mounted.
*
* @param opts A {@link RedirectOptions} object
*/
- redirectToSignUp(opts?: RedirectOptions): Promise;
+ redirectToSignUp(opts?: SignUpRedirectOptions): Promise;
/**
* Redirects to the configured URL where is mounted.
@@ -547,6 +547,20 @@ export interface Resources {
export type RoutingStrategy = 'path' | 'hash' | 'virtual';
+export type SignInInitialValues = {
+ emailAddress?: string;
+ phoneNumber?: string;
+ username?: string;
+};
+
+export type SignUpInitialValues = {
+ emailAddress?: string;
+ phoneNumber?: string;
+ firstName?: string;
+ lastName?: string;
+ username?: string;
+};
+
export type RedirectOptions = {
/**
* Full URL or path to navigate after successful sign in.
@@ -569,6 +583,20 @@ export type RedirectOptions = {
redirectUrl?: string | null;
};
+export type SignInRedirectOptions = RedirectOptions & {
+ /**
+ * Initial values that are used to prefill the sign in form.
+ */
+ initialValues?: SignInInitialValues;
+};
+
+export type SignUpRedirectOptions = RedirectOptions & {
+ /**
+ * Initial values that are used to prefill the sign up form.
+ */
+ initialValues?: SignUpInitialValues;
+};
+
export type SetActiveParams = {
/**
* The session resource or session id (string version) to be set as active.
@@ -611,6 +639,10 @@ export type SignInProps = {
* prop of ClerkProvided (if one is provided)
*/
appearance?: SignInTheme;
+ /**
+ * Initial values that are used to prefill the sign in form.
+ */
+ initialValues?: SignInInitialValues;
} & RedirectOptions;
export type SignUpProps = {
@@ -638,6 +670,10 @@ export type SignUpProps = {
* Additional arbitrary metadata to be stored alongside the User object
*/
unsafeMetadata?: SignUpUnsafeMetadata;
+ /**
+ * Initial values that are used to prefill the sign up form.
+ */
+ initialValues?: SignUpInitialValues;
} & RedirectOptions;
export type UserProfileProps = {