Skip to content

Commit

Permalink
feat(clerk-js): Make sign up flow resumable
Browse files Browse the repository at this point in the history
  • Loading branch information
yourtallness committed May 13, 2022
1 parent f8943f6 commit bfe0d8c
Show file tree
Hide file tree
Showing 14 changed files with 977 additions and 5 deletions.
96 changes: 96 additions & 0 deletions packages/clerk-js/src/core/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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()', () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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');
}
Expand Down Expand Up @@ -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();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/clerk-js/src/ui/common/authForms/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ 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;
showLogo?: boolean;
handleBack?: () => void;
welcomeName?: string;
error?: string;
info?: string;
} & React.HTMLAttributes<HTMLDivElement>;

export function Header({
showBack,
showLogo = true,
welcomeName,
error,
info,
handleBack,
className,
}: HeaderProps): JSX.Element {
Expand All @@ -37,6 +40,7 @@ export function Header({
/>
)}
{error && <Error style={errorStyle}>{error}</Error>}
{info && <Info>{info}</Info>}
{showLogo && (
<div className='cl-auth-form-header-logo'>
<Logo />
Expand Down
11 changes: 11 additions & 0 deletions packages/clerk-js/src/ui/common/info/Info.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { renderJSON } from '@clerk/shared/testUtils';
import * as React from 'react';

import { Info } from './Info';

describe('<info/>', () => {
it('renders the info component', () => {
const tree = renderJSON(<Info>Foo</Info>);
expect(tree).toMatchSnapshot();
});
});
21 changes: 21 additions & 0 deletions packages/clerk-js/src/ui/common/info/Info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

export type InfoProps = {
children: React.ReactNode;
style?: {};
} & React.HTMLAttributes<HTMLDivElement>;

// Renders global info across components, will be replaced by notification snackbars.
export const Info: React.FC<InfoProps> = ({ children, style }) => {
if (!children) {
return null;
}
return (
<div
className='cl-info'
style={style}
>
{children}
</div>
);
};
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/common/info/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Info';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/common/withRedirectToHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { useCoreSession, useEnvironment } from 'ui/contexts';
import { useNavigate } from 'ui/hooks';

export function withRedirectToHome<P extends SignInProps | SignUpProps>(
export function withRedirectToHome<P extends SignInProps | SignUpProps | { continueExisting?: boolean }>(
Component: React.ComponentType<P>,
displayName?: string,
): (props: P) => null | JSX.Element {
Expand Down
4 changes: 4 additions & 0 deletions packages/clerk-js/src/ui/signUp/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -39,6 +40,9 @@ function SignUpRoutes(): JSX.Element {
<Route path='verify'>
<VerifyMagicLink redirectUrlComplete={signUpContext.afterSignUpUrl || signUpContext.redirectUrl || undefined} />
</Route>
<Route path='continue'>
<SignUpContinue />
</Route>
<Route index>
<SignUpStart />
</Route>
Expand Down
Loading

0 comments on commit bfe0d8c

Please sign in to comment.