From f5d5a8166ec01faacc8c120d80c2e75c73bfabaf Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:19:02 -0800 Subject: [PATCH 1/5] wip --- .../src/ui/components/SignIn/SignIn.tsx | 6 +- .../ui/contexts/ClerkUIComponentsContext.tsx | 29 ++++++++-- packages/clerk-js/src/ui/portal/index.tsx | 57 ++++++++++--------- .../src/ui/utils/test/createFixtures.tsx | 15 ++++- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 0f4287db471..1229b10dcf5 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -3,7 +3,7 @@ import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; import { SignInEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; -import { ComponentContext, useSignInContext, withCoreSessionSwitchGuard } from '../../contexts'; +import { SignInContext, useSignInContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; import { ResetPassword } from './ResetPassword'; @@ -85,7 +85,7 @@ export const SignInModal = (props: SignInModalProps): JSX.Element => { return ( - { routing='virtual' /> - + ); }; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 14a42d66829..90a7ce67b05 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -36,6 +36,21 @@ const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ }); export const ComponentContext = React.createContext(null); +export function componentContextWrapper({ componentName }: { componentName: 'SignIn' }): typeof SignInContext; +export function componentContextWrapper({ componentName }: { componentName: string }): typeof ComponentContext; +export function componentContextWrapper({ + componentName, +}: { + componentName: string; +}): typeof SignInContext | typeof ComponentContext { + switch (componentName) { + case 'SignIn': + return SignInContext; + default: + return ComponentContext; + } +} + const getInitialValuesFromQueryParams = (queryString: string, params: string[]) => { const props: Record = {}; const searchParams = new URLSearchParams(queryString); @@ -131,14 +146,22 @@ export type SignInContextType = SignInCtx & { transferable: boolean; }; +export const SignInContext = React.createContext(null); + export const useSignInContext = (): SignInContextType => { - const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignInCtx; + const context = React.useContext(SignInContext); const { navigate } = useRouter(); const { displayConfig } = useEnvironment(); const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); + if (context === null || context.componentName !== 'SignIn') { + throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`); + } + + const { componentName, ...ctx } = context; + const initialValuesFromQueryParams = useMemo( () => getInitialValuesFromQueryParams(queryString, SIGN_IN_INITIAL_VALUE_KEYS), [], @@ -154,10 +177,6 @@ export const useSignInContext = (): SignInContextType => { queryParams, ); - if (componentName !== 'SignIn') { - throw new Error('Clerk: useSignInContext called outside of the mounted SignIn component.'); - } - const afterSignInUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignInUrl()); const afterSignUpUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignUpUrl()); diff --git a/packages/clerk-js/src/ui/portal/index.tsx b/packages/clerk-js/src/ui/portal/index.tsx index 426ade2f227..862ce3ce261 100644 --- a/packages/clerk-js/src/ui/portal/index.tsx +++ b/packages/clerk-js/src/ui/portal/index.tsx @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'; import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants'; import { clerkErrorPathRouterMissingPath } from '../../core/errors'; import { normalizeRoutingOptions } from '../../utils/normalizeRoutingOptions'; -import { ComponentContext } from '../contexts'; +import { ComponentContext, componentContextWrapper } from '../contexts'; import { HashRouter, PathRouter, VirtualRouter } from '../router'; import type { AvailableComponentCtx } from '../types'; @@ -16,37 +16,40 @@ type PortalProps; -export class Portal extends React.PureComponent> { - render() { - const { props, component, componentName, node } = this.props; - const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props?.routing, path: props?.path }) }; - - const el = ( - - - {React.createElement(component, normalizedProps as PortalProps['props'])} - - - ); +export function Portal({ + props, + component, + componentName, + node, +}: PortalProps) { + const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props?.routing, path: props?.path }) }; - if (normalizedProps?.routing === 'path') { - if (!normalizedProps?.path) { - clerkErrorPathRouterMissingPath(componentName); - } + const ComponentContextProvider = componentContextWrapper({ componentName }); + const el = ( + + + {React.createElement(component, normalizedProps as PortalProps['props'])} + + + ); - return ReactDOM.createPortal( - - {el} - , - node, - ); + if (normalizedProps?.routing === 'path') { + if (!normalizedProps?.path) { + clerkErrorPathRouterMissingPath(componentName); } - return ReactDOM.createPortal({el}, node); + return ReactDOM.createPortal( + + {el} + , + node, + ); } + + return ReactDOM.createPortal({el}, node); } type VirtualBodyRootPortalProps> = { diff --git a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx index a1deee34314..303900c2a9d 100644 --- a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx +++ b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx @@ -4,7 +4,13 @@ import React from 'react'; import { Clerk as ClerkCtor } from '../../../core/clerk'; import { Client, Environment } from '../../../core/resources'; -import { ComponentContext, CoreClerkContextWrapper, EnvironmentProvider, OptionsProvider } from '../../contexts'; +import { + ComponentContext, + componentContextWrapper, + CoreClerkContextWrapper, + EnvironmentProvider, + OptionsProvider, +} from '../../contexts'; import { AppearanceProvider } from '../../customizables'; import { FlowMetadataProvider } from '../../elements'; import { RouteContext } from '../../router'; @@ -84,21 +90,24 @@ const unboundCreateFixtures = [ const MockClerkProvider = (props: any) => { const { children } = props; + + const ContextProvider = componentContextWrapper({ componentName }); return ( new Map() }} > + - + {children} - + From bd64602a8e4ab301981266cfa41325bb796d1af8 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:36:13 -0800 Subject: [PATCH 2/5] feat(clerk-js): Replace ComponentContext with SignUpContext and UserProfileContext --- .../src/ui/components/SignUp/SignUp.tsx | 6 ++-- .../ui/components/UserProfile/UserProfile.tsx | 6 ++-- .../ui/contexts/ClerkUIComponentsContext.tsx | 30 ++++++++++++++----- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 0ae292691c8..e231773fc81 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -3,7 +3,7 @@ import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; -import { ComponentContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; +import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; import { SignUpContinue } from './SignUpContinue'; @@ -98,7 +98,7 @@ export const SignUpModal = (props: SignUpModalProps): JSX.Element => { return ( - { routing='virtual' /> - + ); }; diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx index 0eef00cec53..ad641d35a5a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx @@ -1,7 +1,7 @@ import type { UserProfileModalProps, UserProfileProps } from '@clerk/types'; import React from 'react'; -import { ComponentContext, withCoreUserGuard } from '../../contexts'; +import { UserProfileContext, withCoreUserGuard } from '../../contexts'; import { Flow, localizationKeys } from '../../customizables'; import { NavbarMenuButtonRow, ProfileCard, withCardStateProvider } from '../../elements'; import { Route, Switch } from '../../router'; @@ -54,12 +54,12 @@ export const UserProfileModal = (props: UserProfileModalProps): JSX.Element => { return ( - + {/*TODO: Used by InvisibleRootBox, can we simplify? */}
-
+
); }; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 0d4751c7ff4..1ce81c775b5 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -38,15 +38,21 @@ const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ }); export const ComponentContext = React.createContext(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({ componentName, }: { componentName: string; -}): typeof SignInContext | typeof ComponentContext { +}): typeof SignInContext | typeof SignUpContext | typeof UserProfileContext | typeof ComponentContext { switch (componentName) { case 'SignIn': return SignInContext; + case 'SignUp': + return SignUpContext; + case 'UserProfile': + return UserProfileContext; default: return ComponentContext; } @@ -76,8 +82,10 @@ export type SignUpContextType = SignUpCtx & { waitlistUrl: string; }; +export const SignUpContext = React.createContext(null); + export const useSignUpContext = (): SignUpContextType => { - const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as SignUpCtx; + const context = React.useContext(SignUpContext); const { navigate } = useRouter(); const { displayConfig } = useEnvironment(); const { queryParams, queryString } = useRouter(); @@ -89,6 +97,12 @@ export const useSignUpContext = (): SignUpContextType => { [], ); + if (!context || context.componentName !== 'SignUp') { + throw new Error('Clerk: useSignUpContext called outside of the mounted SignUp component.'); + } + + const { componentName, ...ctx } = context; + const redirectUrls = new RedirectUrls( options, { @@ -99,10 +113,6 @@ export const useSignUpContext = (): SignUpContextType => { queryParams, ); - if (componentName !== 'SignUp') { - throw new Error('Clerk: useSignUpContext called outside of the mounted SignUp component.'); - } - const afterSignUpUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignUpUrl()); const afterSignInUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignInUrl()); @@ -253,15 +263,19 @@ export type UserProfileContextType = UserProfileCtx & { pages: PagesType; }; +export const UserProfileContext = React.createContext(null); + export const useUserProfileContext = (): UserProfileContextType => { - const { componentName, customPages, ...ctx } = (React.useContext(ComponentContext) || {}) as UserProfileCtx; + const context = React.useContext(UserProfileContext); const { queryParams } = useRouter(); const clerk = useClerk(); - if (componentName !== 'UserProfile') { + if (!context || context.componentName !== 'UserProfile') { throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.'); } + const { componentName, customPages, ...ctx } = context; + const pages = useMemo(() => { return createUserProfileCustomPages(customPages || [], clerk); }, [customPages]); From c0c85a17dce0a5717ff15df7a8bc868344446902 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:37:23 -0800 Subject: [PATCH 3/5] chore(repo): Add changeset --- .changeset/poor-countries-count.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-countries-count.md diff --git a/.changeset/poor-countries-count.md b/.changeset/poor-countries-count.md new file mode 100644 index 00000000000..55ffcf3a072 --- /dev/null +++ b/.changeset/poor-countries-count.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Internal change to use component-specific context providers. This change does not impact consumers. From 44e98f24f009d0a8efde8369de89071bf6921d08 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:47:20 -0800 Subject: [PATCH 4/5] feat(clerk-js): Replace ComponentContext with component-specific contexts --- .../CreateOrganization/CreateOrganization.tsx | 6 +- .../OrganizationProfile.tsx | 6 +- .../ui/components/UserVerification/index.tsx | 6 +- .../src/ui/components/Waitlist/Waitlist.tsx | 6 +- .../ui/contexts/ClerkUIComponentsContext.tsx | 147 ++++++++++++++---- packages/clerk-js/src/ui/portal/index.tsx | 23 +-- packages/clerk-js/src/ui/types.ts | 2 + .../src/ui/utils/test/createFixtures.tsx | 34 ++-- 8 files changed, 165 insertions(+), 65 deletions(-) diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx index 559555c0bdf..214817620b6 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx @@ -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'; @@ -37,11 +37,11 @@ export const CreateOrganizationModal = (props: CreateOrganizationModalProps): JS return ( - +
-
+
); }; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx index e540dacf640..116eb1f5d4b 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx @@ -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'; @@ -58,12 +58,12 @@ export const OrganizationProfileModal = (props: OrganizationProfileModalProps): return ( - + {/*TODO: Used by InvisibleRootBox, can we simplify? */}
-
+
); }; diff --git a/packages/clerk-js/src/ui/components/UserVerification/index.tsx b/packages/clerk-js/src/ui/components/UserVerification/index.tsx index 6a5d1d38634..964487cdef7 100644 --- a/packages/clerk-js/src/ui/components/UserVerification/index.tsx +++ b/packages/clerk-js/src/ui/components/UserVerification/index.tsx @@ -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'; @@ -37,7 +37,7 @@ const UserVerification: React.ComponentType<__experimental_UserVerificationProps const UserVerificationModal = (props: __experimental_UserVerificationModalProps): JSX.Element => { return ( - - + ); }; diff --git a/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx b/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx index b88a8f7e5d3..398b4707e02 100644 --- a/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx +++ b/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx @@ -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'; @@ -52,11 +52,11 @@ export const WaitlistModal = (props: WaitlistModalProps): JSX.Element => { return ( - +
-
+
); }; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 1ce81c775b5..429887243da 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -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'; @@ -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, @@ -35,26 +42,70 @@ import { const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ }); -export const ComponentContext = React.createContext(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 {children}; case 'SignUp': - return SignUpContext; + return {children}; case 'UserProfile': - return UserProfileContext; + return {children}; + case 'UserVerification': + return ( + + {children} + + ); + case 'UserButton': + return ( + + {children} + + ); + case 'OrganizationSwitcher': + return ( + + {children} + + ); + case 'OrganizationList': + return ( + + {children} + + ); + case 'OrganizationProfile': + return ( + + {children} + + ); + case 'CreateOrganization': + return ( + + {children} + + ); + case 'GoogleOneTap': + return ( + {children} + ); + case 'Waitlist': + return ( + + {children} + + ); default: - return ComponentContext; + throw new Error(`Unknown component context: ${componentName}`); } } @@ -291,30 +342,38 @@ export const useUserProfileContext = (): UserProfileContextType => { export type UserVerificationContextType = UserVerificationCtx; +export const UserVerificationContext = React.createContext(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(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; @@ -361,15 +420,19 @@ export const useUserButtonContext = () => { }; }; +export const OrganizationSwitcherContext = React.createContext(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; @@ -464,15 +527,19 @@ export const useOrganizationSwitcherContext = () => { }; }; +export const OrganizationListContext = React.createContext(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) => { @@ -550,16 +617,20 @@ export type OrganizationProfileContextType = OrganizationProfileCtx & { isGeneralPageRoot: boolean; }; +export const OrganizationProfileContext = React.createContext(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 = () => @@ -581,15 +652,19 @@ export const useOrganizationProfileContext = (): OrganizationProfileContextType }; }; +export const CreateOrganizationContext = React.createContext(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)); @@ -615,16 +690,20 @@ export const useCreateOrganizationContext = () => { }; }; +export const GoogleOneTapContext = React.createContext(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( @@ -704,11 +783,19 @@ export type WaitlistContextType = WaitlistCtx & { redirectUrl?: string; }; +export const WaitlistContext = React.createContext(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 }); diff --git a/packages/clerk-js/src/ui/portal/index.tsx b/packages/clerk-js/src/ui/portal/index.tsx index 862ce3ce261..69850b4a69d 100644 --- a/packages/clerk-js/src/ui/portal/index.tsx +++ b/packages/clerk-js/src/ui/portal/index.tsx @@ -5,16 +5,16 @@ import ReactDOM from 'react-dom'; import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants'; import { clerkErrorPathRouterMissingPath } from '../../core/errors'; import { normalizeRoutingOptions } from '../../utils/normalizeRoutingOptions'; -import { ComponentContext, componentContextWrapper } from '../contexts'; +import { ComponentContextProvider } from '../contexts'; import { HashRouter, PathRouter, VirtualRouter } from '../router'; -import type { AvailableComponentCtx } from '../types'; +import type { AvailableComponentCtx, AvailableComponentName } from '../types'; type PortalProps> = { node: HTMLDivElement; component: React.FunctionComponent | React.ComponentClass; // Aligning this with props attributes of ComponentControls props?: PropsType & RoutingOptions; -} & Pick; +} & { componentName: AvailableComponentName }; export function Portal({ props, @@ -24,13 +24,15 @@ export function Portal({ }: PortalProps) { const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props?.routing, path: props?.path }) }; - const ComponentContextProvider = componentContextWrapper({ componentName }); const el = ( - + {React.createElement(component, normalizedProps as PortalProps['props'])} - + ); if (normalizedProps?.routing === 'path') { @@ -56,7 +58,7 @@ type VirtualBodyRootPortalProps | React.ComponentClass; props?: PropsType; startPath: string; -} & Pick; +} & { componentName: AvailableComponentName }; export class VirtualBodyRootPortal extends React.PureComponent< VirtualBodyRootPortalProps @@ -76,9 +78,12 @@ export class VirtualBodyRootPortal extend return ReactDOM.createPortal( - + {React.createElement(component, props as PortalProps['props'])} - + , this.elRef, ); diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 04564ce52b3..366358c2d7d 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -106,3 +106,5 @@ export type AvailableComponentCtx = | OrganizationListCtx | GoogleOneTapCtx | WaitlistCtx; + +export type AvailableComponentName = AvailableComponentCtx['componentName']; diff --git a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx index 303900c2a9d..1692abeb502 100644 --- a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx +++ b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx @@ -5,22 +5,21 @@ import React from 'react'; import { Clerk as ClerkCtor } from '../../../core/clerk'; import { Client, Environment } from '../../../core/resources'; import { - ComponentContext, - componentContextWrapper, + ComponentContextProvider, CoreClerkContextWrapper, EnvironmentProvider, + GoogleOneTapContext, OptionsProvider, } from '../../contexts'; import { AppearanceProvider } from '../../customizables'; import { FlowMetadataProvider } from '../../elements'; import { RouteContext } from '../../router'; import { InternalThemeProvider } from '../../styledSystem'; +import type { AvailableComponentName, AvailableComponentProps } from '../../types'; import { createClientFixtureHelpers, createEnvironmentFixtureHelpers } from './fixtureHelpers'; import { createBaseClientJSON, createBaseEnvironmentJSON } from './fixtures'; import { mockClerkMethods, mockRouteContextValue } from './mockHelpers'; -type UnpackContext = NonNullable ? U : T>; - const createInitialStateConfigParam = (baseEnvironment: EnvironmentJSON, baseClient: ClientJSON) => { return { ...createEnvironmentFixtureHelpers(baseEnvironment), @@ -40,8 +39,8 @@ export const bindCreateFixtures = ( return { createFixtures: unboundCreateFixtures(componentName, mockOpts) }; }; -const unboundCreateFixtures = ['componentName']>( - componentName: N, +const unboundCreateFixtures = ( + componentName: AvailableComponentName, mockOpts?: { router?: Parameters[0]; }, @@ -81,7 +80,7 @@ const unboundCreateFixtures = [ options: optionsMock, }; - let componentContextProps: Partial & { componentName: N }>; + let componentContextProps: AvailableComponentProps; const props = { setProps: (props: typeof componentContextProps) => { componentContextProps = props; @@ -91,24 +90,31 @@ const unboundCreateFixtures = [ const MockClerkProvider = (props: any) => { const { children } = props; - const ContextProvider = componentContextWrapper({ componentName }); + const componentsWithoutContext = ['UsernameSection', 'UserProfileSection']; + const contextWrappedChildren = !componentsWithoutContext.includes(componentName) ? ( + + {children} + + ) : ( + <>{children} + ); + return ( new Map() }} > - + - - - {children} - - + {contextWrappedChildren} From b4225f178a818bec40fd2ba6bef79b460ebcbfe0 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:36:17 -0800 Subject: [PATCH 5/5] fix(clerk-js): Remove unnecessary GoogleOneTapProvider --- packages/clerk-js/src/ui/utils/test/createFixtures.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx index 1692abeb502..6ce3783e8dd 100644 --- a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx +++ b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx @@ -8,7 +8,6 @@ import { ComponentContextProvider, CoreClerkContextWrapper, EnvironmentProvider, - GoogleOneTapContext, OptionsProvider, } from '../../contexts'; import { AppearanceProvider } from '../../customizables'; @@ -108,7 +107,6 @@ const unboundCreateFixtures = ( // Clear swr cache swrConfig={{ provider: () => new Map() }} > -