Skip to content

Commit 299978a

Browse files
NicolasLopes7LauraBeatris
authored andcommitted
wip
1 parent 666953d commit 299978a

File tree

10 files changed

+200
-7
lines changed

10 files changed

+200
-7
lines changed

packages/clerk-js/src/core/resources/EmailAddress.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { Poller } from '@clerk/shared/poller';
22
import type {
33
AttemptEmailAddressVerificationParams,
44
CreateEmailLinkFlowReturn,
5+
CreateEnterpriseConnectionLinkFlowReturn,
56
EmailAddressJSON,
67
EmailAddressResource,
78
IdentificationLinkResource,
89
PrepareEmailAddressVerificationParams,
910
StartEmailLinkFlowParams,
11+
StartEnterpriseConnectionLinkFlowParams,
1012
VerificationResource,
1113
} from '@clerk/types';
1214

@@ -16,6 +18,7 @@ import { BaseResource, IdentificationLink, Verification } from './internal';
1618
export class EmailAddress extends BaseResource implements EmailAddressResource {
1719
id!: string;
1820
emailAddress = '';
21+
matchesEnterpriseConnection = false;
1922
linkedTo: IdentificationLinkResource[] = [];
2023
verification!: VerificationResource;
2124

@@ -77,6 +80,45 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
7780
return { startEmailLinkFlow, cancelEmailLinkFlow: stop };
7881
};
7982

83+
createEnterpriseConnectionLinkFlow = (): CreateEnterpriseConnectionLinkFlowReturn<
84+
StartEnterpriseConnectionLinkFlowParams,
85+
EmailAddressResource
86+
> => {
87+
const { run, stop } = Poller();
88+
89+
const startEnterpriseConnectionLinkFlow = async ({
90+
redirectUrl,
91+
}: StartEnterpriseConnectionLinkFlowParams): Promise<EmailAddressResource> => {
92+
if (!this.id) {
93+
clerkVerifyEmailAddressCalledBeforeCreate('SignUp');
94+
}
95+
const response = await this.prepareVerification({
96+
strategy: 'saml',
97+
redirectUrl: redirectUrl,
98+
});
99+
if (!response.verification.externalVerificationRedirectURL) {
100+
throw Error('Unexpected: External verification redirect URL is missing');
101+
}
102+
window.open(response.verification.externalVerificationRedirectURL, '_blank');
103+
return new Promise((resolve, reject) => {
104+
void run(() => {
105+
return this.reload()
106+
.then(res => {
107+
if (res.verification.status === 'verified') {
108+
stop();
109+
resolve(res);
110+
}
111+
})
112+
.catch(err => {
113+
stop();
114+
reject(err);
115+
});
116+
});
117+
});
118+
};
119+
return { startEnterpriseConnectionLinkFlow, cancelEnterpriseConnectionLinkFlow: stop };
120+
};
121+
80122
destroy = (): Promise<void> => this._baseDelete();
81123

82124
toString = (): string => this.emailAddress;
@@ -89,6 +131,7 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
89131
this.id = data.id;
90132
this.emailAddress = data.email_address;
91133
this.verification = new Verification(data.verification);
134+
this.matchesEnterpriseConnection = data.matches_enterprise_connection;
92135
this.linkedTo = (data.linked_to || []).map(link => new IdentificationLink(link));
93136
return this;
94137
}

packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { localizationKeys } from '../../customizables';
88
import type { FormProps } from '../../elements';
99
import { Form, FormButtons, FormContainer, useCardState, withCardStateProvider } from '../../elements';
1010
import { handleError, useFormControl } from '../../utils';
11-
import { emailLinksEnabledForInstance } from './utils';
11+
import { emailLinksEnabledForInstance, getVerificationStrategy } from './utils';
1212
import { VerifyWithCode } from './VerifyWithCode';
13+
import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection';
1314
import { VerifyWithLink } from './VerifyWithLink';
1415

