Skip to content

Commit

Permalink
chore(*): Rename refs of MagicLink to EmailLink - deprecation JS Docs (
Browse files Browse the repository at this point in the history
…#1833)

* chore(types): Add new types and deprecation JSDoc for renaming MagicLink to EmailLink

* chore(shared): Add deprecation JSDoc for renaming *MagicLink* to *EmailLink*

* chore(clerk-js): Drop MagicLink refs and use EmailLink in /ui

Since the /ui is internal it is safe to use the EmailLink
references instead of the EmailLink.

* chore(clerk-js): Add deprecation JSDoc for renaming *MagicLink* to *EmailLink*

* chore(clerk-react): Add deprecation JSDoc for renaming *MagicLink* to *EmailLink*

* chore(clerk-expo): Re-export modules for renaming *MagicLink* to *EmailLink*

* chore(nextjs): Re-export new modules for renaming *MagicLink* to *EmailLink*

* chore(chrome-extension): Fix chrome-extension snapshot tests caused by re-export

* chore(repo): Add changeset
  • Loading branch information
dimkl authored Oct 6, 2023
1 parent fcf0e46 commit 7f4d4b9
Show file tree
Hide file tree
Showing 39 changed files with 754 additions and 132 deletions.
12 changes: 12 additions & 0 deletions .changeset/tricky-olives-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@clerk/chrome-extension': patch
'@clerk/clerk-js': patch
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
'@clerk/clerk-expo': patch
---

Introduce new `*EmailLink*` helpers that will replace the `*MagicLink*` helpers.
Also marked all the `*MagicLink*` as deprecated using JSDocs.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoading",
"ClerkProvider",
"CreateOrganization",
"EmailLinkErrorCode",
"MagicLinkErrorCode",
"MultisessionAppSupport",
"OrganizationList",
Expand All @@ -32,11 +33,13 @@ exports[`public exports should not include a breaking change 1`] = `
"WithUser",
"__internal__setErrorThrowerOptions",
"isClerkAPIResponseError",
"isEmailLinkError",
"isKnownError",
"isMagicLinkError",
"isMetamaskError",
"useAuth",
"useClerk",
"useEmailLink",
"useMagicLink",
"useOrganization",
"useOrganizationList",
Expand Down
264 changes: 263 additions & 1 deletion packages/clerk-js/src/core/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { mockNativeRuntime } from '../testUtils';
import Clerk from './clerk';
import { eventBus, events } from './events';
import type { AuthConfig, DisplayConfig, Organization } from './resources/internal';
import { Client, Environment, MagicLinkErrorCode, SignIn, SignUp } from './resources/internal';
import { Client, EmailLinkErrorCode, Environment, MagicLinkErrorCode, SignIn, SignUp } from './resources/internal';
import { SessionCookieService } from './services';
import { mockJwt } from './test/fixtures';

Expand Down Expand Up @@ -1355,6 +1355,7 @@ describe('Clerk singleton', () => {
});
});

// deprecated: Will be replaced by handleEmailLinkVerification
describe('.handleMagicLinkVerification()', () => {
beforeEach(() => {
mockClientFetch.mockReset();
Expand Down Expand Up @@ -1616,6 +1617,267 @@ describe('Clerk singleton', () => {
});
});

describe('.handleEmailLinkVerification()', () => {
beforeEach(() => {
mockClientFetch.mockReset();
mockEnvironmentFetch.mockReset();
});

it('completes the sign in flow if a session was created on this client', async () => {
const createdSessionId = 'sess_123';
setWindowQueryParams([
['__clerk_status', 'verified'],
['__clerk_created_session', createdSessionId],
]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [{ id: createdSessionId }],
signIn: new SignIn({
status: 'completed',
} as any as SignInJSON),
signUp: new SignUp(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

const redirectUrlComplete = '/redirect-to';
sut.handleEmailLinkVerification({ redirectUrlComplete });

await waitFor(() => {
expect(mockSetActive).toHaveBeenCalledWith({
session: createdSessionId,
beforeEmit: expect.any(Function),
});
});
});

it("continues to redirectUrl for sign in that's not completed", async () => {
setWindowQueryParams([['__clerk_status', 'verified']]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signIn: new SignIn({
status: 'needs_second_factor',
} as any as SignInJSON),
signUp: new SignUp(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

const redirectUrl = '/2fa';
sut.handleEmailLinkVerification({ redirectUrl });

await waitFor(() => {
expect(mockSetActive).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(redirectUrl);
});
});

it('completes the sign up flow if a session was created on this client', async () => {
const createdSessionId = 'sess_123';
setWindowQueryParams([
['__clerk_status', 'verified'],
['__clerk_created_session', createdSessionId],
]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [{ id: createdSessionId }],
signUp: new SignUp({
status: 'completed',
} as any as SignUpJSON),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

const redirectUrlComplete = '/redirect-to';
sut.handleEmailLinkVerification({ redirectUrlComplete });

await waitFor(() => {
expect(mockSetActive).toHaveBeenCalledWith({
session: createdSessionId,
beforeEmit: expect.any(Function),
});
});
});

it("continues the sign up flow for a sign up that's not completed", async () => {
setWindowQueryParams([['__clerk_status', 'verified']]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signUp: new SignUp({
status: 'missing_requirements',
} as any as SignUpJSON),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

const redirectUrl = '/next-up';
sut.handleEmailLinkVerification({ redirectUrl });

await waitFor(() => {
expect(mockSetActive).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(redirectUrl);
});
});

it('throws an error for expired verification status parameter', async () => {
setWindowQueryParams([['__clerk_status', 'expired']]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signUp: new SignUp(null),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

await expect(async () => {
await sut.handleEmailLinkVerification({});
}).rejects.toThrow(EmailLinkErrorCode.Expired);
expect(mockSetActive).not.toHaveBeenCalled();
});

it('throws an error for failed verification status parameter', async () => {
setWindowQueryParams([['__clerk_status', 'failed']]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signUp: new SignUp(null),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();

const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

await expect(async () => {
await sut.handleEmailLinkVerification({});
}).rejects.toThrow(EmailLinkErrorCode.Failed);
expect(mockSetActive).not.toHaveBeenCalled();
});

it('runs a callback when verified on other device', async () => {
setWindowQueryParams([
['__clerk_status', 'verified'],
['__clerk_created_session', 'sess_123'],
]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signUp: new SignUp(null),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();
const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;
const res = { ping: 'ping' };
const cb = () => {
res.ping = 'pong';
};
await sut.handleEmailLinkVerification({ onVerifiedOnOtherDevice: cb });
expect(res.ping).toEqual('pong');
expect(mockSetActive).not.toHaveBeenCalled();
});

it('throws an error with no status query parameter', async () => {
setWindowQueryParams([['__clerk_created_session', 'sess_123']]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [],
signUp: new SignUp(null),
signIn: new SignIn(null),
}),
);
const mockSetActive = jest.fn();
const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;
await expect(async () => {
await sut.handleEmailLinkVerification({});
}).rejects.toThrow(EmailLinkErrorCode.Failed);
expect(mockSetActive).not.toHaveBeenCalled();
});

it('throws an error for invalid status query parameter', async () => {
setWindowQueryParams([
['__clerk_status', 'whatever'],
['__clerk_created_session', 'sess_123'],
]);
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
sessions: [{ id: 'sess_123' }],
signIn: new SignIn({
status: 'completed',
} as any as SignInJSON),
signUp: new SignUp(null),
}),
);
const mockSetActive = jest.fn();
const sut = new Clerk(frontendApi);
await sut.load({
navigate: mockNavigate,
});
sut.setActive = mockSetActive;

await expect(async () => {
await sut.handleEmailLinkVerification({});
}).rejects.toThrow(EmailLinkErrorCode.Failed);
expect(mockSetActive).not.toHaveBeenCalled();
});
});

/**
* TODO:
* 1) Write better test names for this.domain and this.isSatellite
Expand Down
50 changes: 50 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
DomainOrProxyUrl,
EnvironmentJSON,
EnvironmentResource,
HandleEmailLinkVerificationParams,
HandleMagicLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
Expand Down Expand Up @@ -104,6 +105,8 @@ import createFapiClient from './fapiClient';
import {
BaseResource,
Client,
EmailLinkError,
EmailLinkErrorCode,
Environment,
MagicLinkError,
MagicLinkErrorCode,
Expand Down Expand Up @@ -809,6 +812,10 @@ export default class Clerk implements ClerkInterface {
return;
};

/**
*
* @deprecated Use `handleEmailLinkVerification` instead.
*/
public handleMagicLinkVerification = async (
params: HandleMagicLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
Expand Down Expand Up @@ -852,6 +859,49 @@ export default class Clerk implements ClerkInterface {
return null;
};

public handleEmailLinkVerification = async (
params: HandleEmailLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
): Promise<unknown> => {
if (!this.client) {
return;
}

const verificationStatus = getClerkQueryParam('__clerk_status');
if (verificationStatus === 'expired') {
throw new EmailLinkError(EmailLinkErrorCode.Expired);
} else if (verificationStatus !== 'verified') {
throw new EmailLinkError(EmailLinkErrorCode.Failed);
}

const newSessionId = getClerkQueryParam('__clerk_created_session');
const { signIn, signUp, sessions } = this.client;

const shouldCompleteOnThisDevice = sessions.some(s => s.id === newSessionId);
const shouldContinueOnThisDevice =
signIn.status === 'needs_second_factor' || signUp.status === 'missing_requirements';

const navigate = (to: string) =>
customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);

const redirectComplete = params.redirectUrlComplete ? () => navigate(params.redirectUrlComplete as string) : noop;
const redirectContinue = params.redirectUrl ? () => navigate(params.redirectUrl as string) : noop;

if (shouldCompleteOnThisDevice) {
return this.setActive({
session: newSessionId,
beforeEmit: redirectComplete,
});
} else if (shouldContinueOnThisDevice) {
return redirectContinue();
}

if (typeof params.onVerifiedOnOtherDevice === 'function') {
params.onVerifiedOnOtherDevice();
}
return null;
};

public handleRedirectCallback = async (
params: HandleOAuthCallbackParams = {},
customNavigate?: (to: string) => Promise<unknown>,
Expand Down
Loading

0 comments on commit 7f4d4b9

Please sign in to comment.