Skip to content

Commit

Permalink
feat(clerk-js): Prefill SignIn/Up components with initial values
Browse files Browse the repository at this point in the history
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/>
  • Loading branch information
desiprisg authored and SokratisVidros committed Sep 28, 2023
1 parent 01cfcdd commit 5c87542
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 59 deletions.
7 changes: 7 additions & 0 deletions .changeset/fresh-papayas-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

`<SignIn/>` and `<SignUp/>` input fields can now be prefilled with the `initialValues` prop.
18 changes: 12 additions & 6 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ import type {
Resources,
SetActiveParams,
SignInProps,
SignInRedirectOptions,
SignInResource,
SignOut,
SignOutCallback,
SignOutOptions,
SignUpField,
SignUpProps,
SignUpRedirectOptions,
SignUpResource,
UnsubscribeCallback,
UserButtonProps,
Expand All @@ -58,6 +60,7 @@ import type { MountComponentRenderer } from '../ui/Components';
import { completeSignUpFlow } from '../ui/components/SignUp/util';
import {
appendAsQueryParams,
appendUrlsAsQueryParams,
buildURL,
createBeforeUnloadTracker,
createCookieHandler,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -757,14 +760,14 @@ export default class Clerk implements ClerkInterface {
return;
};

public redirectToSignIn = async (options?: RedirectOptions): Promise<unknown> => {
public redirectToSignIn = async (options?: SignInRedirectOptions): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildSignInUrl(options));
}
return;
};

public redirectToSignUp = async (options?: RedirectOptions): Promise<unknown> => {
public redirectToSignUp = async (options?: SignUpRedirectOptions): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildSignUpUrl(options));
}
Expand Down Expand Up @@ -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 '';
}
Expand All @@ -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<MountComponentRenderer> {
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
55 changes: 42 additions & 13 deletions packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<SignInStartIdentifier, string | undefined> = 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<HTMLInputElement>(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;
}
Expand Down Expand Up @@ -137,7 +168,7 @@ export function _SignInStart(): JSX.Element {
});
}, []);

React.useEffect(() => {
useEffect(() => {
async function handleOauthError() {
const error = signIn?.firstFactorVerification?.error;
if (error) {
Expand Down Expand Up @@ -244,9 +275,6 @@ export function _SignInStart(): JSX.Element {
return <LoadingCard />;
}

const hasSocialOrWeb3Buttons = !!authenticatableSocialStrategies.length || !!web3FirstFactors.length;
const shouldAutofocus = !isMobileDevice() && !hasSocialOrWeb3Buttons;

return (
<Flow.Part part='start'>
<Card>
Expand All @@ -271,6 +299,7 @@ export function _SignInStart(): JSX.Element {
<Form.Root onSubmit={handleFirstPartySubmit}>
<Form.ControlRow elementId={identifierField.id}>
<Form.Control
ref={identifierFieldRef}
actionLabel={nextIdentifier?.action}
onActionClicked={switchToNextIdentifier}
{...identifierField.props}
Expand Down Expand Up @@ -303,7 +332,7 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> })
const ref = useRef<HTMLInputElement>(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';
Expand Down
12 changes: 6 additions & 6 deletions packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActiveIdentifier>(
Expand All @@ -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'),
Expand Down
11 changes: 6 additions & 5 deletions packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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'),
Expand Down
32 changes: 29 additions & 3 deletions packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,6 +23,18 @@ const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ });

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

const getInitialValuesFromQueryParams = (queryString: string, params: string[]) => {
const props: Record<string, string> = {};
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;
Expand All @@ -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.');
}
Expand Down Expand Up @@ -87,6 +106,7 @@ export const useSignUpContext = (): SignUpContextType => {
afterSignInUrl,
navigateAfterSignUp,
queryParams,
initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
authQueryString: authQs,
};
};
Expand All @@ -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.');
}
Expand Down Expand Up @@ -154,6 +179,7 @@ export const useSignInContext = (): SignInContextType => {
navigateAfterSignIn,
signUpContinueUrl,
queryParams,
initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
authQueryString: authQs,
};
};
Expand Down
Loading

0 comments on commit 5c87542

Please sign in to comment.