Skip to content

Commit

Permalink
feat(clerk-js): Replace ComponentContext with component-specific cont…
Browse files Browse the repository at this point in the history
…exts
  • Loading branch information
dstaley committed Nov 5, 2024
1 parent c0c85a1 commit 44e98f2
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CreateOrganizationModalProps } from '@clerk/types';

import { ComponentContext, withCoreUserGuard } from '../../contexts';
import { CreateOrganizationContext, withCoreUserGuard } from '../../contexts';
import { Flow } from '../../customizables';
import { withCardStateProvider } from '../../elements';
import { Route, Switch } from '../../router';
Expand Down Expand Up @@ -37,11 +37,11 @@ export const CreateOrganizationModal = (props: CreateOrganizationModalProps): JS

return (
<Route path='createOrganization'>
<ComponentContext.Provider value={createOrganizationProps}>
<CreateOrganizationContext.Provider value={createOrganizationProps}>
<div>
<CreateOrganization />
</div>
</ComponentContext.Provider>
</CreateOrganizationContext.Provider>
</Route>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useOrganization } from '@clerk/shared/react';
import type { OrganizationProfileModalProps, OrganizationProfileProps } from '@clerk/types';
import React from 'react';

import { ComponentContext, withCoreUserGuard } from '../../contexts';
import { OrganizationProfileContext, withCoreUserGuard } from '../../contexts';
import { Flow, localizationKeys } from '../../customizables';
import { NavbarMenuButtonRow, ProfileCard, withCardStateProvider } from '../../elements';
import { Route, Switch } from '../../router';
Expand Down Expand Up @@ -58,12 +58,12 @@ export const OrganizationProfileModal = (props: OrganizationProfileModalProps):

