Skip to content

feat(clerk-js,localizations,types): Enable email verification in UserProfile via enterprise SSO #4406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
506d032
wip
NicolasLopes7 Oct 24, 2024
f2deba4
Use enterprise SSO as strategy
LauraBeatris Dec 2, 2024
9cf7052
Rename verification hook to enterprise SSO
LauraBeatris Dec 2, 2024
52c74f2
Rename `matchesEnterpriseConnection` to `hasEnterpriseSso`
LauraBeatris Dec 3, 2024
5728883
fix types
NicolasLopes7 Dec 3, 2024
ce5b660
Introduce localization key for enterprise SSO
LauraBeatris Dec 3, 2024
d733090
Do not declare verification success component within enterprise context
LauraBeatris Dec 3, 2024
38bbabc
Add changeset
LauraBeatris Dec 3, 2024
2293ab4
Define noopener when opening external verification URL
LauraBeatris Dec 3, 2024
24094ff
Remove `console.log`
LauraBeatris Dec 3, 2024
7c5b49d
Fix translation key for email code
LauraBeatris Dec 4, 2024
78fa7e0
Use unified form hint for all verification strategies
LauraBeatris Dec 4, 2024
c6eaa66
Update test with latest translation key
LauraBeatris Dec 4, 2024
ef40cde
Re-generate localization files
LauraBeatris Dec 4, 2024
eea850c
rename: `has_enterprise_sso` -> `matches_sso_connection`
NicolasLopes7 Dec 5, 2024
c3cbcfa
Fix gap between provider and email address
LauraBeatris Dec 6, 2024
a5815ac
Fix rebase
LauraBeatris Dec 18, 2024
fddfdda
Update changeset
LauraBeatris Dec 18, 2024
3313081
chore: Preserve `UserProfile` modal behavior
LauraBeatris Jan 17, 2025
21d5b9b
chore: Preserve `UserProfile` modal behavior
LauraBeatris Jan 17, 2025
e899573
Update pt-BR translation
LauraBeatris Jan 20, 2025
9944630
Cover de-DE translation
LauraBeatris Jan 20, 2025
7789738
Update UI test
LauraBeatris Jan 20, 2025
b0174d1
Adjust SSO casing
LauraBeatris Jan 20, 2025
58318d5
Serve previous form hints for older clerk-js versions
LauraBeatris Jan 21, 2025
9cc2331
Update error message for `EmailAddress` type
LauraBeatris Jan 21, 2025
b32cf69
Add JSDocs to clarify strategy resolution
LauraBeatris Jan 21, 2025
00101f0
Refactor flow to initiate SSO on button click
LauraBeatris Jan 21, 2025
95f19a3
Improve copy of form hint translation
LauraBeatris Jan 21, 2025
315276c
Remove unnecessary error from `EmailAddress` resource
LauraBeatris Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-jobs-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/types': minor
---

Deprecated `userProfile.emailAddressPage.emailLink.formHint` and `userProfile.emailAddressPage.emailCode.formHint` in favor of `userProfile.emailAddressPage.formHint`
5 changes: 5 additions & 0 deletions .changeset/funny-camels-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/localizations': minor
---

Unified `formHint` under `userProfile.emailAddressPage` for all first factor auth methods
5 changes: 5 additions & 0 deletions .changeset/ninety-colts-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': minor
---

Enable email verification in `UserProfile` via enterprise SSO
44 changes: 40 additions & 4 deletions packages/clerk-js/src/core/resources/EmailAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import { Poller } from '@clerk/shared/poller';
import type {
AttemptEmailAddressVerificationParams,
CreateEmailLinkFlowReturn,
CreateEnterpriseSSOLinkFlowReturn,
EmailAddressJSON,
EmailAddressJSONSnapshot,
EmailAddressResource,
IdentificationLinkResource,
PrepareEmailAddressVerificationParams,
StartEmailLinkFlowParams,
StartEnterpriseSSOLinkFlowParams,
VerificationResource,
} from '@clerk/types';

import { clerkVerifyEmailAddressCalledBeforeCreate } from '../errors';
import { BaseResource, IdentificationLink, Verification } from './internal';

export class EmailAddress extends BaseResource implements EmailAddressResource {
id!: string;
emailAddress = '';
matchesSsoConnection = false;
linkedTo: IdentificationLinkResource[] = [];
verification!: VerificationResource;

Expand Down Expand Up @@ -52,9 +54,6 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
const { run, stop } = Poller();

const startEmailLinkFlow = async ({ redirectUrl }: StartEmailLinkFlowParams): Promise<EmailAddressResource> => {
if (!this.id) {
clerkVerifyEmailAddressCalledBeforeCreate('SignUp');
}
await this.prepareVerification({
strategy: 'email_link',
redirectUrl: redirectUrl,
Expand All @@ -78,6 +77,41 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
return { startEmailLinkFlow, cancelEmailLinkFlow: stop };
};

createEnterpriseSSOLinkFlow = (): CreateEnterpriseSSOLinkFlowReturn<
StartEnterpriseSSOLinkFlowParams,
EmailAddressResource
> => {
const { run, stop } = Poller();

const startEnterpriseSSOLinkFlow = async ({
redirectUrl,
}: StartEnterpriseSSOLinkFlowParams): Promise<EmailAddressResource> => {
const response = await this.prepareVerification({
strategy: 'enterprise_sso',
redirectUrl,
});
if (!response.verification.externalVerificationRedirectURL) {
throw Error('Unexpected: External verification redirect URL is missing');
}
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 { startEnterpriseSSOLinkFlow, cancelEnterpriseSSOLinkFlow: stop };
};

destroy = (): Promise<void> => this._baseDelete();

toString = (): string => this.emailAddress;
Expand All @@ -90,6 +124,7 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
this.id = data.id;
this.emailAddress = data.email_address;
this.verification = new Verification(data.verification);
this.matchesSsoConnection = data.matches_sso_connection;
this.linkedTo = (data.linked_to || []).map(link => new IdentificationLink(link));
return this;
}
Expand All @@ -101,6 +136,7 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
email_address: this.emailAddress,
verification: this.verification.__internal_toSnapshot(),
linked_to: this.linkedTo.map(link => link.__internal_toSnapshot()),
matches_sso_connection: this.matchesSsoConnection,
};
}
}
34 changes: 17 additions & 17 deletions packages/clerk-js/src/ui/common/__tests__/redirects.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { buildEmailLinkRedirectUrl, buildSSOCallbackURL } from '../redirects';
import { buildSSOCallbackURL, buildVerificationRedirectUrl, buildVerificationRedirectUrl } from '../redirects';

describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
describe('buildVerificationRedirectUrl(routing, baseUrl)', () => {
it('defaults to hash based routing strategy on empty routing', function () {
expect(
buildEmailLinkRedirectUrl({ ctx: { path: '', authQueryString: '' } as any, baseUrl: '', intent: 'sign-in' }),
buildVerificationRedirectUrl({ ctx: { path: '', authQueryString: '' } as any, baseUrl: '', intent: 'sign-in' }),
).toBe('http://localhost/#/verify');
});

it('returns the magic link redirect url for components using path based routing ', function () {
expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: { routing: 'path', authQueryString: '' } as any,
baseUrl: '',
intent: 'sign-in',
}),
).toBe('http://localhost/verify');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: { routing: 'path', path: '/sign-in', authQueryString: '' } as any,
baseUrl: '',
intent: 'sign-in',
}),
).toBe('http://localhost/sign-in/verify');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'path',
path: '',
Expand All @@ -37,7 +37,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/verify?redirectUrl=https://clerk.com');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'path',
path: '/sign-in',
Expand All @@ -49,7 +49,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'path',
path: '/sign-in',
Expand All @@ -63,7 +63,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {

it('returns the magic link redirect url for components using hash based routing ', function () {
expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'hash',
authQueryString: '',
Expand All @@ -74,7 +74,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/#/verify');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'hash',
path: '/sign-in',
Expand All @@ -86,7 +86,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/#/verify');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'hash',
path: '',
Expand All @@ -98,7 +98,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'hash',
path: '/sign-in',
Expand All @@ -110,7 +110,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'hash',
path: '/sign-in',
Expand All @@ -124,7 +124,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {

it('returns the magic link redirect url for components using virtual routing ', function () {
expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'virtual',
authQueryString: 'redirectUrl=https://clerk.com',
Expand All @@ -135,7 +135,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('https://accounts.clerk.com/sign-in#/verify?redirectUrl=https://clerk.com');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'virtual',
} as any,
Expand All @@ -147,7 +147,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {

it('returns the magic link redirect url for components using the combined flow based on intent', function () {
expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'path',
path: '/sign-up',
Expand All @@ -159,7 +159,7 @@ describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => {
).toBe('http://localhost/sign-up/create/verify');

expect(
buildEmailLinkRedirectUrl({
buildVerificationRedirectUrl({
ctx: {
routing: 'path',
path: '/sign-in',
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/common/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { SignInContextType, SignUpContextType, UserProfileContextType } fro
export const SSO_CALLBACK_PATH_ROUTE = '/sso-callback';
export const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify';

export function buildEmailLinkRedirectUrl({
export function buildVerificationRedirectUrl({
ctx,
baseUrl = '',
intent = 'sign-in',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EmailLinkFactor, SignInResource } from '@clerk/types';
import React from 'react';

import { EmailLinkStatusCard } from '../../common';
import { buildEmailLinkRedirectUrl } from '../../common/redirects';
import { buildVerificationRedirectUrl } from '../../common/redirects';
import { useCoreSignIn, useSignInContext } from '../../contexts';
import { Flow, localizationKeys, useLocalizations } from '../../customizables';
import type { VerificationCodeCardProps } from '../../elements';
Expand Down Expand Up @@ -45,7 +45,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard
const startEmailLinkVerification = () => {
startEmailLinkFlow({
emailAddressId: props.factor.emailAddressId,
redirectUrl: buildEmailLinkRedirectUrl({ ctx: signInContext, baseUrl: signInUrl, intent: 'sign-in' }),
redirectUrl: buildVerificationRedirectUrl({ ctx: signInContext, baseUrl: signInUrl, intent: 'sign-in' }),
})
.then(res => handleVerificationResult(res))
.catch(err => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) =>
<ImageOrInitial />
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<Flex
gap={2}
gap={1}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes an issue with the spacing between the provider and email address:

With gap 1:

CleanShot 2024-12-06 at 07 38 19

With gap 2:

CleanShot 2024-12-06 at 07 37 55

center
>
<Text sx={t => ({ color: t.colors.$colorText })}>{`${
Expand Down
Loading
Loading