diff --git a/.changeset/tricky-olives-rescue.md b/.changeset/tricky-olives-rescue.md new file mode 100644 index 00000000000..b76cd1fe925 --- /dev/null +++ b/.changeset/tricky-olives-rescue.md @@ -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. diff --git a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap index 448480e58fd..2d7332aaf87 100644 --- a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap @@ -7,6 +7,7 @@ exports[`public exports should not include a breaking change 1`] = ` "ClerkLoading", "ClerkProvider", "CreateOrganization", + "EmailLinkErrorCode", "MagicLinkErrorCode", "MultisessionAppSupport", "OrganizationList", @@ -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", diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index fda04e96c4b..1018b2ce3cf 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -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'; @@ -1355,6 +1355,7 @@ describe('Clerk singleton', () => { }); }); + // deprecated: Will be replaced by handleEmailLinkVerification describe('.handleMagicLinkVerification()', () => { beforeEach(() => { mockClientFetch.mockReset(); @@ -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 diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d34eddc8ebe..93262f0a48c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -27,6 +27,7 @@ import type { DomainOrProxyUrl, EnvironmentJSON, EnvironmentResource, + HandleEmailLinkVerificationParams, HandleMagicLinkVerificationParams, HandleOAuthCallbackParams, InstanceType, @@ -104,6 +105,8 @@ import createFapiClient from './fapiClient'; import { BaseResource, Client, + EmailLinkError, + EmailLinkErrorCode, Environment, MagicLinkError, MagicLinkErrorCode, @@ -809,6 +812,10 @@ export default class Clerk implements ClerkInterface { return; }; + /** + * + * @deprecated Use `handleEmailLinkVerification` instead. + */ public handleMagicLinkVerification = async ( params: HandleMagicLinkVerificationParams, customNavigate?: (to: string) => Promise, @@ -852,6 +859,49 @@ export default class Clerk implements ClerkInterface { return null; }; + public handleEmailLinkVerification = async ( + params: HandleEmailLinkVerificationParams, + customNavigate?: (to: string) => Promise, + ): Promise => { + 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, diff --git a/packages/clerk-js/src/core/resources/EmailAddress.ts b/packages/clerk-js/src/core/resources/EmailAddress.ts index 9d216ce7c70..4995a054d87 100644 --- a/packages/clerk-js/src/core/resources/EmailAddress.ts +++ b/packages/clerk-js/src/core/resources/EmailAddress.ts @@ -1,11 +1,13 @@ import { Poller } from '@clerk/shared'; import type { AttemptEmailAddressVerificationParams, + CreateEmailLinkFlowReturn, CreateMagicLinkFlowReturn, EmailAddressJSON, EmailAddressResource, IdentificationLinkResource, PrepareEmailAddressVerificationParams, + StartEmailLinkFlowParams, StartMagicLinkFlowParams, VerificationResource, } from '@clerk/types'; @@ -46,7 +48,9 @@ export class EmailAddress extends BaseResource implements EmailAddressResource { body: { code }, }); }; - + /** + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow = (): CreateMagicLinkFlowReturn => { const { run, stop } = Poller(); @@ -77,6 +81,36 @@ export class EmailAddress extends BaseResource implements EmailAddressResource { return { startMagicLinkFlow, cancelMagicLinkFlow: stop }; }; + createEmailLinkFlow = (): CreateEmailLinkFlowReturn => { + const { run, stop } = Poller(); + + const startEmailLinkFlow = async ({ redirectUrl }: StartEmailLinkFlowParams): Promise => { + if (!this.id) { + clerkVerifyEmailAddressCalledBeforeCreate('SignUp'); + } + await this.prepareVerification({ + strategy: 'email_link', + redirectUrl: redirectUrl, + }); + return new Promise((resolve, reject) => { + void run(() => { + return this.reload() + .then(res => { + if (res.verification.status === 'verified') { + stop(); + resolve(res); + } + }) + .catch(err => { + stop(); + reject(err); + }); + }); + }); + }; + return { startEmailLinkFlow, cancelEmailLinkFlow: stop }; + }; + destroy = (): Promise => this._baseDelete(); toString = (): string => this.emailAddress; diff --git a/packages/clerk-js/src/core/resources/Error.ts b/packages/clerk-js/src/core/resources/Error.ts index 761989b20c4..b659a38014f 100644 --- a/packages/clerk-js/src/core/resources/Error.ts +++ b/packages/clerk-js/src/core/resources/Error.ts @@ -1,14 +1,17 @@ export { + ClerkRuntimeError, + ClerkAPIResponseError, + EmailLinkError, + EmailLinkErrorCode, + isClerkAPIResponseError, + isClerkRuntimeError, + isEmailLinkError, isKnownError, isMagicLinkError, isMetamaskError, - isClerkAPIResponseError, + MagicLinkError, MagicLinkErrorCode, parseError, parseErrors, - MagicLinkError, - ClerkAPIResponseError, - isClerkRuntimeError, - ClerkRuntimeError, } from '@clerk/shared'; export type { MetamaskError } from '@clerk/shared'; diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 42f930937ff..d3586d3df41 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -4,6 +4,7 @@ import type { AttemptSecondFactorParams, AuthenticateWithRedirectParams, AuthenticateWithWeb3Params, + CreateEmailLinkFlowReturn, CreateMagicLinkFlowReturn, EmailCodeConfig, EmailLinkConfig, @@ -20,6 +21,7 @@ import type { SignInJSON, SignInResource, SignInSecondFactor, + SignInStartEmailLinkFlowParams, SignInStartMagicLinkFlowParams, SignInStatus, VerificationResource, @@ -119,7 +121,9 @@ export class SignIn extends BaseResource implements SignInResource { action: 'attempt_first_factor', }); }; - + /** + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow = (): CreateMagicLinkFlowReturn => { const { run, stop } = Poller(); @@ -156,6 +160,42 @@ export class SignIn extends BaseResource implements SignInResource { return { startMagicLinkFlow, cancelMagicLinkFlow: stop }; }; + createEmailLinkFlow = (): CreateEmailLinkFlowReturn => { + const { run, stop } = Poller(); + + const startEmailLinkFlow = async ({ + emailAddressId, + redirectUrl, + }: SignInStartEmailLinkFlowParams): Promise => { + if (!this.id) { + clerkVerifyEmailAddressCalledBeforeCreate('SignIn'); + } + await this.prepareFirstFactor({ + strategy: 'email_link', + emailAddressId: emailAddressId, + redirectUrl: redirectUrl, + }); + return new Promise((resolve, reject) => { + void run(() => { + return this.reload() + .then(res => { + const status = res.firstFactorVerification.status; + if (status === 'verified' || status === 'expired') { + stop(); + resolve(res); + } + }) + .catch(err => { + stop(); + reject(err); + }); + }); + }); + }; + + return { startEmailLinkFlow, cancelEmailLinkFlow: stop }; + }; + prepareSecondFactor = (params: PrepareSecondFactorParams): Promise => { return this._basePost({ body: params, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index ed63e4feb28..97fd1a643fe 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -6,6 +6,7 @@ import type { AttemptWeb3WalletVerificationParams, AuthenticateWithRedirectParams, AuthenticateWithWeb3Params, + CreateEmailLinkFlowReturn, CreateMagicLinkFlowReturn, PrepareEmailAddressVerificationParams, PreparePhoneNumberVerificationParams, @@ -18,6 +19,7 @@ import type { SignUpResource, SignUpStatus, SignUpUpdateParams, + StartEmailLinkFlowParams, StartMagicLinkFlowParams, } from '@clerk/types'; @@ -113,6 +115,9 @@ export class SignUp extends BaseResource implements SignUpResource { return this.attemptVerification({ ...params, strategy: 'email_code' }); }; + /** + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow = (): CreateMagicLinkFlowReturn => { const { run, stop } = Poller(); @@ -146,6 +151,39 @@ export class SignUp extends BaseResource implements SignUpResource { return { startMagicLinkFlow, cancelMagicLinkFlow: stop }; }; + createEmailLinkFlow = (): CreateEmailLinkFlowReturn => { + const { run, stop } = Poller(); + + const startEmailLinkFlow = async ({ redirectUrl }: StartEmailLinkFlowParams): Promise => { + if (!this.id) { + clerkVerifyEmailAddressCalledBeforeCreate('SignUp'); + } + await this.prepareEmailAddressVerification({ + strategy: 'email_link', + redirectUrl, + }); + + return new Promise((resolve, reject) => { + void run(() => { + return this.reload() + .then(res => { + const status = res.verifications.emailAddress.status; + if (status === 'verified' || status === 'expired') { + stop(); + resolve(res); + } + }) + .catch(err => { + stop(); + reject(err); + }); + }); + }); + }; + + return { startEmailLinkFlow, cancelEmailLinkFlow: stop }; + }; + preparePhoneNumberVerification = (params?: PreparePhoneNumberVerificationParams): Promise => { return this.prepareVerification(params || { strategy: 'phone_code' }); }; diff --git a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx index 417f3adb3d8..188eee35a69 100644 --- a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx +++ b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx @@ -1,4 +1,4 @@ -import { isMagicLinkError, MagicLinkErrorCode } from '@clerk/shared'; +import { EmailLinkErrorCode, isEmailLinkError } from '@clerk/shared'; import React from 'react'; import type { VerificationStatus } from '../../utils/getClerkQueryParam'; @@ -19,7 +19,7 @@ export type EmailLinkVerifyProps = { export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { const { redirectUrl, redirectUrlComplete, verifyEmailPath, verifyPhonePath } = props; - const { handleMagicLinkVerification } = useCoreClerk(); + const { handleEmailLinkVerification } = useCoreClerk(); const { navigate } = useRouter(); const signUp = useCoreSignUp(); const [verificationStatus, setVerificationStatus] = React.useState('loading'); @@ -28,7 +28,7 @@ export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { try { // Avoid loading flickering await sleep(750); - await handleMagicLinkVerification({ redirectUrlComplete, redirectUrl }, navigate); + await handleEmailLinkVerification({ redirectUrlComplete, redirectUrl }, navigate); setVerificationStatus('verified_switch_tab'); await sleep(750); return completeSignUpFlow({ @@ -39,7 +39,7 @@ export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { }); } catch (err) { let status: VerificationStatus = 'failed'; - if (isMagicLinkError(err) && err.code === MagicLinkErrorCode.Expired) { + if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = 'expired'; } setVerificationStatus(status); diff --git a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts b/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts index d0dc1f6c9ca..31cff699efc 100644 --- a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts +++ b/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts @@ -1,21 +1,21 @@ -import { buildMagicLinkRedirectUrl, buildSSOCallbackURL } from '../redirects'; +import { buildEmailLinkRedirectUrl, buildSSOCallbackURL } from '../redirects'; -describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { +describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => { it('handles empty routing strategy based routing ', function () { - expect(buildMagicLinkRedirectUrl({ path: '', authQueryString: '' } as any, '')).toBe('http://localhost/#/verify'); + expect(buildEmailLinkRedirectUrl({ path: '', authQueryString: '' } as any, '')).toBe('http://localhost/#/verify'); }); it('returns the magic link redirect url for components using path based routing ', function () { - expect(buildMagicLinkRedirectUrl({ routing: 'path', authQueryString: '' } as any, '')).toBe( + expect(buildEmailLinkRedirectUrl({ routing: 'path', authQueryString: '' } as any, '')).toBe( 'http://localhost/verify', ); - expect(buildMagicLinkRedirectUrl({ routing: 'path', path: '/sign-in', authQueryString: '' } as any, '')).toBe( + expect(buildEmailLinkRedirectUrl({ routing: 'path', path: '/sign-in', authQueryString: '' } as any, '')).toBe( 'http://localhost/sign-in/verify', ); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'path', path: '', @@ -26,7 +26,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/verify?redirectUrl=https://clerk.com'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'path', path: '/sign-in', @@ -37,7 +37,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'path', path: '/sign-in', @@ -50,7 +50,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { it('returns the magic link redirect url for components using hash based routing ', function () { expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'hash', authQueryString: '', @@ -60,7 +60,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/#/verify'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'hash', path: '/sign-in', @@ -71,7 +71,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/#/verify'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'hash', path: '', @@ -82,7 +82,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'hash', path: '/sign-in', @@ -93,7 +93,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'hash', path: '/sign-in', @@ -106,7 +106,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { it('returns the magic link redirect url for components using virtual routing ', function () { expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'virtual', authQueryString: 'redirectUrl=https://clerk.com', @@ -116,7 +116,7 @@ describe('buildMagicLinkRedirectUrl(routing, baseUrl)', () => { ).toBe('https://accounts.clerk.com/sign-in#/verify?redirectUrl=https://clerk.com'); expect( - buildMagicLinkRedirectUrl( + buildEmailLinkRedirectUrl( { routing: 'virtual', } as any, diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 692f03eec02..c5bf2858cfe 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -4,7 +4,7 @@ import type { SignInContextType, SignUpContextType, UserProfileContextType } fro const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; -export function buildMagicLinkRedirectUrl( +export function buildEmailLinkRedirectUrl( ctx: SignInContextType | SignUpContextType | UserProfileContextType, baseUrl: string | undefined = '', ): string { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx index 57fa2a5a37c..44acdc7086d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx @@ -2,13 +2,13 @@ import type { EmailLinkFactor, SignInResource } from '@clerk/types'; import React from 'react'; import { EmailLinkStatusCard } from '../../common'; -import { buildMagicLinkRedirectUrl } from '../../common/redirects'; +import { buildEmailLinkRedirectUrl } from '../../common/redirects'; import { useCoreClerk, useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; import { Flow, localizationKeys, useLocalizations } from '../../customizables'; import type { VerificationCodeCardProps } from '../../elements'; import { VerificationLinkCard } from '../../elements'; import { useCardState } from '../../elements/contexts'; -import { useMagicLink } from '../../hooks/useMagicLink'; +import { useEmailLink } from '../../hooks/useEmailLink'; import { useRouter } from '../../router/RouteContext'; import { handleError } from '../../utils'; @@ -27,7 +27,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard const { navigate } = useRouter(); const { navigateAfterSignIn } = useSignInContext(); const { setActive } = useCoreClerk(); - const { startMagicLinkFlow, cancelMagicLinkFlow } = useMagicLink(signIn); + const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signIn); const [showVerifyModal, setShowVerifyModal] = React.useState(false); React.useEffect(() => { @@ -35,14 +35,14 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard }, []); const restartVerification = () => { - cancelMagicLinkFlow(); + cancelEmailLinkFlow(); void startEmailLinkVerification(); }; const startEmailLinkVerification = () => { - startMagicLinkFlow({ + startEmailLinkFlow({ emailAddressId: props.factor.emailAddressId, - redirectUrl: buildMagicLinkRedirectUrl(signInContext, signInUrl), + redirectUrl: buildEmailLinkRedirectUrl(signInContext, signInUrl), }) .then(res => handleVerificationResult(res)) .catch(err => handleError(err, [], card.setError)); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index e21d571192f..da106ec0507 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -276,16 +276,16 @@ describe('SignInFactorOne', () => { it('shows message to use the magic link in their email', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress(); - f.withMagicLink(); + f.withEmailLink(); f.startSignInWithEmailAddress({ supportEmailLink: true, supportPassword: false }); }); fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); - fixtures.signIn.createMagicLinkFlow.mockImplementation( + fixtures.signIn.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); @@ -298,16 +298,16 @@ describe('SignInFactorOne', () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress(); - f.withMagicLink(); + f.withEmailLink(); f.startSignInWithEmailAddress({ supportEmailLink: true, supportPassword: false }); }); fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); - fixtures.signIn.createMagicLinkFlow.mockImplementation( + fixtures.signIn.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); @@ -594,11 +594,11 @@ describe('SignInFactorOne', () => { f.startSignInWithEmailAddress({ supportEmailLink: true, identifier: email }); }); fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); - fixtures.signIn.createMagicLinkFlow.mockImplementation( + fixtures.signIn.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); const { userEvent } = render(, { wrapper }); @@ -618,9 +618,9 @@ describe('SignInFactorOne', () => { f.startSignInWithEmailAddress({ supportEmailCode: true, identifier: email }); }); fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); - fixtures.signIn.createMagicLinkFlow.mockReturnValue({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + fixtures.signIn.createEmailLinkFlow.mockReturnValue({ + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any); const { userEvent } = render(, { wrapper }); await userEvent.click(screen.getByText('Use another method')); @@ -638,9 +638,9 @@ describe('SignInFactorOne', () => { f.startSignInWithPhoneNumber({ supportPhoneCode: true }); }); fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); - fixtures.signIn.createMagicLinkFlow.mockReturnValue({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + fixtures.signIn.createEmailLinkFlow.mockReturnValue({ + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any); const { userEvent } = render(, { wrapper }); await userEvent.click(screen.getByText('Use another method')); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx index b5b19527cbe..b852bf181b2 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx @@ -2,12 +2,12 @@ import type { SignUpResource } from '@clerk/types'; import React from 'react'; import { EmailLinkStatusCard } from '../../common'; -import { buildMagicLinkRedirectUrl } from '../../common/redirects'; +import { buildEmailLinkRedirectUrl } from '../../common/redirects'; import { useCoreClerk, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { Flow, localizationKeys, useLocalizations } from '../../customizables'; import { VerificationLinkCard } from '../../elements'; import { useCardState } from '../../elements/contexts'; -import { useMagicLink } from '../../hooks/useMagicLink'; +import { useEmailLink } from '../../hooks/useEmailLink'; import { useRouter } from '../../router'; import { handleError } from '../../utils'; import { completeSignUpFlow } from './util'; @@ -23,19 +23,19 @@ export const SignUpEmailLinkCard = () => { const { setActive } = useCoreClerk(); const [showVerifyModal, setShowVerifyModal] = React.useState(false); - const { startMagicLinkFlow, cancelMagicLinkFlow } = useMagicLink(signUp); + const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signUp); React.useEffect(() => { void startEmailLinkVerification(); }, []); const restartVerification = () => { - cancelMagicLinkFlow(); + cancelEmailLinkFlow(); void startEmailLinkVerification(); }; const startEmailLinkVerification = () => { - return startMagicLinkFlow({ redirectUrl: buildMagicLinkRedirectUrl(signUpContext, displayConfig.signUpUrl) }) + return startEmailLinkFlow({ redirectUrl: buildEmailLinkRedirectUrl(signUpContext, displayConfig.signUpUrl) }) .then(res => handleVerificationResult(res)) .catch(err => { handleError(err, [], card.setError); diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpEmailLinkFlowComplete.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpEmailLinkFlowComplete.test.tsx index 040dd0f9f53..a73189666c9 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpEmailLinkFlowComplete.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpEmailLinkFlowComplete.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { MagicLinkError, MagicLinkErrorCode } from '../../../../core/resources'; +import { EmailLinkError, EmailLinkErrorCode } from '../../../../core/resources'; import { bindCreateFixtures, render, runFakeTimers, screen, waitFor } from '../../../../testUtils'; import { SignUpEmailLinkFlowComplete } from '../../../common/EmailLinkCompleteFlowCard'; @@ -24,7 +24,7 @@ describe('SignUpEmailLinkFlowComplete', () => { await runFakeTimers(async timers => { render(, { wrapper }); timers.runOnlyPendingTimers(); - await waitFor(() => expect(fixtures.clerk.handleMagicLinkVerification).toHaveBeenCalled()); + await waitFor(() => expect(fixtures.clerk.handleEmailLinkVerification).toHaveBeenCalled()); }); }); @@ -36,7 +36,7 @@ describe('SignUpEmailLinkFlowComplete', () => { await runFakeTimers(async timers => { render(, { wrapper }); timers.runOnlyPendingTimers(); - await waitFor(() => expect(fixtures.clerk.handleMagicLinkVerification).toHaveBeenCalled()); + await waitFor(() => expect(fixtures.clerk.handleEmailLinkVerification).toHaveBeenCalled()); screen.getByText(/success/i); }); }); @@ -47,16 +47,16 @@ describe('SignUpEmailLinkFlowComplete', () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress({ required: true }); }); - fixtures.clerk.handleMagicLinkVerification.mockImplementationOnce( + fixtures.clerk.handleEmailLinkVerification.mockImplementationOnce( await Promise.resolve(() => { - throw new MagicLinkError(MagicLinkErrorCode.Expired); + throw new EmailLinkError(EmailLinkErrorCode.Expired); }), ); await runFakeTimers(async timers => { render(, { wrapper }); timers.runOnlyPendingTimers(); - await waitFor(() => expect(fixtures.clerk.handleMagicLinkVerification).toHaveBeenCalled()); + await waitFor(() => expect(fixtures.clerk.handleEmailLinkVerification).toHaveBeenCalled()); screen.getByText(/expired/i); }); }); @@ -65,15 +65,15 @@ describe('SignUpEmailLinkFlowComplete', () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress({ required: true }); }); - fixtures.clerk.handleMagicLinkVerification.mockImplementationOnce( + fixtures.clerk.handleEmailLinkVerification.mockImplementationOnce( await Promise.resolve(() => { - throw new MagicLinkError(MagicLinkErrorCode.Failed); + throw new EmailLinkError(EmailLinkErrorCode.Failed); }), ); await runFakeTimers(async timers => { render(, { wrapper }); timers.runOnlyPendingTimers(); - await waitFor(() => expect(fixtures.clerk.handleMagicLinkVerification).toHaveBeenCalled()); + await waitFor(() => expect(fixtures.clerk.handleEmailLinkVerification).toHaveBeenCalled()); screen.getByText(/invalid/i); }); }); diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx index d9cb0d95a75..6c74f029ae3 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx @@ -26,11 +26,11 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_link'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.dev' }); }); - fixtures.signUp.createMagicLinkFlow.mockImplementation( + fixtures.signUp.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); @@ -43,11 +43,11 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_code'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.dev' }); }); - fixtures.signUp.createMagicLinkFlow.mockImplementation( + fixtures.signUp.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); @@ -74,11 +74,11 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_link'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.dev' }); }); - fixtures.signUp.createMagicLinkFlow.mockImplementation( + fixtures.signUp.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); @@ -92,11 +92,11 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_code'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.dev' }); }); - fixtures.signUp.createMagicLinkFlow.mockImplementation( + fixtures.signUp.createEmailLinkFlow.mockImplementation( () => ({ - startMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelMagicLinkFlow: jest.fn(() => new Promise(() => ({}))), + startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), + cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), } as any), ); diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailPage.tsx index b7fcf230e32..2a43f931c4a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailPage.tsx @@ -17,7 +17,7 @@ export const EmailPage = withCardStateProvider(() => { const card = useCardState(); const user = useCoreUser(); const environment = useEnvironment(); - const preferMagicLinks = magicLinksEnabledForInstance(environment); + const preferEmailLinks = magicLinksEnabledForInstance(environment); const { params } = useRouter(); const { id } = params || {}; @@ -64,7 +64,7 @@ export const EmailPage = withCardStateProvider(() => { { headerTitle={title} Breadcrumbs={UserProfileBreadcrumbs} > - {preferMagicLinks ? ( + {preferEmailLinks ? ( { { const { email, nextStep } = props; const card = useCardState(); const profileContext = useUserProfileContext(); - const { startMagicLinkFlow } = useMagicLink(email); + const { startEmailLinkFlow } = useEmailLink(email); const { displayConfig } = useEnvironment(); React.useEffect(() => { @@ -36,8 +36,8 @@ export const VerifyWithLink = (props: VerifyWithLinkProps) => { const { routing } = profileContext; const baseUrl = routing === 'virtual' ? displayConfig.userProfileUrl : ''; - const redirectUrl = buildMagicLinkRedirectUrl(profileContext, baseUrl); - startMagicLinkFlow({ redirectUrl }) + const redirectUrl = buildEmailLinkRedirectUrl(profileContext, baseUrl); + startEmailLinkFlow({ redirectUrl }) .then(() => nextStep()) .catch(err => handleError(err, [], card.setError)); } diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index e68d3360978..26e551acccb 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -1,7 +1,7 @@ export * from './useDelayedVisibility'; export * from './useSaml'; export * from './useWindowEventListener'; -export * from './useMagicLink'; +export * from './useEmailLink'; export * from './useClipboard'; export * from './useEnabledThirdPartyProviders'; export * from './useFetch'; diff --git a/packages/clerk-js/src/ui/hooks/useEmailLink.ts b/packages/clerk-js/src/ui/hooks/useEmailLink.ts new file mode 100644 index 00000000000..35dd8f83142 --- /dev/null +++ b/packages/clerk-js/src/ui/hooks/useEmailLink.ts @@ -0,0 +1,34 @@ +import type { + CreateEmailLinkFlowReturn, + EmailAddressResource, + SignInResource, + SignInStartEmailLinkFlowParams, + SignUpResource, + StartEmailLinkFlowParams, +} from '@clerk/types'; +import React from 'react'; + +type EmailLinkable = SignUpResource | EmailAddressResource | SignInResource; +type UseEmailLinkSignInReturn = CreateEmailLinkFlowReturn; +type UseEmailLinkSignUpReturn = CreateEmailLinkFlowReturn; +type UseEmailLinkEmailAddressReturn = CreateEmailLinkFlowReturn; + +function useEmailLink(resource: SignInResource): UseEmailLinkSignInReturn; +function useEmailLink(resource: SignUpResource): UseEmailLinkSignUpReturn; +function useEmailLink(resource: EmailAddressResource): UseEmailLinkEmailAddressReturn; +function useEmailLink( + resource: EmailLinkable, +): UseEmailLinkSignInReturn | UseEmailLinkSignUpReturn | UseEmailLinkEmailAddressReturn { + const { startEmailLinkFlow, cancelEmailLinkFlow } = React.useMemo(() => resource.createEmailLinkFlow(), [resource]); + + React.useEffect(() => { + return cancelEmailLinkFlow; + }, []); + + return { + startEmailLinkFlow, + cancelEmailLinkFlow, + } as UseEmailLinkSignInReturn | UseEmailLinkSignUpReturn | UseEmailLinkEmailAddressReturn; +} + +export { useEmailLink }; diff --git a/packages/clerk-js/src/ui/hooks/useMagicLink.ts b/packages/clerk-js/src/ui/hooks/useMagicLink.ts deleted file mode 100644 index 43db227078e..00000000000 --- a/packages/clerk-js/src/ui/hooks/useMagicLink.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { - CreateMagicLinkFlowReturn, - EmailAddressResource, - SignInResource, - SignInStartMagicLinkFlowParams, - SignUpResource, - StartMagicLinkFlowParams, -} from '@clerk/types'; -import React from 'react'; - -type MagicLinkable = SignUpResource | EmailAddressResource | SignInResource; -type UseMagicLinkSignInReturn = CreateMagicLinkFlowReturn; -type UseMagicLinkSignUpReturn = CreateMagicLinkFlowReturn; -type UseMagicLinkEmailAddressReturn = CreateMagicLinkFlowReturn; - -function useMagicLink(resource: SignInResource): UseMagicLinkSignInReturn; -function useMagicLink(resource: SignUpResource): UseMagicLinkSignUpReturn; -function useMagicLink(resource: EmailAddressResource): UseMagicLinkEmailAddressReturn; -function useMagicLink( - resource: MagicLinkable, -): UseMagicLinkSignInReturn | UseMagicLinkSignUpReturn | UseMagicLinkEmailAddressReturn { - const { startMagicLinkFlow, cancelMagicLinkFlow } = React.useMemo(() => resource.createMagicLinkFlow(), [resource]); - - React.useEffect(() => { - return cancelMagicLinkFlow; - }, []); - - return { - startMagicLinkFlow, - cancelMagicLinkFlow, - } as UseMagicLinkSignInReturn | UseMagicLinkSignUpReturn | UseMagicLinkEmailAddressReturn; -} - -export { useMagicLink }; diff --git a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts index 191827b0bf9..b6f24754680 100644 --- a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts +++ b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts @@ -340,7 +340,7 @@ const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => { }; }; - const withMagicLink = () => { + const withEmailLink = () => { withEmailAddress({ first_factors: ['email_link'], verifications: ['email_link'] }); }; @@ -420,7 +420,7 @@ const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => { return { withEmailAddress, - withMagicLink, + withEmailLink, withPhoneNumber, withUsername, withWeb3Wallet, diff --git a/packages/expo/src/index.ts b/packages/expo/src/index.ts index 4697ceff0a1..c7f0dd6616a 100644 --- a/packages/expo/src/index.ts +++ b/packages/expo/src/index.ts @@ -7,6 +7,7 @@ export { useAuth, useClerk, useMagicLink, + useEmailLink, useOrganization, useOrganizationList, useOrganizations, @@ -23,8 +24,10 @@ export { WithUser, isClerkAPIResponseError, isMagicLinkError, + isEmailLinkError, isMetamaskError, isKnownError, + EmailLinkErrorCode, MagicLinkErrorCode, } from '@clerk/clerk-react'; diff --git a/packages/nextjs/src/app-beta/client/clerk-react.ts b/packages/nextjs/src/app-beta/client/clerk-react.ts index f519e2418b3..bae3acab597 100644 --- a/packages/nextjs/src/app-beta/client/clerk-react.ts +++ b/packages/nextjs/src/app-beta/client/clerk-react.ts @@ -13,6 +13,7 @@ import { SignedOut as _SignedOut, useAuth as _useAuth, useClerk as _useClerk, + useEmailLink as _useEmailLink, useMagicLink as _useMagicLink, useOrganization as _useOrganization, useOrganizationList as _useOrganizationList, @@ -44,7 +45,11 @@ export const useClerk = _useClerk; * For more details, consult the middleware documentation: https://clerk.com/docs/nextjs/middleware */ export const useMagicLink = _useMagicLink; - +/** + * @deprecated Use imports from `@clerk/nextjs` instead. + * For more details, consult the middleware documentation: https://clerk.com/docs/nextjs/middleware + */ +export const useEmailLink = _useEmailLink; /** * @deprecated Use imports from `@clerk/nextjs` instead. * For more details, consult the middleware documentation: https://clerk.com/docs/nextjs/middleware diff --git a/packages/nextjs/src/app-beta/client/index.ts b/packages/nextjs/src/app-beta/client/index.ts index ff30bd699aa..3df75e9c717 100644 --- a/packages/nextjs/src/app-beta/client/index.ts +++ b/packages/nextjs/src/app-beta/client/index.ts @@ -6,6 +6,7 @@ export { useAuth, useClerk, useMagicLink, + useEmailLink, useOrganization, useOrganizationList, useOrganizations, diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index d024051fe9b..d24961083e4 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -11,6 +11,7 @@ export { useOrganization, useOrganizationList, useOrganizations, + useEmailLink, useMagicLink, withUser, withSession, @@ -24,8 +25,10 @@ export type { WithUserProp, WithSessionProp, WithClerkProp } from '@clerk/clerk- export { isClerkAPIResponseError, + EmailLinkErrorCode, MagicLinkErrorCode, isKnownError, isMetamaskError, + isEmailLinkError, isMagicLinkError, } from '@clerk/clerk-react'; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index c1e2261f7a2..ed565c78731 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -48,8 +48,11 @@ export { useOrganization, useOrganizationList, useOrganizations, + useEmailLink, useMagicLink, + EmailLinkErrorCode, MagicLinkErrorCode, + isEmailLinkError, isMagicLinkError, isClerkAPIResponseError, isMetamaskError, diff --git a/packages/react/src/errors.ts b/packages/react/src/errors.ts index 78eb2571fee..739f04142f8 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -1,9 +1,11 @@ export { - isMagicLinkError, MagicLinkErrorCode, + EmailLinkErrorCode, isClerkAPIResponseError, isKnownError, isMetamaskError, + isMagicLinkError, + isEmailLinkError, } from '@clerk/shared'; export const noFrontendApiError = 'Clerk: You must add the frontendApi prop to your '; diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 53c480d7c60..bdbb220379a 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -9,3 +9,4 @@ export * from './useOrganization'; export * from './useOrganizationList'; export * from './useOrganizations'; export * from './useMagicLink'; +export * from './useEmailLink'; diff --git a/packages/react/src/hooks/useEmailLink.ts b/packages/react/src/hooks/useEmailLink.ts new file mode 100644 index 00000000000..35dd8f83142 --- /dev/null +++ b/packages/react/src/hooks/useEmailLink.ts @@ -0,0 +1,34 @@ +import type { + CreateEmailLinkFlowReturn, + EmailAddressResource, + SignInResource, + SignInStartEmailLinkFlowParams, + SignUpResource, + StartEmailLinkFlowParams, +} from '@clerk/types'; +import React from 'react'; + +type EmailLinkable = SignUpResource | EmailAddressResource | SignInResource; +type UseEmailLinkSignInReturn = CreateEmailLinkFlowReturn; +type UseEmailLinkSignUpReturn = CreateEmailLinkFlowReturn; +type UseEmailLinkEmailAddressReturn = CreateEmailLinkFlowReturn; + +function useEmailLink(resource: SignInResource): UseEmailLinkSignInReturn; +function useEmailLink(resource: SignUpResource): UseEmailLinkSignUpReturn; +function useEmailLink(resource: EmailAddressResource): UseEmailLinkEmailAddressReturn; +function useEmailLink( + resource: EmailLinkable, +): UseEmailLinkSignInReturn | UseEmailLinkSignUpReturn | UseEmailLinkEmailAddressReturn { + const { startEmailLinkFlow, cancelEmailLinkFlow } = React.useMemo(() => resource.createEmailLinkFlow(), [resource]); + + React.useEffect(() => { + return cancelEmailLinkFlow; + }, []); + + return { + startEmailLinkFlow, + cancelEmailLinkFlow, + } as UseEmailLinkSignInReturn | UseEmailLinkSignUpReturn | UseEmailLinkEmailAddressReturn; +} + +export { useEmailLink }; diff --git a/packages/react/src/hooks/useMagicLink.ts b/packages/react/src/hooks/useMagicLink.ts index 43db227078e..7203de0aff0 100644 --- a/packages/react/src/hooks/useMagicLink.ts +++ b/packages/react/src/hooks/useMagicLink.ts @@ -13,8 +13,17 @@ type UseMagicLinkSignInReturn = CreateMagicLinkFlowReturn; type UseMagicLinkEmailAddressReturn = CreateMagicLinkFlowReturn; +/** + * @deprecated Use `useEmailLink` instead. + */ function useMagicLink(resource: SignInResource): UseMagicLinkSignInReturn; +/** + * @deprecated Use `useEmailLink` instead. + */ function useMagicLink(resource: SignUpResource): UseMagicLinkSignUpReturn; +/** + * @deprecated Use `useEmailLink` instead. + */ function useMagicLink(resource: EmailAddressResource): UseMagicLinkEmailAddressReturn; function useMagicLink( resource: MagicLinkable, diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 434f77a6027..3a59897df44 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -12,5 +12,14 @@ export type { WithSessionProp, IsomorphicClerkOptions, } from './types'; -export { MagicLinkErrorCode, isClerkAPIResponseError, isKnownError, isMetamaskError, isMagicLinkError } from './errors'; +export { + MagicLinkErrorCode, + EmailLinkErrorCode, + isClerkAPIResponseError, + isKnownError, + isMetamaskError, + isMagicLinkError, + isEmailLinkError, +} from './errors'; export { useMagicLink } from './hooks/useMagicLink'; +export { useEmailLink } from './hooks/useEmailLink'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 9fa62ef93e7..b68ad6411b6 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -8,6 +8,7 @@ import type { CreateOrganizationParams, CreateOrganizationProps, DomainOrProxyUrl, + HandleEmailLinkVerificationParams, HandleMagicLinkVerificationParams, HandleOAuthCallbackParams, ListenerCallback, @@ -667,7 +668,9 @@ export default class IsomorphicClerk { this.premountMethodCalls.set('handleRedirectCallback', callback); } }; - + /** + * @deprecated Use `handleEmailLinkVerification` instead. + */ handleMagicLinkVerification = async (params: HandleMagicLinkVerificationParams): Promise => { const callback = () => this.clerkjs?.handleMagicLinkVerification(params); if (this.clerkjs && this.#loaded) { @@ -677,6 +680,15 @@ export default class IsomorphicClerk { } }; + handleEmailLinkVerification = async (params: HandleEmailLinkVerificationParams): Promise => { + const callback = () => this.clerkjs?.handleEmailLinkVerification(params); + if (this.clerkjs && this.#loaded) { + return callback() as Promise; + } else { + this.premountMethodCalls.set('handleEmailLinkVerification', callback); + } + }; + authenticateWithMetamask = async (params: AuthenticateWithMetamaskParams): Promise => { const callback = () => this.clerkjs?.authenticateWithMetamask(params); if (this.clerkjs && this.#loaded) { diff --git a/packages/shared/src/errors/Error.ts b/packages/shared/src/errors/Error.ts index 2e6fab866ee..256ebbd1a6c 100644 --- a/packages/shared/src/errors/Error.ts +++ b/packages/shared/src/errors/Error.ts @@ -136,6 +136,9 @@ export class ClerkRuntimeError extends Error { }; } +/** + * @deprecated Use `EmailLinkError` instead. + */ export class MagicLinkError extends Error { code: string; @@ -146,13 +149,37 @@ export class MagicLinkError extends Error { } } -// Check if the error is a MagicLinkError. +export class EmailLinkError extends Error { + code: string; + + constructor(code: string) { + super(code); + this.code = code; + Object.setPrototypeOf(this, EmailLinkError.prototype); + } +} +/** + * Check if the error is a MagicLinkError. + * @deprecated Use `isEmailLinkError` instead. + */ export function isMagicLinkError(err: Error): err is MagicLinkError { return err instanceof MagicLinkError; } +export function isEmailLinkError(err: Error): err is EmailLinkError { + return err instanceof EmailLinkError; +} + +/** + * @deprecated Use `EmailLinkErrorCode` instead. + */ export const MagicLinkErrorCode = { Expired: 'expired', Failed: 'failed', }; + +export const EmailLinkErrorCode = { + Expired: 'expired', + Failed: 'failed', +}; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index a88be545796..c92530cb0cf 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -398,12 +398,21 @@ export interface Clerk { /** * Completes a Magic Link flow started by {@link Clerk.client.signIn.createMagicLinkFlow} or {@link Clerk.client.signUp.createMagicLinkFlow} + * @deprecated Use `handleEmailLinkVerification` instead. */ handleMagicLinkVerification: ( params: HandleMagicLinkVerificationParams, customNavigate?: (to: string) => Promise, ) => Promise; + /** + * Completes a Email Link flow started by {@link Clerk.client.signIn.createEmailLinkFlow} or {@link Clerk.client.signUp.createEmailLinkFlow} + */ + handleEmailLinkVerification: ( + params: HandleEmailLinkVerificationParams, + customNavigate?: (to: string) => Promise, + ) => Promise; + /** * Authenticates user using their Metamask browser extension */ @@ -939,6 +948,9 @@ export type OrganizationListProps = { afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; }; +/** + * @deprecated Use `HandleEmailLinkVerificationParams` instead. + */ export interface HandleMagicLinkVerificationParams { /** * Full URL or path to navigate after successful magic link verification @@ -957,6 +969,24 @@ export interface HandleMagicLinkVerificationParams { onVerifiedOnOtherDevice?: () => void; } +export interface HandleEmailLinkVerificationParams { + /** + * Full URL or path to navigate after successful magic link verification + * on completed sign up or sign in on the same device. + */ + redirectUrlComplete?: string; + /** + * Full URL or path to navigate after successful magic link verification + * on the same device, but not completed sign in or sign up. + */ + redirectUrl?: string; + /** + * Callback function to be executed after successful magic link + * verification on another device. + */ + onVerifiedOnOtherDevice?: () => void; +} + export type CreateOrganizationInvitationParams = { emailAddress: string; role: MembershipRole; diff --git a/packages/types/src/emailAddress.ts b/packages/types/src/emailAddress.ts index 809958cdb4a..60b21379583 100644 --- a/packages/types/src/emailAddress.ts +++ b/packages/types/src/emailAddress.ts @@ -1,7 +1,8 @@ import type { IdentificationLinkResource } from './identificationLink'; import type { ClerkResource } from './resource'; import type { EmailCodeStrategy, EmailLinkStrategy } from './strategies'; -import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams, VerificationResource } from './verification'; +import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams } from './verification'; +import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification'; export type PrepareEmailAddressVerificationParams = | { @@ -24,7 +25,12 @@ export interface EmailAddressResource extends ClerkResource { toString: () => string; prepareVerification: (params: PrepareEmailAddressVerificationParams) => Promise; attemptVerification: (params: AttemptEmailAddressVerificationParams) => Promise; + /** + * + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow: () => CreateMagicLinkFlowReturn; + createEmailLinkFlow: () => CreateEmailLinkFlowReturn; destroy: () => Promise; create: () => Promise; } diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 9f4c4ab8563..a0e04802cdf 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -58,7 +58,8 @@ import type { TOTPStrategy, Web3Strategy, } from './strategies'; -import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams, VerificationResource } from './verification'; +import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams } from './verification'; +import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification'; import type { AuthenticateWithWeb3Params } from './web3Wallet'; export interface SignInResource extends ClerkResource { @@ -90,8 +91,14 @@ export interface SignInResource extends ClerkResource { authenticateWithMetamask: () => Promise; + /** + * + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow: () => CreateMagicLinkFlowReturn; + createEmailLinkFlow: () => CreateEmailLinkFlowReturn; + validatePassword: (password: string, callbacks?: ValidatePasswordCallbacks) => void; } @@ -197,10 +204,18 @@ export type ResetPasswordParams = { signOutOfOtherSessions?: boolean; }; +/** + * + * @deprecated Use `SignInStartEmailLinkFlowParams` instead. + */ export interface SignInStartMagicLinkFlowParams extends StartMagicLinkFlowParams { emailAddressId: string; } +export interface SignInStartEmailLinkFlowParams extends StartEmailLinkFlowParams { + emailAddressId: string; +} + export type SignInStrategy = | PasswordStrategy | ResetPasswordPhoneCodeStrategy diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index 9bc8ef2cd3e..024993875ca 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -27,7 +27,8 @@ import type { Web3Strategy, } from './strategies'; import type { SnakeToCamel } from './utils'; -import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams, VerificationResource } from './verification'; +import type { CreateMagicLinkFlowReturn, StartMagicLinkFlowParams } from './verification'; +import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification'; import type { AttemptWeb3WalletVerificationParams, AuthenticateWithWeb3Params } from './web3Wallet'; declare global { @@ -80,9 +81,13 @@ export interface SignUpResource extends ClerkResource { prepareWeb3WalletVerification: () => Promise; attemptWeb3WalletVerification: (params: AttemptWeb3WalletVerificationParams) => Promise; - + /** + * @deprecated Use `createEmailLinkFlow` instead. + */ createMagicLinkFlow: () => CreateMagicLinkFlowReturn; + createEmailLinkFlow: () => CreateEmailLinkFlowReturn; + validatePassword: (password: string, callbacks?: ValidatePasswordCallbacks) => void; authenticateWithRedirect: ( diff --git a/packages/types/src/verification.ts b/packages/types/src/verification.ts index 849c07ddd93..cc92ead85ab 100644 --- a/packages/types/src/verification.ts +++ b/packages/types/src/verification.ts @@ -27,11 +27,26 @@ export interface SignatureVerificationAttemptParam { export type VerificationAttemptParams = CodeVerificationAttemptParam | SignatureVerificationAttemptParam; +/** + * @deprecated Use `StarEmailLinkFlowParams` instead. + */ export interface StartMagicLinkFlowParams { redirectUrl: string; } +export interface StartEmailLinkFlowParams { + redirectUrl: string; +} + +/** + * @deprecated Use `CreateEmailLinkFlowReturn` instead. + */ export type CreateMagicLinkFlowReturn = { startMagicLinkFlow: (params: Params) => Promise; cancelMagicLinkFlow: () => void; }; + +export type CreateEmailLinkFlowReturn = { + startEmailLinkFlow: (params: Params) => Promise; + cancelEmailLinkFlow: () => void; +};