diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index 5ba08357da..9ee43b797a 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -358,6 +358,61 @@ describe('Clerk singleton', () => { }); }); + it('creates a new sign up and navigates to the continue sign-up path if the user was not found during sso signup and there are missing requirements', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + onWindowLocationHost: () => false, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [], + signIn: new SignIn({ + status: 'needs_identifier', + first_factor_verification: { + status: 'transferable', + strategy: 'oauth_google', + external_verification_redirect_url: '', + error: { + code: 'external_account_not_found', + long_message: 'The External Account was not found.', + message: 'Invalid external account', + }, + }, + second_factor_verification: null, + identifier: '', + user_data: null, + created_session_id: null, + created_user_id: null, + } as any as SignInJSON), + signUp: new SignUp(null), + }), + ); + + const mockSignUpCreate = jest.fn().mockReturnValue(Promise.resolve({ status: 'missing_requirements' })); + + const sut = new Clerk(frontendApi); + await sut.load({ + navigate: mockNavigate, + }); + if (!sut.client) { + fail('we should always have a client'); + } + sut.client.signUp.create = mockSignUpCreate; + + sut.handleRedirectCallback(); + + await waitFor(() => { + expect(mockSignUpCreate).toHaveBeenCalledWith({ transfer: true }); + expect(mockNavigate).toHaveBeenCalledWith('/signUpUrl#/continue'); + }); + }); + it('signs the user in if the user was found during sign up', async () => { mockEnvironmentFetch.mockReturnValue( Promise.resolve({ @@ -771,6 +826,47 @@ describe('Clerk singleton', () => { expect(mockNavigate).toHaveBeenCalledWith('/sign-up'); }); }); + + it('redirects user to the continue sign-up url if the external account was verified but there are still missing requirements', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + onWindowLocationHost: () => false, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [], + signIn: new SignIn(null), + signUp: new SignUp({ + status: 'missing_requirements', + verifications: { + external_account: { + status: 'verified', + strategy: 'oauth_google', + external_verification_redirect_url: '', + error: null, + }, + }, + } as any as SignUpJSON), + }), + ); + + const sut = new Clerk(frontendApi); + await sut.load({ + navigate: mockNavigate, + }); + + sut.handleRedirectCallback(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/signUpUrl#/continue'); + }); + }); }); describe('.handleMagicLinkVerification()', () => { diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2042b4ec4f..5fd1af46a5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -460,6 +460,9 @@ export default class Clerk implements ClerkInterface { params.afterSignUpUrl || params.redirectUrl || displayConfig.afterSignUpUrl, ); + const navigateToContinueSignUp = makeNavigate(displayConfig.signUpUrl + '/continue'); + const navigateToContinueSignUpForTransfer = makeNavigate(displayConfig.signUpUrl + '#/continue'); + const userExistsButNeedsToSignIn = su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists'; @@ -482,6 +485,8 @@ export default class Clerk implements ClerkInterface { switch (res.status) { case 'complete': return this.setSession(res.createdSessionId, navigateAfterSignUp); + case 'missing_requirements': + return navigateToContinueSignUpForTransfer(); default: clerkOAuthCallbackDidNotCompleteSignInSIgnUp('sign in'); } @@ -509,6 +514,10 @@ export default class Clerk implements ClerkInterface { } } + if (su.externalAccountStatus === 'verified' && su.status == 'missing_requirements') { + return navigateToContinueSignUp(); + } + if (hasExternalAccountSignUpError(signUp)) { return navigateToSignUp(); } diff --git a/packages/clerk-js/src/ui/common/authForms/Header.tsx b/packages/clerk-js/src/ui/common/authForms/Header.tsx index 7ea29e9d97..a3be7294f1 100644 --- a/packages/clerk-js/src/ui/common/authForms/Header.tsx +++ b/packages/clerk-js/src/ui/common/authForms/Header.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { Logo } from 'ui/common'; import { BackButton } from 'ui/common/backButton'; import { Error } from 'ui/common/error'; +import { Info } from 'ui/common/info'; export type HeaderProps = { showBack?: boolean; @@ -11,6 +12,7 @@ export type HeaderProps = { handleBack?: () => void; welcomeName?: string; error?: string; + info?: string; } & React.HTMLAttributes; export function Header({ @@ -18,6 +20,7 @@ export function Header({ showLogo = true, welcomeName, error, + info, handleBack, className, }: HeaderProps): JSX.Element { @@ -37,6 +40,7 @@ export function Header({ /> )} {error && {error}} + {info && {info}} {showLogo && (
diff --git a/packages/clerk-js/src/ui/common/info/Info.test.tsx b/packages/clerk-js/src/ui/common/info/Info.test.tsx new file mode 100644 index 0000000000..0275c6f387 --- /dev/null +++ b/packages/clerk-js/src/ui/common/info/Info.test.tsx @@ -0,0 +1,11 @@ +import { renderJSON } from '@clerk/shared/testUtils'; +import * as React from 'react'; + +import { Info } from './Info'; + +describe('', () => { + it('renders the info component', () => { + const tree = renderJSON(Foo); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/clerk-js/src/ui/common/info/Info.tsx b/packages/clerk-js/src/ui/common/info/Info.tsx new file mode 100644 index 0000000000..9ff3848ad2 --- /dev/null +++ b/packages/clerk-js/src/ui/common/info/Info.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +export type InfoProps = { + children: React.ReactNode; + style?: {}; +} & React.HTMLAttributes; + +// Renders global info across components, will be replaced by notification snackbars. +export const Info: React.FC = ({ children, style }) => { + if (!children) { + return null; + } + return ( +
+ {children} +
+ ); +}; diff --git a/packages/clerk-js/src/ui/common/info/index.ts b/packages/clerk-js/src/ui/common/info/index.ts new file mode 100644 index 0000000000..3d93d87eb6 --- /dev/null +++ b/packages/clerk-js/src/ui/common/info/index.ts @@ -0,0 +1 @@ +export * from './Info'; diff --git a/packages/clerk-js/src/ui/common/withRedirectToHome.tsx b/packages/clerk-js/src/ui/common/withRedirectToHome.tsx index e6fc8a92fb..ce01132f91 100644 --- a/packages/clerk-js/src/ui/common/withRedirectToHome.tsx +++ b/packages/clerk-js/src/ui/common/withRedirectToHome.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useCoreSession, useEnvironment } from 'ui/contexts'; import { useNavigate } from 'ui/hooks'; -export function withRedirectToHome

( +export function withRedirectToHome

( Component: React.ComponentType

, displayName?: string, ): (props: P) => null | JSX.Element { diff --git a/packages/clerk-js/src/ui/signUp/SignUp.tsx b/packages/clerk-js/src/ui/signUp/SignUp.tsx index 22acb6b371..0bba0997c9 100644 --- a/packages/clerk-js/src/ui/signUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUp.tsx @@ -6,6 +6,7 @@ import { ComponentContext, useCoreClerk, useSignUpContext, withCoreSessionSwitch import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from 'ui/router'; import { buildURL } from 'utils/url'; +import { SignUpContinue } from './SignUpContinue'; import { SignUpStart } from './SignUpStart'; import { SignUpVerifyEmailAddress, SignUpVerifyPhoneNumber } from './SignUpVerify'; @@ -39,6 +40,9 @@ function SignUpRoutes(): JSX.Element { + + + diff --git a/packages/clerk-js/src/ui/signUp/SignUpContinue.test.tsx b/packages/clerk-js/src/ui/signUp/SignUpContinue.test.tsx new file mode 100644 index 0000000000..1fd88ff0a5 --- /dev/null +++ b/packages/clerk-js/src/ui/signUp/SignUpContinue.test.tsx @@ -0,0 +1,306 @@ +import { render, renderJSON, screen, userEvent, waitFor } from '@clerk/shared/testUtils'; +import { UserSettingsJSON } from '@clerk/types'; +import { Session, UserSettings } from 'core/resources/internal'; +import React from 'react'; +import { useCoreSignUp } from 'ui/contexts'; + +import { SignUpContinue } from './SignUpContinue'; + +const navigateMock = jest.fn(); +const mockUpdateRequest = jest.fn(); +const mockSetSession = jest.fn(); +let mockUserSettings: UserSettings; + +jest.mock('ui/router/RouteContext'); + +jest.mock('ui/contexts', () => { + return { + useCoreSession: () => { + return { + id: 'sess_id', + } as Partial; + }, + useSignUpContext: () => { + return { + signInUrl: 'http://test.host/sign-in', + navigateAfterSignUp: jest.fn(), + }; + }, + useCoreClerk: jest.fn(() => ({ + frontendAPI: 'clerk.clerk.dev', + setSession: mockSetSession, + })), + useCoreSignUp: jest.fn(() => ({ + verifications: { + emailAddress: {}, + phoneNumber: {}, + externalAccount: {}, + }, + })), + useEnvironment: jest.fn(() => ({ + displayConfig: { + applicationName: 'My test app', + afterSignUpUrl: 'http://test.host/welcome', + signUpUrl: 'http://test.host/sign-up', + }, + userSettings: mockUserSettings, + authConfig: { singleSessionMode: false }, + })), + }; +}); + +jest.mock('ui/hooks', () => ({ + useNavigate: () => { + return { + navigate: navigateMock, + }; + }, +})); + +describe('', () => { + const { location } = window; + + beforeEach(() => { + mockUserSettings = new UserSettings({ + attributes: { + username: { + enabled: true, + required: true, + }, + first_name: { + enabled: true, + required: true, + }, + last_name: { + enabled: true, + required: true, + }, + password: { + enabled: true, + required: true, + }, + email_address: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + phone_number: { + enabled: true, + }, + }, + social: { + oauth_google: { + enabled: true, + strategy: 'oauth_google', + }, + oauth_facebook: { + enabled: true, + strategy: 'oauth_facebook', + }, + }, + } as UserSettingsJSON); + }); + + beforeEach(() => { + (useCoreSignUp as jest.Mock).mockImplementation(() => { + return { + id: 'su_perman', + update: mockUpdateRequest, + verifications: { + externalAccount: { + status: 'verified', + }, + emailAddress: { + status: 'unverified', + }, + }, + firstName: null, + lastName: null, + emailAddress: 'bryan@taken.com', + phoneNumber: '+12125551001', + username: 'bryanmills', + }; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + global.window.location = location; + }); + + it('renders the sign up continue screen', () => { + const tree = renderJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('redirects to sign-up if no current sign-up exists', () => { + (useCoreSignUp as jest.Mock).mockImplementation(() => { + return {}; + }); + + render(); + expect(navigateMock).toHaveBeenCalledTimes(1); + expect(navigateMock).toHaveBeenCalledWith('http://test.host/sign-up'); + }); + + it('pre-fills form with unverified email', () => { + render(); + + expect(screen.getByLabelText('Email address')).toHaveValue('bryan@taken.com'); + }); + + it('does not show oauth providers if we already have a verified external account', () => { + render(); + + expect(screen.queryByRole('button', { name: 'Sign up with Facebook' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Sign up with Google' })).not.toBeInTheDocument(); + }); + + it('patches the existing signup when user submits the form', async () => { + mockUpdateRequest.mockImplementation(() => + Promise.resolve({ + firstName: 'Bryan', + lastName: 'Mills', + emailAddress: 'bryan@taken.com', + verifications: { + emailAddress: { + status: 'unverified', + }, + }, + }), + ); + + render(); + + const firstNameInput = screen.getByLabelText('First name'); + userEvent.clear(firstNameInput); + userEvent.type(screen.getByLabelText('First name'), 'Bryan'); + + const lastNameInput = screen.getByLabelText('Last name'); + userEvent.clear(lastNameInput); + userEvent.type(lastNameInput, 'Mills'); + + userEvent.click(screen.getByRole('button', { name: 'Sign up' })); + + await waitFor(() => { + expect(mockUpdateRequest).toHaveBeenCalledTimes(1); + expect(mockUpdateRequest).toHaveBeenCalledWith({ + first_name: 'Bryan', + last_name: 'Mills', + email_address: 'bryan@taken.com', + }); + expect(navigateMock).toHaveBeenCalledTimes(1); + expect(navigateMock).toHaveBeenCalledWith('http://test.host/sign-up/verify-email-address'); + }); + }); + + it('skips the email input if the email is already verified', () => { + (useCoreSignUp as jest.Mock).mockImplementation(() => { + return { + id: 'su_perman', + update: mockUpdateRequest, + verifications: { + externalAccount: { + status: 'verified', + }, + emailAddress: { + status: 'verified', + }, + }, + firstName: null, + lastName: null, + emailAddress: 'bryan@taken.com', + phoneNumber: '+12125551001', + username: 'bryanmills', + }; + }); + + render(); + + expect(screen.queryByText('Email address')).not.toBeInTheDocument(); + }); + + it('skips the password input if there is a verified external account', () => { + render(); + + expect(screen.queryByText('Password')).not.toBeInTheDocument(); + }); + + it('skips already collected fields if they need no verification', () => { + (useCoreSignUp as jest.Mock).mockImplementation(() => { + return { + id: 'su_perman', + update: mockUpdateRequest, + verifications: { + externalAccount: { + status: 'verified', + }, + emailAddress: { + status: 'unverified', + }, + }, + firstName: 'Bryan', + lastName: 'Mills', + emailAddress: 'bryan@taken.com', + phoneNumber: '+12125551001', + username: 'bryanmills', + }; + }); + + render(); + + expect(screen.queryByText('First name')).not.toBeInTheDocument(); + expect(screen.queryByText('Last name')).not.toBeInTheDocument(); + expect(screen.queryByText('Username')).not.toBeInTheDocument(); + }); + + it('skips non-required fields', () => { + mockUserSettings = new UserSettings({ + attributes: { + username: { + enabled: true, + required: false, + }, + first_name: { + enabled: true, + required: false, + }, + last_name: { + enabled: true, + required: false, + }, + password: { + enabled: true, + required: true, + }, + email_address: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + phone_number: { + enabled: true, + }, + }, + social: { + oauth_google: { + enabled: true, + strategy: 'oauth_google', + }, + oauth_facebook: { + enabled: true, + strategy: 'oauth_facebook', + }, + }, + } as UserSettingsJSON); + + render(); + + expect(screen.queryByText('First name')).not.toBeInTheDocument(); + expect(screen.queryByText('Last name')).not.toBeInTheDocument(); + expect(screen.queryByText('Username')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/clerk-js/src/ui/signUp/SignUpContinue.tsx b/packages/clerk-js/src/ui/signUp/SignUpContinue.tsx new file mode 100644 index 0000000000..459becaab3 --- /dev/null +++ b/packages/clerk-js/src/ui/signUp/SignUpContinue.tsx @@ -0,0 +1,350 @@ +import { Control } from '@clerk/shared/components/control'; +import { Form } from '@clerk/shared/components/form'; +import { Input } from '@clerk/shared/components/input'; +import { PhoneInput } from '@clerk/shared/components/phoneInput'; +import { SignUpResource } from '@clerk/types'; +import React from 'react'; +import type { FieldState } from 'ui/common'; +import { + buildRequest, + Footer, + handleError, + PoweredByClerk, + Separator, + useFieldState, + withRedirectToHome, +} from 'ui/common'; +import { Body, Header } from 'ui/common/authForms'; +import { useCoreClerk, useCoreSignUp, useEnvironment, useSignUpContext } from 'ui/contexts'; +import { useNavigate } from 'ui/hooks'; + +import { SignInLink } from './SignInLink'; +import { SignUpOAuth } from './SignUpOAuth'; +import { SignUpWeb3 } from './SignUpWeb3'; +import { determineFirstPartyFields } from './utils'; + +type ActiveIdentifier = 'emailAddress' | 'phoneNumber'; + +function _SignUpContinue(): JSX.Element | null { + const { navigate } = useNavigate(); + const environment = useEnvironment(); + const { displayConfig, userSettings } = environment; + const { setSession } = useCoreClerk(); + const { navigateAfterSignUp } = useSignUpContext(); + const [emailOrPhoneActive, setEmailOrPhoneActive] = React.useState('emailAddress'); + const signUp = useCoreSignUp(); + + // Redirect to sign-up if there is no persisted sign-up + if (!signUp.id) { + void navigate(displayConfig.signUpUrl); + return null; + } + + // Pre-populate fields from existing sign-up object + const formFields = { + firstName: useFieldState('first_name', signUp.firstName || ''), + lastName: useFieldState('last_name', signUp.lastName || ''), + emailAddress: useFieldState('email_address', signUp.emailAddress || ''), + username: useFieldState('username', signUp.username || ''), + phoneNumber: useFieldState('phone_number', signUp.phoneNumber || ''), + password: useFieldState('password', ''), + } as const; + + type FormFieldsKey = keyof typeof formFields; + + const [error, setError] = React.useState(); + + const hasVerifiedEmail = signUp.verifications?.emailAddress?.status == 'verified'; + const hasVerifiedPhone = signUp.verifications?.phoneNumber?.status == 'verified'; + const hasVerifiedExternalAccount = signUp.verifications?.externalAccount?.status == 'verified'; + + const fields = determineFirstPartyFields(environment, false); + + // Show only fields absolutely necessary for completing sign-up to minimize fiction + + if (hasVerifiedEmail) { + delete fields.emailAddress; + delete fields.emailOrPhone; + } + + if (hasVerifiedPhone) { + delete fields.phoneNumber; + delete fields.emailOrPhone; + } + + if (hasVerifiedExternalAccount) { + delete fields.password; + } + + if (signUp.firstName) { + delete fields.firstName; + } + + if (signUp.lastName) { + delete fields.lastName; + } + + if (signUp.username) { + delete fields.username; + } + + // Remove any non-required fields + Object.entries(fields).forEach(([k, v]) => { + if (v !== 'required') { + // @ts-ignore + delete fields[k]; + } + }); + + const oauthOptions = userSettings.socialProviderStrategies; + const web3Options = userSettings.web3FirstFactors; + + // Handle oauth errors? + + // React.useEffect(() => { + // async function handleOauthError() { + // const error = signUp.verifications.externalAccount.error; + // + // if (error) { + // switch (error.code) { + // case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP: + // case ERROR_CODES.OAUTH_ACCESS_DENIED: + // setError(error.longMessage); + // break; + // default: + // // Error from server may be too much information for the end user, so set a generic error + // setError('Unable to complete action at this time. If the problem persists please contact support.'); + // } + // + // // TODO: This is a hack to reset the sign in attempt so that the oauth error + // // does not persist on full page reloads. + // // + // // We will revise this strategy as part of the Clerk DX epic. + // void (await signUp.create({})); + // } + // } + // + // void handleOauthError(); + // }); + + const handleChangeActive = (type: ActiveIdentifier) => (e: React.MouseEvent) => { + e.preventDefault(); + if (!fields.emailOrPhone) { + return; + } + setEmailOrPhoneActive(type); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const reqFields = Object.entries(fields).reduce( + (acc, [k, v]) => [...acc, ...(v && formFields[k as FormFieldsKey] ? [formFields[k as FormFieldsKey]] : [])], + [] as Array>, + ); + + if (fields.emailOrPhone && emailOrPhoneActive === 'emailAddress') { + reqFields.push(formFields.emailAddress); + } + + if (fields.emailOrPhone && emailOrPhoneActive === 'phoneNumber') { + reqFields.push(formFields.phoneNumber); + } + + try { + setError(undefined); + const req = buildRequest(reqFields); + const res = await signUp.update(req); + return completeSignUpFlow(res); + } catch (err) { + handleError(err, reqFields, setError); + } + }; + + const completeSignUpFlow = (su: SignUpResource) => { + if (su.status === 'complete') { + return setSession(su.createdSessionId, navigateAfterSignUp); + } else if (su.emailAddress && su.verifications.emailAddress.status !== 'verified') { + return navigate('../verify-email-address'); + } else if (su.phoneNumber && su.verifications.phoneNumber.status !== 'verified') { + return navigate('../verify-phone-number'); + } else if (su.status === 'missing_requirements') { + // do nothing + } + }; + + const firstNameField = fields.firstName ? ( + + formFields.firstName.setValue(el.value || '')} + /> + + ) : null; + + const lastNameField = fields.lastName ? ( + + formFields.lastName.setValue(el.value || '')} + /> + + ) : null; + + const nameField = (fields.firstName || fields.lastName) && ( +

+ {firstNameField} + {lastNameField} +
+ ); + + const usernameField = fields.username ? ( + + formFields.username.setValue(el.value || '')} + /> + + ) : null; + + const passwordField = fields.password ? ( + + formFields.password.setValue(el.value || '')} + /> + + ) : null; + + const shouldShowEmailAddressField = + fields.emailAddress || (fields.emailOrPhone && emailOrPhoneActive === 'emailAddress'); + + const emailAddressField = shouldShowEmailAddressField && ( + + formFields.emailAddress.setValue(el.value || '')} + /> + + ); + + const shouldShowPhoneNumberField = + fields.phoneNumber || (fields.emailOrPhone && emailOrPhoneActive === 'phoneNumber'); + + const phoneNumberField = shouldShowPhoneNumberField ? ( + + + + ) : null; + + const atLeastOneFormField = nameField || usernameField || emailAddressField || phoneNumberField || passwordField; + + const showOauthProviders = !hasVerifiedExternalAccount && oauthOptions.length > 0; + + const showWeb3Providers = web3Options.length > 0; + + return ( + <> +
+ + {showOauthProviders && ( + + )} + {showWeb3Providers && ( + + )} + {atLeastOneFormField && ( + <> + {(showOauthProviders || showWeb3Providers) && } + + {/* @ts-ignore */} +
+ <> + {nameField} + {usernameField} + {emailAddressField} + {phoneNumberField} + {passwordField} + +
+ + )} +
+ + +
+ + + ); +} + +export const SignUpContinue = withRedirectToHome(_SignUpContinue); diff --git a/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx b/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx index 932583477a..ae5a57d882 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx @@ -1,12 +1,11 @@ -import { mocked, render, renderJSON, screen, userEvent, waitFor } from '@clerk/shared/testUtils'; +import { render, renderJSON, screen, userEvent, waitFor } from '@clerk/shared/testUtils'; import { titleize } from '@clerk/shared/utils/string'; -import { SignInResource, UserSettingsJSON } from '@clerk/types'; +import { UserSettingsJSON } from '@clerk/types'; import { Session, UserSettings } from 'core/resources/internal'; import React from 'react'; -import { useCoreSignIn, useCoreSignUp } from 'ui/contexts'; +import { useCoreSignUp } from 'ui/contexts'; import { SignUpStart } from './SignUpStart'; -import { SignInStart } from 'ui/signIn/SignInStart'; const navigateMock = jest.fn(); const mockCreateRequest = jest.fn(); @@ -323,6 +322,10 @@ describe('', () => { } as UserSettingsJSON); }); + afterEach(() => { + setWindowQueryParams([]); + }); + it('it auto-completes sign up flow if sign up is complete after create', async () => { mockCreateRequest.mockImplementation(() => Promise.resolve({ diff --git a/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpContinue.test.tsx.snap b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpContinue.test.tsx.snap new file mode 100644 index 0000000000..b21caa4d45 --- /dev/null +++ b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpContinue.test.tsx.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders the sign up continue screen 1`] = ` +Array [ +
+
+ Please fill in the following information to complete your sign-up +
+
+
+ My test app +
+
+
, +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+
+ + Already have an account? + + + + Sign in + +
+
+
, +] +`; diff --git a/packages/clerk-js/src/ui/styles/_info.scss b/packages/clerk-js/src/ui/styles/_info.scss new file mode 100644 index 0000000000..b043677160 --- /dev/null +++ b/packages/clerk-js/src/ui/styles/_info.scss @@ -0,0 +1,13 @@ +.info { + padding: 1em; + margin-bottom: 1em; + border-radius: 0.5em; + + color: $primary; + background-color: #e8fcff; + + font-size: 1rem; + line-height: 1.25rem; + font-weight: $semibold-weight; + text-align: center; +} diff --git a/packages/clerk-js/src/ui/styles/clerk.scss b/packages/clerk-js/src/ui/styles/clerk.scss index f12a5ab8aa..18c7a19e67 100644 --- a/packages/clerk-js/src/ui/styles/clerk.scss +++ b/packages/clerk-js/src/ui/styles/clerk.scss @@ -31,6 +31,7 @@ $cl-red-O10: rgba(205, 68, 74, 0.1); @import 'authForms'; @import 'backButton'; @import 'error'; + @import 'info'; @import 'userprofile'; @import 'userbutton'; @import 'backButton';