Skip to content

Commit ca6f89c

Browse files
committed
Add HOC to open modal based on org settings
1 parent aef38f7 commit ca6f89c

File tree

14 files changed

+410
-79
lines changed

14 files changed

+410
-79
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
__experimental_CheckoutInstance,
2424
__experimental_CheckoutOptions,
2525
__internal_CheckoutProps,
26+
__internal_EnableOrganizationsModalProps,
2627
__internal_OAuthConsentProps,
2728
__internal_PlanDetailsProps,
2829
__internal_SubscriptionDetailsProps,
@@ -35,9 +36,9 @@ import type {
3536
AuthenticateWithMetamaskParams,
3637
AuthenticateWithOKXWalletParams,
3738
BillingNamespace,
38-
Clerk as ClerkInterface,
3939
ClerkAPIError,
4040
ClerkAuthenticateWithWeb3Params,
41+
Clerk as ClerkInterface,
4142
ClerkOptions,
4243
ClientJSONSnapshot,
4344
ClientResource,
@@ -715,6 +716,18 @@ export class Clerk implements ClerkInterface {
715716
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification'));
716717
};
717718

719+
public __internal_openEnableOrganizations = (props?: __internal_EnableOrganizationsModalProps): void => {
720+
this.assertComponentsReady(this.#componentControls);
721+
void this.#componentControls
722+
.ensureMounted({ preloadHint: 'EnableOrganizations' })
723+
.then(controls => controls.openModal('enableOrganizations', props || {}));
724+
};
725+
726+
public __internal_closeEnableOrganizations = (): void => {
727+
this.assertComponentsReady(this.#componentControls);
728+
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('enableOrganizations'));
729+
};
730+
718731
public __internal_openBlankCaptchaModal = (): Promise<unknown> => {
719732
this.assertComponentsReady(this.#componentControls);
720733
return this.#componentControls

packages/clerk-js/src/ui/Components.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
__internal_CheckoutProps,
3+
__internal_EnableOrganizationsProps,
34
__internal_PlanDetailsProps,
45
__internal_SubscriptionDetailsProps,
56
__internal_UserVerificationProps,
@@ -27,6 +28,7 @@ import type { ClerkComponentName } from './lazyModules/components';
2728
import {
2829
BlankCaptchaModal,
2930
CreateOrganizationModal,
31+
EnableOrganizationsModal,
3032
ImpersonationFab,
3133
KeylessPrompt,
3234
OrganizationProfileModal,
@@ -79,7 +81,8 @@ export type ComponentControls = {
7981
| 'createOrganization'
8082
| 'userVerification'
8183
| 'waitlist'
82-
| 'blankCaptcha',
84+
| 'blankCaptcha'
85+
| 'enableOrganizations',
8386
>(
8487
modal: T,
8588
props: T extends 'signIn'
@@ -90,7 +93,9 @@ export type ComponentControls = {
9093
? __internal_UserVerificationProps
9194
: T extends 'waitlist'
9295
? WaitlistProps
93-
: UserProfileProps,
96+
: T extends 'enableOrganizations'
97+
? __internal_EnableOrganizationsProps
98+
: UserProfileProps,
9499
) => void;
95100
closeModal: (
96101
modal:
@@ -102,7 +107,8 @@ export type ComponentControls = {
102107
| 'createOrganization'
103108
| 'userVerification'
104109
| 'waitlist'
105-
| 'blankCaptcha',
110+
| 'blankCaptcha'
111+
| 'enableOrganizations',
106112
options?: {
107113
notify?: boolean;
108114
},
@@ -152,6 +158,7 @@ interface ComponentsState {
152158
userVerificationModal: null | __internal_UserVerificationProps;
153159
organizationProfileModal: null | OrganizationProfileProps;
154160
createOrganizationModal: null | CreateOrganizationProps;
161+
enableOrganizationsModal: null | __internal_EnableOrganizationsProps;
155162
blankCaptchaModal: null;
156163
organizationSwitcherPrefetch: boolean;
157164
waitlistModal: null | WaitlistProps;
@@ -245,6 +252,7 @@ const Components = (props: ComponentsProps) => {
245252
userVerificationModal: null,
246253
organizationProfileModal: null,
247254
createOrganizationModal: null,
255+
enableOrganizationsModal: null,
248256
organizationSwitcherPrefetch: false,
249257
waitlistModal: null,
250258
blankCaptchaModal: null,
@@ -274,6 +282,7 @@ const Components = (props: ComponentsProps) => {
274282
createOrganizationModal,
275283
waitlistModal,
276284
blankCaptchaModal,
285+
enableOrganizationsModal,
277286
checkoutDrawer,
278287
planDetailsDrawer,
279288
subscriptionDetailsDrawer,
@@ -325,9 +334,10 @@ const Components = (props: ComponentsProps) => {
325334
clearUrlStateParam();
326335
setState(s => {
327336
function handleCloseModalForExperimentalUserVerification() {
328-
const modal = s[`${name}Modal`] || {};
337+
const modal = s[`${name}Modal`];
329338
if (modal && typeof modal === 'object' && 'afterVerificationCancelled' in modal && notify) {
330-
modal.afterVerificationCancelled?.();
339+
// TypeScript doesn't narrow properly with template literal access and 'in' operator
340+
(modal as { afterVerificationCancelled?: () => void }).afterVerificationCancelled?.();
331341
}
332342
}
333343

@@ -484,6 +494,22 @@ const Components = (props: ComponentsProps) => {
484494
</LazyModalRenderer>
485495
);
486496

497+
const mountedEnableOrganizationsModal = (
498+
<LazyModalRenderer
499+
globalAppearance={state.appearance}
500+
appearanceKey={'enableOrganizations'}
501+
componentAppearance={enableOrganizationsModal?.appearance}
502+
flowName={'enableOrganizations'}
503+
onClose={() => componentsControls.closeModal('enableOrganizations')}
504+
onExternalNavigate={() => componentsControls.closeModal('enableOrganizations')}
505+
startPath={buildVirtualRouterUrl({ base: '/enable-organizations', path: urlStateParam?.path })}
506+
componentName={'EnableOrganizationsModal'}
507+
modalContainerSx={{ alignItems: 'center' }}
508+
>
509+
<EnableOrganizationsModal {...enableOrganizationsModal} />
510+
</LazyModalRenderer>
511+
);
512+
487513
const mountedOrganizationProfileModal = (
488514
<LazyModalRenderer
489515
globalAppearance={state.appearance}
@@ -587,6 +613,7 @@ const Components = (props: ComponentsProps) => {
587613
{createOrganizationModal && mountedCreateOrganizationModal}
588614
{waitlistModal && mountedWaitlistModal}
589615
{blankCaptchaModal && mountedBlankCaptchaModal}
616+
{enableOrganizationsModal && mountedEnableOrganizationsModal}
590617

591618
<MountedCheckoutDrawer
592619
appearance={state.appearance}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type {
2+
__internal_EnableOrganizationsModalProps,
3+
__internal_EnableOrganizationsProps,
4+
} from '@clerk/shared/types';
5+
import React from 'react';
6+
7+
import { EnableOrganizationsContext } from '@/ui/contexts/components/EnableOrganizations';
8+
import { Card } from '@/ui/elements/Card';
9+
import { withCardStateProvider } from '@/ui/elements/contexts';
10+
import { Header } from '@/ui/elements/Header';
11+
12+
import { withCoreSessionSwitchGuard } from '../../contexts';
13+
import { Flow } from '../../customizables';
14+
import { Route, Switch } from '../../router';
15+
16+
const EnableOrganizationsContent = withCardStateProvider(() => (
17+
<Card.Root>
18+
<Card.Content>
19+
<Header.Root>
20+
<Header.Title>Enable organizations</Header.Title>
21+
</Header.Root>
22+
</Card.Content>
23+
<Card.Footer />
24+
</Card.Root>
25+
));
26+
27+
function EnableOrganizationsRoutes(): JSX.Element {
28+
return (
29+
<Flow.Root flow='enableOrganizations'>
30+
<Switch>
31+
<Route index>
32+
<EnableOrganizationsContent />
33+
</Route>
34+
</Switch>
35+
</Flow.Root>
36+
);
37+
}
38+
39+
EnableOrganizationsRoutes.displayName = 'EnableOrganizations';
40+
41+
const EnableOrganizations: React.ComponentType<__internal_EnableOrganizationsProps> =
42+
withCoreSessionSwitchGuard(EnableOrganizationsRoutes);
43+
44+
// TODO -> Maybe move this to a inner folder for all in-app modals
45+
const EnableOrganizationsModal = (props: __internal_EnableOrganizationsModalProps): JSX.Element => {
46+
return (
47+
<Route path='enable-organizations'>
48+
<EnableOrganizationsContext.Provider
49+
value={{
50+
componentName: 'EnableOrganizations',
51+
routing: 'virtual',
52+
...props,
53+
}}
54+
>
55+
<div>
56+
<EnableOrganizations routing='virtual' />
57+
</div>
58+
</EnableOrganizationsContext.Provider>
59+
</Route>
60+
);
61+
};
62+
63+
export { EnableOrganizations, EnableOrganizationsModal };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createContext, useContext } from 'react';
2+
3+
import type { EnableOrganizationsCtx } from '../../types';
4+
5+
export type EnableOrganizationsContextType = EnableOrganizationsCtx;
6+
7+
export const EnableOrganizationsContext = createContext<EnableOrganizationsCtx | null>(null);
8+
9+
export const useEnableOrganizations = (): EnableOrganizationsContextType => {
10+
const context = useContext(EnableOrganizationsContext);
11+
12+
if (!context || context.componentName !== 'EnableOrganizations') {
13+
throw new Error(
14+
'Clerk: useEnableOrganizationsContext called outside of the mounted EnableOrganizations component.',
15+
);
16+
}
17+
18+
const { componentName, ...ctx } = context;
19+
20+
return {
21+
...ctx,
22+
componentName,
23+
};
24+
};

packages/clerk-js/src/ui/elements/contexts/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export type FlowMetadata = {
101101
| 'oauthConsent'
102102
| 'subscriptionDetails'
103103
| 'tasks'
104-
| 'taskChooseOrganization';
104+
| 'taskChooseOrganization'
105+
| 'enableOrganizations';
105106
part?:
106107
| 'start'
107108
| 'emailCode'

packages/clerk-js/src/ui/lazyModules/components.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const componentImportPaths = {
2727
SubscriptionDetails: () => import(/* webpackChunkName: "subscriptionDetails" */ '../components/SubscriptionDetails'),
2828
APIKeys: () => import(/* webpackChunkName: "apiKeys" */ '../components/ApiKeys/ApiKeys'),
2929
OAuthConsent: () => import(/* webpackChunkName: "oauthConsent" */ '../components/OAuthConsent/OAuthConsent'),
30+
EnableOrganizations: () => import(/* webpackChunkName: "enableOrganizations" */ '../components/EnableOrganizations'),
3031
} as const;
3132

3233
export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn })));
@@ -44,6 +45,14 @@ export const UserVerificationModal = lazy(() =>
4445
componentImportPaths.UserVerification().then(module => ({ default: module.UserVerificationModal })),
4546
);
4647

48+
export const EnableOrganizations = lazy(() =>
49+
componentImportPaths.EnableOrganizations().then(module => ({ default: module.EnableOrganizations })),
50+
);
51+
52+
export const EnableOrganizationsModal = lazy(() =>
53+
componentImportPaths.EnableOrganizations().then(module => ({ default: module.EnableOrganizationsModal })),
54+
);
55+
4756
export const SignUp = lazy(() => componentImportPaths.SignUp().then(module => ({ default: module.SignUp })));
4857

4958
export const SignUpModal = lazy(() => componentImportPaths.SignUp().then(module => ({ default: module.SignUpModal })));
@@ -144,6 +153,7 @@ export const ClerkComponents = {
144153
UserButton,
145154
UserProfile,
146155
UserVerification,
156+
EnableOrganizations,
147157
OrganizationSwitcher,
148158
OrganizationList,
149159
OrganizationProfile,
@@ -154,6 +164,7 @@ export const ClerkComponents = {
154164
OrganizationProfileModal,
155165
CreateOrganizationModal,
156166
UserVerificationModal,
167+
EnableOrganizationsModal,
157168
GoogleOneTap,
158169
Waitlist,
159170
WaitlistModal,

packages/clerk-js/src/ui/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
__internal_CheckoutProps,
3+
__internal_EnableOrganizationsProps,
34
__internal_OAuthConsentProps,
45
__internal_PlanDetailsProps,
56
__internal_SubscriptionDetailsProps,
@@ -75,6 +76,11 @@ export type UserVerificationCtx = __internal_UserVerificationProps & {
7576
mode?: ComponentMode;
7677
};
7778

79+
export type EnableOrganizationsCtx = __internal_EnableOrganizationsProps & {
80+
componentName: 'EnableOrganizations';
81+
mode?: ComponentMode;
82+
};
83+
7884
export type UserProfileCtx = UserProfileProps & {
7985
componentName: 'UserProfile';
8086
mode?: ComponentMode;

packages/react/src/isomorphicClerk.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus';
33
import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript';
44
import type {
55
__internal_CheckoutProps,
6+
__internal_EnableOrganizationsModalProps,
67
__internal_OAuthConsentProps,
78
__internal_PlanDetailsProps,
89
__internal_SubscriptionDetailsProps,
@@ -131,6 +132,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
131132
private preopenOrganizationProfile?: null | OrganizationProfileProps = null;
132133
private preopenCreateOrganization?: null | CreateOrganizationProps = null;
133134
private preOpenWaitlist?: null | WaitlistProps = null;
135+
private preopenEnableOrganizations?: null | __internal_EnableOrganizationsModalProps = null;
134136
private premountSignInNodes = new Map<HTMLDivElement, SignInProps | undefined>();
135137
private premountSignUpNodes = new Map<HTMLDivElement, SignUpProps | undefined>();
136138
private premountUserAvatarNodes = new Map<HTMLDivElement, UserAvatarProps | undefined>();
@@ -611,6 +613,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
611613
clerkjs.openWaitlist(this.preOpenWaitlist);
612614
}
613615

616+
if (this.preopenEnableOrganizations !== null) {
617+
clerkjs.__internal_openEnableOrganizations(this.preopenEnableOrganizations);
618+
}
619+
614620
this.premountSignInNodes.forEach((props, node) => {
615621
clerkjs.mountSignIn(node, props);
616622
});
@@ -852,6 +858,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
852858
}
853859
};
854860

861+
__internal_openEnableOrganizations = (props?: __internal_EnableOrganizationsModalProps) => {
862+
if (this.clerkjs && this.loaded) {
863+
this.clerkjs.__internal_openEnableOrganizations(props);
864+
} else {
865+
this.preopenEnableOrganizations = props;
866+
}
867+
};
868+
869+
__internal_closeEnableOrganizations = () => {
870+
if (this.clerkjs && this.loaded) {
871+
this.clerkjs.__internal_closeEnableOrganizations();
872+
} else {
873+
this.preopenEnableOrganizations = null;
874+
}
875+
};
876+
855877
openGoogleOneTap = (props?: GoogleOneTapProps) => {
856878
if (this.clerkjs && this.loaded) {
857879
this.clerkjs.openGoogleOneTap(props);

0 commit comments

Comments
 (0)