diff --git a/.changeset/chilled-laws-pump.md b/.changeset/chilled-laws-pump.md new file mode 100644 index 00000000000..60358ead22a --- /dev/null +++ b/.changeset/chilled-laws-pump.md @@ -0,0 +1,5 @@ +--- +'@clerk/localizations': minor +--- + +Added new keys for email link verification under `signIn.emailLink.clientMismatch` and `signUp.emailLink.clientMismatch` diff --git a/.changeset/tricky-tomatoes-repair.md b/.changeset/tricky-tomatoes-repair.md new file mode 100644 index 00000000000..59faaebe47b --- /dev/null +++ b/.changeset/tricky-tomatoes-repair.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/types': minor +--- + +Introduce new `client_mismatch` verification status for email link sign-in and sign-up. This error (and its message) will be shown if a verification link was opened in another device/browser from which the user initiated the sign-in/sign-up attempt. This functionality needs to be enabled in the Clerk dashboard. \ No newline at end of file diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d3195a10cd6..1431b178f0d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -981,6 +981,8 @@ export class Clerk implements ClerkInterface { const verificationStatus = getClerkQueryParam('__clerk_status'); if (verificationStatus === 'expired') { throw new EmailLinkError(EmailLinkErrorCode.Expired); + } else if (verificationStatus === 'client_mismatch') { + throw new EmailLinkError(EmailLinkErrorCode.ClientMismatch); } else if (verificationStatus !== 'verified') { throw new EmailLinkError(EmailLinkErrorCode.Failed); } diff --git a/packages/clerk-js/src/ui/common/EmailLinkCompleteFlowCard.tsx b/packages/clerk-js/src/ui/common/EmailLinkCompleteFlowCard.tsx index af37d800d85..2d376c50481 100644 --- a/packages/clerk-js/src/ui/common/EmailLinkCompleteFlowCard.tsx +++ b/packages/clerk-js/src/ui/common/EmailLinkCompleteFlowCard.tsx @@ -24,6 +24,10 @@ const signInLocalizationKeys = { title: localizationKeys('signIn.emailLink.expired.title'), subtitle: localizationKeys('signIn.emailLink.expired.subtitle'), }, + client_mismatch: { + title: localizationKeys('signIn.emailLink.clientMismatch.title'), + subtitle: localizationKeys('signIn.emailLink.clientMismatch.subtitle'), + }, }; const signUpLocalizationKeys = { @@ -40,6 +44,10 @@ const signUpLocalizationKeys = { ...signInLocalizationKeys.loading, title: localizationKeys('signUp.emailLink.loading.title'), }, + client_mismatch: { + ...signInLocalizationKeys.client_mismatch, + subtitle: localizationKeys('signUp.emailLink.clientMismatch.subtitle'), + }, }; export const SignInEmailLinkFlowComplete = withCardStateProvider((props: Omit) => { diff --git a/packages/clerk-js/src/ui/common/EmailLinkStatusCard.tsx b/packages/clerk-js/src/ui/common/EmailLinkStatusCard.tsx index 0f66044b757..dae369a3010 100644 --- a/packages/clerk-js/src/ui/common/EmailLinkStatusCard.tsx +++ b/packages/clerk-js/src/ui/common/EmailLinkStatusCard.tsx @@ -20,6 +20,7 @@ const StatusToIcon: Record, React.Compone verified_switch_tab: SwitchArrows, expired: ExclamationTriangle, failed: ExclamationTriangle, + client_mismatch: ExclamationTriangle, }; const statusToColor = (theme: InternalTheme, status: Exclude) => @@ -28,6 +29,7 @@ const statusToColor = (theme: InternalTheme, status: Exclude { diff --git a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx index 5aec89f70ba..9f0c08110c9 100644 --- a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx +++ b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx @@ -43,6 +43,9 @@ export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = 'expired'; } + if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.ClientMismatch) { + status = 'client_mismatch'; + } setVerificationStatus(status); } }; diff --git a/packages/clerk-js/src/utils/getClerkQueryParam.ts b/packages/clerk-js/src/utils/getClerkQueryParam.ts index 6df43a62965..d2973fb6d35 100644 --- a/packages/clerk-js/src/utils/getClerkQueryParam.ts +++ b/packages/clerk-js/src/utils/getClerkQueryParam.ts @@ -26,7 +26,13 @@ type ClerkQueryParamsToValuesMap = { __clerk_help: string; }; -export type VerificationStatus = 'expired' | 'failed' | 'loading' | 'verified' | 'verified_switch_tab'; +export type VerificationStatus = + | 'expired' + | 'failed' + | 'loading' + | 'verified' + | 'verified_switch_tab' + | 'client_mismatch'; export function getClerkQueryParam(param: T): ClerkQueryParamsToValuesMap[T] | null { const val = new URL(window.location.href).searchParams.get(param); diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index b442b48db20..db218de77fa 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -325,6 +325,11 @@ export const enUS: LocalizationResource = { subtitleNewTab: 'Return to the newly opened tab to continue', titleNewTab: 'Signed in on other tab', }, + clientMismatch: { + subtitle: + 'To continue, open the verification link on the device and browser from which you initiated the sign-in', + title: 'Verification link is invalid for this device', + }, }, forgotPassword: { formTitle: 'Reset password code', @@ -426,6 +431,11 @@ export const enUS: LocalizationResource = { subtitleNewTab: 'Return to previous tab to continue', title: 'Successfully verified email', }, + clientMismatch: { + subtitle: + 'To continue, open the verification link on the device and browser from which you initiated the sign-up', + title: 'Verification link is invalid for this device', + }, }, phoneCode: { formSubtitle: 'Enter the verification code sent to your phone number', diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index c7c88c56402..dd3fac4e4b3 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -188,6 +188,7 @@ export function isEmailLinkError(err: Error): err is EmailLinkError { export const EmailLinkErrorCode = { Expired: 'expired', Failed: 'failed', + ClientMismatch: 'client_mismatch', }; const DefaultMessages = Object.freeze({ diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 59acbf76263..ad1295db624 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -117,6 +117,10 @@ type _LocalizationResource = { subtitle: LocalizationValue; subtitleNewTab: LocalizationValue; }; + clientMismatch: { + title: LocalizationValue; + subtitle: LocalizationValue; + }; }; emailCode: { title: LocalizationValue; @@ -221,6 +225,10 @@ type _LocalizationResource = { title: LocalizationValue; subtitle: LocalizationValue; }; + clientMismatch: { + title: LocalizationValue; + subtitle: LocalizationValue; + }; }; phoneCode: { title: LocalizationValue;