Skip to content
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

orgs-236: wip #4406

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions packages/clerk-js/src/core/resources/EmailAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Poller } from '@clerk/shared/poller';
import type {
AttemptEmailAddressVerificationParams,
CreateEmailLinkFlowReturn,
CreateEnterpriseConnectionLinkFlowReturn,
EmailAddressJSON,
EmailAddressResource,
IdentificationLinkResource,
PrepareEmailAddressVerificationParams,
StartEmailLinkFlowParams,
StartEnterpriseConnectionLinkFlowParams,
VerificationResource,
} from '@clerk/types';

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

Expand Down Expand Up @@ -77,6 +80,45 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
return { startEmailLinkFlow, cancelEmailLinkFlow: stop };
};

createEnterpriseConnectionLinkFlow = (): CreateEnterpriseConnectionLinkFlowReturn<
StartEnterpriseConnectionLinkFlowParams,
EmailAddressResource
> => {
const { run, stop } = Poller();

const startEnterpriseConnectionLinkFlow = async ({
redirectUrl,
}: StartEnterpriseConnectionLinkFlowParams): Promise<EmailAddressResource> => {
if (!this.id) {
clerkVerifyEmailAddressCalledBeforeCreate('SignUp');
}
const response = await this.prepareVerification({
strategy: 'saml',
redirectUrl: redirectUrl,
});
if (!response.verification.externalVerificationRedirectURL) {
throw Error('Unexpected: External verification redirect URL is missing');
}
window.open(response.verification.externalVerificationRedirectURL, '_blank');
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 { startEnterpriseConnectionLinkFlow, cancelEnterpriseConnectionLinkFlow: stop };
};

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

toString = (): string => this.emailAddress;
Expand All @@ -89,6 +131,7 @@ export class EmailAddress extends BaseResource implements EmailAddressResource {
this.id = data.id;
this.emailAddress = data.email_address;
this.verification = new Verification(data.verification);
this.matchesEnterpriseConnection = data.matches_enterprise_connection;
this.linkedTo = (data.linked_to || []).map(link => new IdentificationLink(link));
return this;
}
Expand Down
17 changes: 13 additions & 4 deletions packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import type { FormProps } from '../../elements';
import { Form, FormButtons, FormContainer, useCardState, withCardStateProvider } from '../../elements';
import { useAssurance } from '../../hooks/useAssurance';
import { handleError, useFormControl } from '../../utils';
import { emailLinksEnabledForInstance } from './utils';
import { emailLinksEnabledForInstance, getVerificationStrategy } from './utils';
import { VerifyWithCode } from './VerifyWithCode';
import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection';
import { VerifyWithLink } from './VerifyWithLink';

type EmailFormProps = FormProps & {
Expand All @@ -23,8 +24,8 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
const { handleAssurance } = useAssurance();
const environment = useEnvironment();
const preferEmailLinks = emailLinksEnabledForInstance(environment);

const emailAddressRef = React.useRef<EmailAddressResource | undefined>(user?.emailAddresses.find(a => a.id === id));
const strategy = getVerificationStrategy(emailAddressRef.current, preferEmailLinks);
const wizard = useWizard({
defaultStep: emailAddressRef.current ? 1 : 0,
onNextStep: () => card.setError(undefined),
Expand Down Expand Up @@ -89,13 +90,14 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
})
}
>
{preferEmailLinks ? (
{strategy === 'email_link' && (
<VerifyWithLink
nextStep={onSuccess}
email={emailAddressRef.current as any}
onReset={onReset}
/>
) : (
)}
{strategy === 'email_code' && (
<VerifyWithCode
nextStep={onSuccess}
identification={emailAddressRef.current}
Expand All @@ -104,6 +106,13 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
onReset={onReset}
/>
)}
{strategy === 'saml' && (
<VerifyWithEnterpriseConnection
nextStep={onSuccess}
email={emailAddressRef.current as any}
onReset={onReset}
/>
)}
</FormContainer>
</Wizard>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { EmailAddressResource } from '@clerk/types';
import React from 'react';

import { EmailLinkStatusCard } from '../../common';
import { buildEmailLinkRedirectUrl } from '../../common/redirects';
import { useEnvironment, useUserProfileContext } from '../../contexts';
import { Button, descriptors, localizationKeys } from '../../customizables';
import { FormButtonContainer, useCardState, VerificationLink } from '../../elements';
import { useEnterpriseConnectionLink } from '../../hooks';
import { handleError } from '../../utils';

type VerifyWithEnterpriseConnectionProps = {
email: EmailAddressResource;
onReset: () => void;
nextStep: () => void;
};

export const VerifyWithEnterpriseConnection = (props: VerifyWithEnterpriseConnectionProps) => {
const { email, nextStep, onReset } = props;
const card = useCardState();
const profileContext = useUserProfileContext();
const { startEnterpriseConnectionLinkFlow } = useEnterpriseConnectionLink(email);
const { displayConfig } = useEnvironment();

React.useEffect(() => {
startVerification();
}, []);

function startVerification() {
/**
* The following workaround is used in order to make magic links work when the
* <UserProfile/> is used as a modal. In modals, the routing is virtual. For
* magic links the flow needs to end by invoking the /verify path of the <UserProfile/>
* that renders the <VerificationSuccessPage/>. So, we use the userProfileUrl that
* defaults to Clerk Hosted Pages /user as a fallback.
*/
const { routing } = profileContext;
const baseUrl = routing === 'virtual' ? displayConfig.userProfileUrl : '';
const redirectUrl = buildEmailLinkRedirectUrl(profileContext, baseUrl);
startEnterpriseConnectionLinkFlow({ redirectUrl })
.then(() => nextStep())
.catch(err => handleError(err, [], card.setError));
}

return (
<>
<VerificationLink
resendButton={localizationKeys('userProfile.emailAddressPage.emailLink.resendButton')}
onResendCodeClicked={startVerification}
/>
<FormButtonContainer>
<Button
variant='ghost'
localizationKey={localizationKeys('userProfile.formButtonReset')}
elementDescriptor={descriptors.formButtonReset}
onClick={onReset}
/>
</FormButtonContainer>
</>
);
};

export const VerificationSuccessPage = () => {
return (
<EmailLinkStatusCard
title={localizationKeys('signUp.emailLink.verifiedSwitchTab.title')}
subtitle={localizationKeys('signUp.emailLink.verifiedSwitchTab.subtitle')}
status='verified'
/>
);
};
12 changes: 12 additions & 0 deletions packages/clerk-js/src/ui/components/UserProfile/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
EmailAddressResource,
EnvironmentResource,
PhoneNumberResource,
PrepareEmailAddressVerificationParams,
UserResource,
} from '@clerk/types';

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

return [...primaryItem, ...verifiedItems, ...unverifiedItems, ...unverifiedItemsWithoutVerification] as T;
}

export const getVerificationStrategy = (
emailAddress: EmailAddressResource | undefined,
preferLinks: boolean,
): PrepareEmailAddressVerificationParams['strategy'] => {
if (emailAddress?.matchesEnterpriseConnection) {
return 'saml';
}

return preferLinks ? 'email_link' : 'email_code';
};
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './useDebounce';
export * from './useScrollLock';
export * from './useClerkModalStateParams';
export * from './useNavigateToFlowStart';
export * from './useEnterpriseConnectionLink';
32 changes: 32 additions & 0 deletions packages/clerk-js/src/ui/hooks/useEnterpriseConnectionLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {
CreateEnterpriseConnectionLinkFlowReturn,
EmailAddressResource,
StartEnterpriseConnectionLinkFlowParams,
} from '@clerk/types';
import React from 'react';

type EnterpriseConnectionLinkable = EmailAddressResource;
type EnterpriseConnectionLinkEmailAddressReturn = CreateEnterpriseConnectionLinkFlowReturn<
StartEnterpriseConnectionLinkFlowParams,
EmailAddressResource
>;

function useEnterpriseConnectionLink(
resource: EnterpriseConnectionLinkable,
): EnterpriseConnectionLinkEmailAddressReturn {
const { startEnterpriseConnectionLinkFlow, cancelEnterpriseConnectionLinkFlow } = React.useMemo(
() => resource.createEnterpriseConnectionLinkFlow(),
[resource],
);

React.useEffect(() => {
return cancelEnterpriseConnectionLinkFlow;
}, []);

return {
startEnterpriseConnectionLinkFlow,
cancelEnterpriseConnectionLinkFlow,
};
}

export { useEnterpriseConnectionLink };
19 changes: 17 additions & 2 deletions packages/types/src/emailAddress.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { IdentificationLinkResource } from './identificationLink';
import type { ClerkResource } from './resource';
import type { EmailCodeStrategy, EmailLinkStrategy } from './strategies';
import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification';
import type { EmailCodeStrategy, EmailLinkStrategy, EmailSAMLStrategy } from './strategies';
import type {
CreateEmailLinkFlowReturn,
CreateEnterpriseConnectionLinkFlowReturn,
StartEmailLinkFlowParams,
StartEnterpriseConnectionLinkFlowParams,
VerificationResource,
} from './verification';

export type PrepareEmailAddressVerificationParams =
| {
Expand All @@ -10,6 +16,10 @@ export type PrepareEmailAddressVerificationParams =
| {
strategy: EmailLinkStrategy;
redirectUrl: string;
}
| {
strategy: EmailSAMLStrategy;
redirectUrl: string;
};

export type AttemptEmailAddressVerificationParams = {
Expand All @@ -20,11 +30,16 @@ export interface EmailAddressResource extends ClerkResource {
id: string;
emailAddress: string;
verification: VerificationResource;
matchesEnterpriseConnection: boolean;
linkedTo: IdentificationLinkResource[];
toString: () => string;
prepareVerification: (params: PrepareEmailAddressVerificationParams) => Promise<EmailAddressResource>;
attemptVerification: (params: AttemptEmailAddressVerificationParams) => Promise<EmailAddressResource>;
createEmailLinkFlow: () => CreateEmailLinkFlowReturn<StartEmailLinkFlowParams, EmailAddressResource>;
createEnterpriseConnectionLinkFlow: () => CreateEnterpriseConnectionLinkFlowReturn<
StartEnterpriseConnectionLinkFlowParams,
EmailAddressResource
>;
destroy: () => Promise<void>;
create: () => Promise<EmailAddressResource>;
}
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface EmailAddressJSON extends ClerkResourceJSON {
email_address: string;
verification: VerificationJSON | null;
linked_to: IdentificationLinkJSON[];
matches_enterprise_connection: boolean;
}

export interface IdentificationLinkJSON extends ClerkResourceJSON {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type PasswordStrategy = 'password';
export type PhoneCodeStrategy = 'phone_code';
export type EmailCodeStrategy = 'email_code';
export type EmailLinkStrategy = 'email_link';
export type EmailSAMLStrategy = 'saml';
export type TicketStrategy = 'ticket';
export type TOTPStrategy = 'totp';
export type BackupCodeStrategy = 'backup_code';
Expand Down
9 changes: 9 additions & 0 deletions packages/types/src/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ export type CreateEmailLinkFlowReturn<Params, Resource> = {
startEmailLinkFlow: (params: Params) => Promise<Resource>;
cancelEmailLinkFlow: () => void;
};

export interface StartEnterpriseConnectionLinkFlowParams {
redirectUrl: string;
}

export type CreateEnterpriseConnectionLinkFlowReturn<Params, Resource> = {
startEnterpriseConnectionLinkFlow: (params: Params) => Promise<Resource>;
cancelEnterpriseConnectionLinkFlow: () => void;
};
Loading