return (
<Route path='organizationProfile'>
<ComponentContext.Provider value={organizationProfileProps}>
<OrganizationProfileContext.Provider value={organizationProfileProps}>
{/*TODO: Used by InvisibleRootBox, can we simplify? */}
<div>
<OrganizationProfile {...organizationProfileProps} />
</div>
</ComponentContext.Provider>
</OrganizationProfileContext.Provider>
</Route>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { __experimental_UserVerificationModalProps, __experimental_UserVerificationProps } from '@clerk/types';
import React, { useEffect } from 'react';

import { ComponentContext, withCoreSessionSwitchGuard } from '../../contexts';
import { UserVerificationContext, withCoreSessionSwitchGuard } from '../../contexts';
import { Flow } from '../../customizables';
import { Route, Switch } from '../../router';
import { UserVerificationFactorOne } from './UserVerificationFactorOne';
Expand Down Expand Up @@ -37,7 +37,7 @@ const UserVerification: React.ComponentType<__experimental_UserVerificationProps
const UserVerificationModal = (props: __experimental_UserVerificationModalProps): JSX.Element => {
return (
<Route path='user-verification'>
<ComponentContext.Provider
<UserVerificationContext.Provider
value={{
componentName: 'UserVerification',
...props,
Expand All @@ -51,7 +51,7 @@ const UserVerificationModal = (props: __experimental_UserVerificationModalProps)
routing='virtual'
/>
</div>
</ComponentContext.Provider>
</UserVerificationContext.Provider>
</Route>
);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useClerk } from '@clerk/shared/react';
import type { WaitlistModalProps } from '@clerk/types';

import { ComponentContext, useWaitlistContext } from '../../contexts';
import { useWaitlistContext, WaitlistContext } from '../../contexts';
import { Flow, localizationKeys } from '../../customizables';
import { Card, withCardStateProvider } from '../../elements';
import { Route, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
Expand Down Expand Up @@ -52,11 +52,11 @@ export const WaitlistModal = (props: WaitlistModalProps): JSX.Element => {

return (
<Route path='waitlist'>
<ComponentContext.Provider value={{ ...waitlistProps, componentName: 'Waitlist', mode: 'modal' }}>
<WaitlistContext.Provider value={{ ...waitlistProps, componentName: 'Waitlist', mode: 'modal' }}>
<div>
<Waitlist />
</div>
</ComponentContext.Provider>
</WaitlistContext.Provider>
</Route>
);
};
147 changes: 117 additions & 30 deletions packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { deprecatedObjectProperty } from '@clerk/shared/deprecated';
import { useClerk } from '@clerk/shared/react';
import { snakeToCamel } from '@clerk/shared/underscore';
import type { HandleOAuthCallbackParams, OrganizationResource, UserResource } from '@clerk/types';
import type {
HandleOAuthCallbackParams,
OrganizationResource,
UserButtonProps,
UserResource,
WaitlistProps,
} from '@clerk/types';
import React, { useCallback, useMemo } from 'react';

import { SIGN_IN_INITIAL_VALUE_KEYS, SIGN_UP_INITIAL_VALUE_KEYS } from '../../core/constants';
Expand All @@ -13,7 +19,8 @@ import type { NavbarRoute } from '../elements';
import type { ParsedQueryString } from '../router';
import { useRouter } from '../router';
import type {
AvailableComponentCtx,
AvailableComponentName,
AvailableComponentProps,
CreateOrganizationCtx,
GoogleOneTapCtx,
OrganizationListCtx,
Expand All @@ -35,26 +42,70 @@ import {

const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ });

export const ComponentContext = React.createContext<AvailableComponentCtx | null>(null);

export function componentContextWrapper({ componentName }: { componentName: 'SignIn' }): typeof SignInContext;
export function componentContextWrapper({ componentName }: { componentName: 'SignUp' }): typeof SignUpContext;
export function componentContextWrapper({ componentName }: { componentName: 'UserProfile' }): typeof UserProfileContext;
export function componentContextWrapper({ componentName }: { componentName: string }): typeof ComponentContext;
export function componentContextWrapper({
export function ComponentContextProvider({
componentName,
props,
children,
}: {
componentName: string;
}): typeof SignInContext | typeof SignUpContext | typeof UserProfileContext | typeof ComponentContext {
componentName: AvailableComponentName;
props: AvailableComponentProps;
children: React.ReactNode;
}) {
switch (componentName) {
case 'SignIn':
return SignInContext;
return <SignInContext.Provider value={{ componentName, ...props }}>{children}</SignInContext.Provider>;
case 'SignUp':
return SignUpContext;
return <SignUpContext.Provider value={{ componentName, ...props }}>{children}</SignUpContext.Provider>;
case 'UserProfile':
return UserProfileContext;
return <UserProfileContext.Provider value={{ componentName, ...props }}>{children}</UserProfileContext.Provider>;
case 'UserVerification':
return (
<UserVerificationContext.Provider value={{ componentName, ...props }}>
{children}
</UserVerificationContext.Provider>
);
case 'UserButton':
return (
<UserButtonContext.Provider value={{ componentName, ...(props as UserButtonProps) }}>
{children}
</UserButtonContext.Provider>
);
case 'OrganizationSwitcher':
return (
<OrganizationSwitcherContext.Provider value={{ componentName, ...props }}>
{children}
</OrganizationSwitcherContext.Provider>
);
case 'OrganizationList':
return (
<OrganizationListContext.Provider value={{ componentName, ...props }}>
{children}
</OrganizationListContext.Provider>
);
case 'OrganizationProfile':
return (
<OrganizationProfileContext.Provider value={{ componentName, ...props }}>
{children}
</OrganizationProfileContext.Provider>
);
case 'CreateOrganization':
return (
<CreateOrganizationContext.Provider value={{ componentName, ...props }}>
{children}
</CreateOrganizationContext.Provider>
);
case 'GoogleOneTap':
return (
<GoogleOneTapContext.Provider value={{ componentName, ...props }}>{children}</GoogleOneTapContext.Provider>
);
case 'Waitlist':
return (
<WaitlistContext.Provider value={{ componentName, ...(props as WaitlistProps) }}>
{children}
</WaitlistContext.Provider>
);
default:
return ComponentContext;
throw new Error(`Unknown component context: ${componentName}`);
}
}

Expand Down Expand Up @@ -291,30 +342,38 @@ export const useUserProfileContext = (): UserProfileContextType => {

export type UserVerificationContextType = UserVerificationCtx;

export const UserVerificationContext = React.createContext<UserVerificationCtx | null>(null);

export const useUserVerification = (): UserVerificationContextType => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as UserVerificationCtx;
const context = React.useContext(UserVerificationContext);

if (componentName !== 'UserVerification') {
if (!context || context.componentName !== 'UserVerification') {
throw new Error('Clerk: useUserVerificationContext called outside of the mounted UserVerification component.');
}

const { componentName, ...ctx } = context;

return {
...ctx,
componentName,
};
};

export const UserButtonContext = React.createContext<UserButtonCtx | null>(null);

export const useUserButtonContext = () => {
const { componentName, customMenuItems, ...ctx } = (React.useContext(ComponentContext) || {}) as UserButtonCtx;
const context = React.useContext(UserButtonContext);
const clerk = useClerk();
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();
const options = useOptions();

if (componentName !== 'UserButton') {
if (!context || context.componentName !== 'UserButton') {
throw new Error('Clerk: useUserButtonContext called outside of the mounted UserButton component.');
}

const { componentName, customMenuItems, ...ctx } = context;

const signInUrl = ctx.signInUrl || options.signInUrl || displayConfig.signInUrl;
const userProfileUrl = ctx.userProfileUrl || displayConfig.userProfileUrl;

Expand Down Expand Up @@ -361,15 +420,19 @@ export const useUserButtonContext = () => {
};
};

export const OrganizationSwitcherContext = React.createContext<OrganizationSwitcherCtx | null>(null);

export const useOrganizationSwitcherContext = () => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as OrganizationSwitcherCtx;
const context = React.useContext(OrganizationSwitcherContext);
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();

if (componentName !== 'OrganizationSwitcher') {
if (!context || context.componentName !== 'OrganizationSwitcher') {
throw new Error('Clerk: useOrganizationSwitcherContext called outside OrganizationSwitcher.');
}

const { componentName, ...ctx } = context;

const afterCreateOrganizationUrl = ctx.afterCreateOrganizationUrl || displayConfig.afterCreateOrganizationUrl;
const afterLeaveOrganizationUrl = ctx.afterLeaveOrganizationUrl || displayConfig.afterLeaveOrganizationUrl;

Expand Down Expand Up @@ -464,15 +527,19 @@ export const useOrganizationSwitcherContext = () => {
};
};

export const OrganizationListContext = React.createContext<OrganizationListCtx | null>(null);

export const useOrganizationListContext = () => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as unknown as OrganizationListCtx;
const context = React.useContext(OrganizationListContext);
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();

if (componentName !== 'OrganizationList') {
if (!context || context.componentName !== 'OrganizationList') {
throw new Error('Clerk: useOrganizationListContext called outside OrganizationList.');
}

const { componentName, ...ctx } = context;

const afterCreateOrganizationUrl = ctx.afterCreateOrganizationUrl || displayConfig.afterCreateOrganizationUrl;

const navigateAfterCreateOrganization = (organization: OrganizationResource) => {
Expand Down Expand Up @@ -550,16 +617,20 @@ export type OrganizationProfileContextType = OrganizationProfileCtx & {
isGeneralPageRoot: boolean;
};

export const OrganizationProfileContext = React.createContext<OrganizationProfileCtx | null>(null);

export const useOrganizationProfileContext = (): OrganizationProfileContextType => {
const { componentName, customPages, ...ctx } = (React.useContext(ComponentContext) || {}) as OrganizationProfileCtx;
const context = React.useContext(OrganizationProfileContext);
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();
const clerk = useClerk();

if (componentName !== 'OrganizationProfile') {
if (!context || context.componentName !== 'OrganizationProfile') {
throw new Error('Clerk: useOrganizationProfileContext called outside OrganizationProfile.');
}

const { componentName, customPages, ...ctx } = context;

const pages = useMemo(() => createOrganizationProfileCustomPages(customPages || [], clerk), [customPages]);

const navigateAfterLeaveOrganization = () =>
Expand All @@ -581,15 +652,19 @@ export const useOrganizationProfileContext = (): OrganizationProfileContextType
};
};

export const CreateOrganizationContext = React.createContext<CreateOrganizationCtx | null>(null);

export const useCreateOrganizationContext = () => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as CreateOrganizationCtx;
const context = React.useContext(CreateOrganizationContext);
const { navigate } = useRouter();
const { displayConfig } = useEnvironment();

if (componentName !== 'CreateOrganization') {
if (!context || context.componentName !== 'CreateOrganization') {
throw new Error('Clerk: useCreateOrganizationContext called outside CreateOrganization.');
}

const { componentName, ...ctx } = context;

const navigateAfterCreateOrganization = (organization: OrganizationResource) => {
if (typeof ctx.afterCreateOrganizationUrl === 'function') {
return navigate(ctx.afterCreateOrganizationUrl(organization));
Expand All @@ -615,16 +690,20 @@ export const useCreateOrganizationContext = () => {
};
};

export const GoogleOneTapContext = React.createContext<GoogleOneTapCtx | null>(null);

export const useGoogleOneTapContext = () => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as GoogleOneTapCtx;
const context = React.useContext(GoogleOneTapContext);
const options = useOptions();
const { displayConfig } = useEnvironment();
const { queryParams } = useRouter();

if (componentName !== 'GoogleOneTap') {
if (!context || context.componentName !== 'GoogleOneTap') {
throw new Error('Clerk: useGoogleOneTapContext called outside GoogleOneTap.');
}

const { componentName, ...ctx } = context;

const generateCallbackUrls = useCallback(
(returnBackUrl: string): HandleOAuthCallbackParams => {
const redirectUrls = new RedirectUrls(
Expand Down Expand Up @@ -704,11 +783,19 @@ export type WaitlistContextType = WaitlistCtx & {
redirectUrl?: string;
};

export const WaitlistContext = React.createContext<WaitlistCtx | null>(null);

export const useWaitlistContext = (): WaitlistContextType => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as WaitlistCtx;
const context = React.useContext(WaitlistContext);
const { displayConfig } = useEnvironment();
const options = useOptions();

if (!context || context.componentName !== 'Waitlist') {
throw new Error('Clerk: useWaitlistContext called outside Waitlist.');
}

const { componentName, ...ctx } = context;

let signInUrl = ctx.signInUrl || options.signInUrl || displayConfig.signInUrl;
signInUrl = buildURL({ base: signInUrl }, { stringify: true });

Expand Down
Loading

0 comments on commit 44e98f2

Please sign in to comment.