1516
type EmailFormProps = FormProps & {
@@ -21,10 +22,9 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
2122
const { user } = useUser();
2223
const environment = useEnvironment();
2324
const preferEmailLinks = emailLinksEnabledForInstance(environment);
24-
2525
const [createEmailAddress] = useReverification(() => user?.createEmailAddress({ email: emailField.value }));
26-
2726
const emailAddressRef = React.useRef<EmailAddressResource | undefined>(user?.emailAddresses.find(a => a.id === id));
27+
const strategy = getVerificationStrategy(emailAddressRef.current, preferEmailLinks);
2828
const wizard = useWizard({
2929
defaultStep: emailAddressRef.current ? 1 : 0,
3030
onNextStep: () => card.setError(undefined),
@@ -89,13 +89,14 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
8989
})
9090
}
9191
>
92-
{preferEmailLinks ? (
92+
{strategy === 'email_link' && (
9393
<VerifyWithLink
9494
nextStep={onSuccess}
9595
email={emailAddressRef.current as any}
9696
onReset={onReset}
9797
/>
98-
) : (
98+
)}
99+
{strategy === 'email_code' && (
99100
<VerifyWithCode
100101
nextStep={onSuccess}
101102
identification={emailAddressRef.current}
@@ -104,6 +105,13 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
104105
onReset={onReset}
105106
/>
106107
)}
108+
{strategy === 'saml' && (
109+
<VerifyWithEnterpriseConnection
110+
nextStep={onSuccess}
111+
email={emailAddressRef.current as any}
112+
onReset={onReset}
113+
/>
114+
)}
107115
</FormContainer>
108116
</Wizard>
109117
);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { EmailAddressResource } from '@clerk/types';
2+
import React from 'react';
3+
4+
import { EmailLinkStatusCard } from '../../common';
5+
import { buildEmailLinkRedirectUrl } from '../../common/redirects';
6+
import { useEnvironment, useUserProfileContext } from '../../contexts';
7+
import { Button, descriptors, localizationKeys } from '../../customizables';
8+
import { FormButtonContainer, useCardState, VerificationLink } from '../../elements';
9+
import { useEnterpriseConnectionLink } from '../../hooks';
10+
import { handleError } from '../../utils';
11+
12+
type VerifyWithEnterpriseConnectionProps = {
13+
email: EmailAddressResource;
14+
onReset: () => void;
15+
nextStep: () => void;
16+
};
17+
18+
export const VerifyWithEnterpriseConnection = (props: VerifyWithEnterpriseConnectionProps) => {
19+
const { email, nextStep, onReset } = props;
20+
const card = useCardState();
21+
const profileContext = useUserProfileContext();
22+
const { startEnterpriseConnectionLinkFlow } = useEnterpriseConnectionLink(email);
23+
const { displayConfig } = useEnvironment();
24+
25+
React.useEffect(() => {
26+
startVerification();
27+
}, []);
28+
29+
function startVerification() {
30+
/**
31+
* The following workaround is used in order to make magic links work when the
32+
* <UserProfile/> is used as a modal. In modals, the routing is virtual. For
33+
* magic links the flow needs to end by invoking the /verify path of the <UserProfile/>
34+
* that renders the <VerificationSuccessPage/>. So, we use the userProfileUrl that
35+
* defaults to Clerk Hosted Pages /user as a fallback.
36+
*/
37+
const { routing } = profileContext;
38+
const baseUrl = routing === 'virtual' ? displayConfig.userProfileUrl : '';
39+
const redirectUrl = buildEmailLinkRedirectUrl(profileContext, baseUrl);
40+
startEnterpriseConnectionLinkFlow({ redirectUrl })
41+
.then(() => nextStep())
42+
.catch(err => handleError(err, [], card.setError));
43+
}
44+
45+
return (
46+
<>
47+
<VerificationLink
48+
resendButton={localizationKeys('userProfile.emailAddressPage.emailLink.resendButton')}
49+
onResendCodeClicked={startVerification}
50+
/>
51+
<FormButtonContainer>
52+
<Button
53+
variant='ghost'
54+
localizationKey={localizationKeys('userProfile.formButtonReset')}
55+
elementDescriptor={descriptors.formButtonReset}
56+
onClick={onReset}
57+
/>
58+
</FormButtonContainer>
59+
</>
60+
);
61+
};
62+
63+
export const VerificationSuccessPage = () => {
64+
return (
65+
<EmailLinkStatusCard
66+
title={localizationKeys('signUp.emailLink.verifiedSwitchTab.title')}
67+
subtitle={localizationKeys('signUp.emailLink.verifiedSwitchTab.subtitle')}
68+
status='verified'
69+
/>
70+
);
71+
};

packages/clerk-js/src/ui/components/UserProfile/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
EmailAddressResource,
44
EnvironmentResource,
55
PhoneNumberResource,
6+
PrepareEmailAddressVerificationParams,
67
UserResource,
78
} from '@clerk/types';
89

@@ -76,3 +77,14 @@ export function sortIdentificationBasedOnVerification<T extends Array<EmailAddre
7677

7778
return [...primaryItem, ...verifiedItems, ...unverifiedItems, ...unverifiedItemsWithoutVerification] as T;
7879
}
80+
81+
export const getVerificationStrategy = (
82+
emailAddress: EmailAddressResource | undefined,
83+
preferLinks: boolean,
84+
): PrepareEmailAddressVerificationParams['strategy'] => {
85+
if (emailAddress?.matchesEnterpriseConnection) {
86+
return 'saml';
87+
}
88+
89+
return preferLinks ? 'email_link' : 'email_code';
90+
};

packages/clerk-js/src/ui/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * from './useDebounce';
1818
export * from './useScrollLock';
1919
export * from './useClerkModalStateParams';
2020
export * from './useNavigateToFlowStart';
21+
export * from './useEnterpriseConnectionLink';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type {
2+
CreateEnterpriseConnectionLinkFlowReturn,
3+
EmailAddressResource,
4+
StartEnterpriseConnectionLinkFlowParams,
5+
} from '@clerk/types';
6+
import React from 'react';
7+
8+
type EnterpriseConnectionLinkable = EmailAddressResource;
9+
type EnterpriseConnectionLinkEmailAddressReturn = CreateEnterpriseConnectionLinkFlowReturn<
10+
StartEnterpriseConnectionLinkFlowParams,
11+
EmailAddressResource
12+
>;
13+
14+
function useEnterpriseConnectionLink(
15+
resource: EnterpriseConnectionLinkable,
16+
): EnterpriseConnectionLinkEmailAddressReturn {
17+
const { startEnterpriseConnectionLinkFlow, cancelEnterpriseConnectionLinkFlow } = React.useMemo(
18+
() => resource.createEnterpriseConnectionLinkFlow(),
19+
[resource],
20+
);
21+
22+
React.useEffect(() => {
23+
return cancelEnterpriseConnectionLinkFlow;
24+
}, []);
25+
26+
return {
27+
startEnterpriseConnectionLinkFlow,
28+
cancelEnterpriseConnectionLinkFlow,
29+
};
30+
}
31+
32+
export { useEnterpriseConnectionLink };

packages/types/src/emailAddress.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import type { IdentificationLinkResource } from './identificationLink';
22
import type { ClerkResource } from './resource';
3-
import type { EmailCodeStrategy, EmailLinkStrategy } from './strategies';
4-
import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification';
3+
import type { EmailCodeStrategy, EmailLinkStrategy, EmailSAMLStrategy } from './strategies';
4+
import type {
5+
CreateEmailLinkFlowReturn,
6+
CreateEnterpriseConnectionLinkFlowReturn,
7+
StartEmailLinkFlowParams,
8+
StartEnterpriseConnectionLinkFlowParams,
9+
VerificationResource,
10+
} from './verification';
511

612
export type PrepareEmailAddressVerificationParams =
713
| {
@@ -10,6 +16,10 @@ export type PrepareEmailAddressVerificationParams =
1016
| {
1117
strategy: EmailLinkStrategy;
1218
redirectUrl: string;
19+
}
20+
| {
21+
strategy: EmailSAMLStrategy;
22+
redirectUrl: string;
1323
};
1424

1525
export type AttemptEmailAddressVerificationParams = {
@@ -20,11 +30,16 @@ export interface EmailAddressResource extends ClerkResource {
2030
id: string;
2131
emailAddress: string;
2232
verification: VerificationResource;
33+
matchesEnterpriseConnection: boolean;
2334
linkedTo: IdentificationLinkResource[];
2435
toString: () => string;
2536
prepareVerification: (params: PrepareEmailAddressVerificationParams) => Promise<EmailAddressResource>;
2637
attemptVerification: (params: AttemptEmailAddressVerificationParams) => Promise<EmailAddressResource>;
2738
createEmailLinkFlow: () => CreateEmailLinkFlowReturn<StartEmailLinkFlowParams, EmailAddressResource>;
39+
createEnterpriseConnectionLinkFlow: () => CreateEnterpriseConnectionLinkFlowReturn<
40+
StartEnterpriseConnectionLinkFlowParams,
41+
EmailAddressResource
42+
>;
2843
destroy: () => Promise<void>;
2944
create: () => Promise<EmailAddressResource>;
3045
}

packages/types/src/json.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export interface EmailAddressJSON extends ClerkResourceJSON {
139139
email_address: string;
140140
verification: VerificationJSON | null;
141141
linked_to: IdentificationLinkJSON[];
142+
matches_enterprise_connection: boolean;
142143
}
143144

144145
export interface IdentificationLinkJSON extends ClerkResourceJSON {

packages/types/src/strategies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type PasswordStrategy = 'password';
77
export type PhoneCodeStrategy = 'phone_code';
88
export type EmailCodeStrategy = 'email_code';
99
export type EmailLinkStrategy = 'email_link';
10+
export type EmailSAMLStrategy = 'saml';
1011
export type TicketStrategy = 'ticket';
1112
export type TOTPStrategy = 'totp';
1213
export type BackupCodeStrategy = 'backup_code';

packages/types/src/verification.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,12 @@ export type CreateEmailLinkFlowReturn<Params, Resource> = {
4141
startEmailLinkFlow: (params: Params) => Promise<Resource>;
4242
cancelEmailLinkFlow: () => void;
4343
};
44+
45+
export interface StartEnterpriseConnectionLinkFlowParams {
46+
redirectUrl: string;
47+
}
48+
49+
export type CreateEnterpriseConnectionLinkFlowReturn<Params, Resource> = {
50+
startEnterpriseConnectionLinkFlow: (params: Params) => Promise<Resource>;
51+
cancelEnterpriseConnectionLinkFlow: () => void;
52+
};

0 commit comments

Comments
 (0)