From 101ba6b60180708fc4839aef2718fa9484553d06 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 23 Nov 2023 11:11:14 +0200 Subject: [PATCH] feat(clerk-js,types,nextjs,remix): Terse path parameters (#1957) * feat(clerk-js,types,nextjs,remix): Terse paths parameters * chore(repo): Warning instead of erroring for no unused variables rule * chore(repo): Remove changes from NextJS playground * feat(clerk-js,clerk-react,types): Terse params for OrganizationSwitcher and UserButton * fix(clerk-js): Use only userProfileUrl prop to check what userProfileMode should be used * test(clerk-js): Added tests for normalizeRoutingOptions utility function * chore(repo): Added Changeset * feat(clerk-js): Omit routing prop from Modal components * chore(types): Fix typo * fix(clerk-js): Check all falsy values instead of just undefined * test(clerk-js): Fix tests for normalizeRoutingOptions * feat(types): Added WithoutRouting type helper * fix(remix): Revert SignIn and SIgnUp components * chore(clerk-js): Remove uneeded import * fix(remix): Revert changes * fix(remix,nextjs,types): Components with routing * test(clerk-react): Added types tests for SignIn component * test(clerk-react): Added types tests for SignUp component * test(clerk-react): Added types tests for UserButton component * test(clerk-react): Added types tests for OrganizationSwitcher component * test(clerk-react): Added type tests for UserProfile and OrganizationProfile components * feat(clerk-js): Apply changes to ui.retheme * refactor(clerk-js): Make normalizeRoutingOptions more verbose * chore(clerk-js): Remove unedeed type * refactor(clerk-js,types): Add types for modal components * refactor(nextjs,remix): Refactor SignIn/SignUp components for Next and Remix * tests(repo): Add integration tests * chore(repo): Fix formatting * test(repo): Update navigation integration tests * test(repo): Update navigation integration tests * test(repo): Add more test cases for navigation integration tests --- .changeset/dry-feet-join.md | 11 + .../src/app/user/[[...catchall]]/page.tsx | 9 + .../next-app-router/src/middleware.ts | 2 +- integration/templates/react-vite/src/main.tsx | 5 + .../react-vite/src/sign-in/index.tsx | 1 - .../react-vite/src/sign-up/index.tsx | 1 - .../templates/react-vite/src/user/index.tsx | 9 + integration/testUtils/index.ts | 2 + .../testUtils/userProfilePageObject.ts | 20 ++ integration/tests/navigation.test.ts | 200 +++++++++++++++ .../CreateOrganization/CreateOrganization.tsx | 4 +- .../OrganizationProfile.tsx | 4 +- .../ui.retheme/components/SignIn/SignIn.tsx | 4 +- .../ui.retheme/components/SignUp/SignUp.tsx | 6 +- .../components/UserProfile/UserProfile.tsx | 4 +- .../contexts/ClerkUIComponentsContext.tsx | 13 +- .../clerk-js/src/ui.retheme/portal/index.tsx | 15 +- .../common/__tests__/authPropHelpers.test.ts | 15 ++ .../CreateOrganization/CreateOrganization.tsx | 4 +- .../OrganizationProfile.tsx | 4 +- .../src/ui/components/SignIn/SignIn.tsx | 4 +- .../src/ui/components/SignUp/SignUp.tsx | 8 +- .../ui/components/UserProfile/UserProfile.tsx | 4 +- .../ui/contexts/ClerkUIComponentsContext.tsx | 13 +- packages/clerk-js/src/ui/portal/index.tsx | 15 +- .../clerk-js/src/utils/authPropHelpers.ts | 16 +- .../src/client-boundary/uiComponents.tsx | 40 +-- .../__tests__/OrganizationProfile.test.tsx | 39 +++ .../__tests__/OrganizationSwitcher.test.tsx | 42 ++++ .../src/components/__tests__/SignIn.test.tsx | 39 +++ .../src/components/__tests__/SignUp.test.tsx | 39 +++ .../components/__tests__/UserButton.test.tsx | 46 ++++ .../components/__tests__/UserProfile.test.tsx | 39 +++ .../react/src/components/uiComponents.tsx | 9 +- packages/react/src/components/withClerk.tsx | 4 +- packages/remix/src/client/uiComponents.tsx | 16 +- packages/types/src/clerk.retheme.ts | 230 ++++++++---------- packages/types/src/clerk.ts | 230 ++++++++---------- packages/types/src/utils.ts | 7 + .../create-organization/[[...index]].tsx | 1 - .../nextjs/pages/sign-in/[[...index]].tsx | 1 - 41 files changed, 836 insertions(+), 339 deletions(-) create mode 100644 .changeset/dry-feet-join.md create mode 100644 integration/templates/next-app-router/src/app/user/[[...catchall]]/page.tsx create mode 100644 integration/templates/react-vite/src/user/index.tsx create mode 100644 integration/testUtils/userProfilePageObject.ts create mode 100644 integration/tests/navigation.test.ts create mode 100644 packages/clerk-js/src/ui/common/__tests__/authPropHelpers.test.ts create mode 100644 packages/react/src/components/__tests__/OrganizationProfile.test.tsx create mode 100644 packages/react/src/components/__tests__/OrganizationSwitcher.test.tsx create mode 100644 packages/react/src/components/__tests__/SignIn.test.tsx create mode 100644 packages/react/src/components/__tests__/SignUp.test.tsx create mode 100644 packages/react/src/components/__tests__/UserButton.test.tsx create mode 100644 packages/react/src/components/__tests__/UserProfile.test.tsx diff --git a/.changeset/dry-feet-join.md b/.changeset/dry-feet-join.md new file mode 100644 index 00000000000..c6999eb2e93 --- /dev/null +++ b/.changeset/dry-feet-join.md @@ -0,0 +1,11 @@ +--- +'@clerk/clerk-js': major +'@clerk/nextjs': minor +'@clerk/clerk-react': minor +'@clerk/remix': minor +'@clerk/types': patch +--- + +- By default, all the components with routing will have the `routing` prop assigned as `'path'` by default when the `path` prop is filled. +- The `` component will set the default value of the `userProfileMode` prop to `'navigation'` if the `userProfileUrl` prop is provided. +- The `` component will have the `organizationProfileMode` and `createOrganizationMode` props assigned with `'navigation'` by default if the `organizationProfileUrl` and `createOrganizationUrl` props are filled accordingly. diff --git a/integration/templates/next-app-router/src/app/user/[[...catchall]]/page.tsx b/integration/templates/next-app-router/src/app/user/[[...catchall]]/page.tsx new file mode 100644 index 00000000000..9e9a4c8e8dd --- /dev/null +++ b/integration/templates/next-app-router/src/app/user/[[...catchall]]/page.tsx @@ -0,0 +1,9 @@ +import { UserProfile } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/integration/templates/next-app-router/src/middleware.ts b/integration/templates/next-app-router/src/middleware.ts index 13ffa4ae502..c421a473eee 100644 --- a/integration/templates/next-app-router/src/middleware.ts +++ b/integration/templates/next-app-router/src/middleware.ts @@ -1,7 +1,7 @@ import { authMiddleware } from '@clerk/nextjs/server'; export default authMiddleware({ - publicRoutes: ['/'], + publicRoutes: ['/', '/hash/sign-in', '/hash/sign-up'], }); export const config = { diff --git a/integration/templates/react-vite/src/main.tsx b/integration/templates/react-vite/src/main.tsx index 1615de2a228..4609a674b38 100644 --- a/integration/templates/react-vite/src/main.tsx +++ b/integration/templates/react-vite/src/main.tsx @@ -7,6 +7,7 @@ import App from './App.tsx'; import Protected from './protected'; import SignIn from './sign-in'; import SignUp from './sign-up'; +import UserProfile from './user'; const Root = () => { const navigate = useNavigate(); @@ -39,6 +40,10 @@ const router = createBrowserRouter([ path: '/sign-up/*', element: , }, + { + path: '/user/*', + element: , + }, { path: '/protected', element: , diff --git a/integration/templates/react-vite/src/sign-in/index.tsx b/integration/templates/react-vite/src/sign-in/index.tsx index 187e1bedb07..39caef7d00c 100644 --- a/integration/templates/react-vite/src/sign-in/index.tsx +++ b/integration/templates/react-vite/src/sign-in/index.tsx @@ -4,7 +4,6 @@ export default function Page() { return (
diff --git a/integration/templates/react-vite/src/sign-up/index.tsx b/integration/templates/react-vite/src/sign-up/index.tsx index 085288b9899..9aef78cf857 100644 --- a/integration/templates/react-vite/src/sign-up/index.tsx +++ b/integration/templates/react-vite/src/sign-up/index.tsx @@ -5,7 +5,6 @@ export default function Page() {
diff --git a/integration/templates/react-vite/src/user/index.tsx b/integration/templates/react-vite/src/user/index.tsx new file mode 100644 index 00000000000..007d4639ecb --- /dev/null +++ b/integration/templates/react-vite/src/user/index.tsx @@ -0,0 +1,9 @@ +import { UserProfile } from '@clerk/clerk-react'; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts index fd7de347e1f..40f63e55e2d 100644 --- a/integration/testUtils/index.ts +++ b/integration/testUtils/index.ts @@ -7,6 +7,7 @@ import { createEmailService } from './emailService'; import type { EnchancedPage, TestArgs } from './signInPageObject'; import { createSignInComponentPageObject } from './signInPageObject'; import { createSignUpComponentPageObject } from './signUpPageObject'; +import { createUserProfileComponentPageObject } from './userProfilePageObject'; import type { FakeUser } from './usersService'; import { createUserService } from './usersService'; @@ -64,6 +65,7 @@ export const createTestUtils = < const pageObjects = { signUp: createSignUpComponentPageObject(testArgs), signIn: createSignInComponentPageObject(testArgs), + userProfile: createUserProfileComponentPageObject(testArgs), expect: createExpectPageObject(testArgs), }; diff --git a/integration/testUtils/userProfilePageObject.ts b/integration/testUtils/userProfilePageObject.ts new file mode 100644 index 00000000000..c263a1cf862 --- /dev/null +++ b/integration/testUtils/userProfilePageObject.ts @@ -0,0 +1,20 @@ +import type { Browser, BrowserContext } from '@playwright/test'; + +import type { createAppPageObject } from './appPageObject'; + +export type EnchancedPage = ReturnType; +export type TestArgs = { page: EnchancedPage; context: BrowserContext; browser: Browser }; + +export const createUserProfileComponentPageObject = (testArgs: TestArgs) => { + const { page } = testArgs; + const self = { + goTo: async (opts?: { searchParams: URLSearchParams }) => { + await page.goToRelative('/user', opts); + return self.waitForMounted(); + }, + waitForMounted: () => { + return page.waitForSelector('.cl-userProfile-root', { state: 'attached' }); + }, + }; + return self; +}; diff --git a/integration/tests/navigation.test.ts b/integration/tests/navigation.test.ts new file mode 100644 index 00000000000..637d2b9a811 --- /dev/null +++ b/integration/tests/navigation.test.ts @@ -0,0 +1,200 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils } from '../testUtils'; + +test.describe('navigation modes @generic', () => { + test.describe.configure({ mode: 'serial' }); + let app: Application; + let fakeUser: FakeUser; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/app/provider.tsx', + () => `'use client' +import { ClerkProvider } from "@clerk/nextjs" + +export function Provider({ children }: { children: any }) { + return ( + + {children} + + ) +}`, + ) + .addFile( + 'src/app/layout.tsx', + () => `import './globals.css'; +import { Inter } from 'next/font/google'; +import { Provider } from './provider'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +}`, + ) + .addFile( + 'src/app/hash/user/[[...catchall]]/page.tsx', + () => ` +import { UserProfile, UserButton } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ + +
+ ); +}`, + ) + .addFile( + 'src/app/hash/sign-in/[[...catchall]]/page.tsx', + () => ` +import { SignIn } from '@clerk/nextjs'; + +export default function Page() { + return ( + + ); +}`, + ) + .commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withEmailCodes); + await app.build(); + + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + + await app.serve(); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('user profile with path routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.po.userProfile.goTo(); + await u.po.userProfile.waitForMounted(); + + await u.page.getByText(/Set username/i).click(); + + await u.page.waitForURL(`${app.serverUrl}/user/username`); + + await u.page.getByText(/Cancel/i).click(); + + await u.page.waitForURL(`${app.serverUrl}/user`); + + await u.page.getByText(/Add an email address/i).click(); + + await u.page.waitForURL(`${app.serverUrl}/user/email-address`); + + await u.page.getByText(/Cancel/i).click(); + + await u.page.waitForURL(`${app.serverUrl}/user`); + }); + + test('user profile with hash routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/hash/user'); + await u.po.userProfile.waitForMounted(); + + await u.page.getByText(/Set username/i).click(); + + expect(u.page.url()).toBe(`${app.serverUrl}/hash/user#/username`); + + await u.page.getByText(/Cancel/i).click(); + + expect(u.page.url()).toBe(`${app.serverUrl}/hash/user#`); + + await u.page.getByText(/Add an email address/i).click(); + + expect(u.page.url()).toBe(`${app.serverUrl}/hash/user#/email-address`); + + await u.page.getByText(/Cancel/i).click(); + + expect(u.page.url()).toBe(`${app.serverUrl}/hash/user#`); + }); + + test('sign in with path routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.page.waitForURL(`${app.serverUrl}/sign-in/factor-one`); + + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + + await u.po.expect.toBeSignedIn(); + }); + + test('sign in with hash routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/hash/sign-in'); + await u.po.signIn.waitForMounted(); + + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.page.waitForURL(`${app.serverUrl}/hash/sign-in#/factor-one`); + + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + + await u.po.expect.toBeSignedIn(); + }); + + test('user profile from user button navigates correctly', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/'); + await u.page.waitForClerkComponentMounted(); + + await u.page.getByRole('button', { name: 'Open user button' }).click(); + + await u.page.getByText(/Manage account/).click(); + + await u.page.waitForSelector('.cl-modalContent > .cl-userProfile-root', { state: 'attached' }); + + await u.page.getByText(/Set username/i).click(); + await u.page.getByText(/Cancel/i).click(); + + await u.page.getByText(/Add an email address/i).click(); + await u.page.getByText(/Cancel/i).click(); + }); +}); diff --git a/packages/clerk-js/src/ui.retheme/components/CreateOrganization/CreateOrganization.tsx b/packages/clerk-js/src/ui.retheme/components/CreateOrganization/CreateOrganization.tsx index 69c87f8753a..be2ba48bdad 100644 --- a/packages/clerk-js/src/ui.retheme/components/CreateOrganization/CreateOrganization.tsx +++ b/packages/clerk-js/src/ui.retheme/components/CreateOrganization/CreateOrganization.tsx @@ -1,4 +1,4 @@ -import type { CreateOrganizationProps } from '@clerk/types'; +import type { CreateOrganizationModalProps, CreateOrganizationProps } from '@clerk/types'; import { withOrganizationsEnabledGuard } from '../../common'; import { ComponentContext, withCoreUserGuard } from '../../contexts'; @@ -38,7 +38,7 @@ export const CreateOrganization = withOrganizationsEnabledGuard( { mode: 'redirect' }, ); -export const CreateOrganizationModal = (props: CreateOrganizationProps): JSX.Element => { +export const CreateOrganizationModal = (props: CreateOrganizationModalProps): JSX.Element => { const createOrganizationProps: CreateOrganizationCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfile.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfile.tsx index 425bd5a9a64..6b856730520 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfile.tsx @@ -1,4 +1,4 @@ -import type { OrganizationProfileProps } from '@clerk/types'; +import type { OrganizationProfileModalProps, OrganizationProfileProps } from '@clerk/types'; import React from 'react'; import { withOrganizationsEnabledGuard, withRedirectToHomeOrganizationGuard } from '../../common'; @@ -48,7 +48,7 @@ export const OrganizationProfile = withRedirectToHomeOrganizationGuard( }), ); -export const OrganizationProfileModal = (props: OrganizationProfileProps): JSX.Element => { +export const OrganizationProfileModal = (props: OrganizationProfileModalProps): JSX.Element => { const organizationProfileProps: OrganizationProfileCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui.retheme/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui.retheme/components/SignIn/SignIn.tsx index 8bb09cc7df7..424de26d85a 100644 --- a/packages/clerk-js/src/ui.retheme/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui.retheme/components/SignIn/SignIn.tsx @@ -1,4 +1,4 @@ -import type { SignInProps } from '@clerk/types'; +import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; import { SignInEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -74,7 +74,7 @@ SignInRoutes.displayName = 'SignIn'; export const SignIn: React.ComponentType = withCoreSessionSwitchGuard(SignInRoutes); -export const SignInModal = (props: SignInProps): JSX.Element => { +export const SignInModal = (props: SignInModalProps): JSX.Element => { const signInProps = { signUpUrl: `/${VIRTUAL_ROUTER_BASE_PATH}/sign-up`, ...props, diff --git a/packages/clerk-js/src/ui.retheme/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui.retheme/components/SignUp/SignUp.tsx index b99a84e09f5..e7e8f9ad025 100644 --- a/packages/clerk-js/src/ui.retheme/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui.retheme/components/SignUp/SignUp.tsx @@ -1,4 +1,4 @@ -import type { SignUpProps } from '@clerk/types'; +import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -87,8 +87,8 @@ SignUpRoutes.displayName = 'SignUp'; export const SignUp: React.ComponentType = withCoreSessionSwitchGuard(SignUpRoutes); -export const SignUpModal = (props: SignUpProps): JSX.Element => { - const signUpProps: SignUpProps = { +export const SignUpModal = (props: SignUpModalProps): JSX.Element => { + const signUpProps = { signInUrl: `/${VIRTUAL_ROUTER_BASE_PATH}/sign-in`, ...props, }; diff --git a/packages/clerk-js/src/ui.retheme/components/UserProfile/UserProfile.tsx b/packages/clerk-js/src/ui.retheme/components/UserProfile/UserProfile.tsx index 2f7d13ed8b2..b9244243915 100644 --- a/packages/clerk-js/src/ui.retheme/components/UserProfile/UserProfile.tsx +++ b/packages/clerk-js/src/ui.retheme/components/UserProfile/UserProfile.tsx @@ -1,4 +1,4 @@ -import type { UserProfileProps } from '@clerk/types'; +import type { UserProfileModalProps, UserProfileProps } from '@clerk/types'; import React from 'react'; import { withRedirectToHomeUserGuard } from '../../common'; @@ -42,7 +42,7 @@ const AuthenticatedRoutes = withCoreUserGuard(() => { export const UserProfile = withRedirectToHomeUserGuard(withCardStateProvider(_UserProfile)); -export const UserProfileModal = (props: UserProfileProps): JSX.Element => { +export const UserProfileModal = (props: UserProfileModalProps): JSX.Element => { const userProfileProps: UserProfileCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui.retheme/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui.retheme/contexts/ClerkUIComponentsContext.tsx index 1d1ce8a0840..d2815dc85a7 100644 --- a/packages/clerk-js/src/ui.retheme/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui.retheme/contexts/ClerkUIComponentsContext.tsx @@ -241,6 +241,8 @@ export const useUserButtonContext = () => { const afterSwitchSessionUrl = ctx.afterSwitchSessionUrl || displayConfig.afterSwitchSessionUrl; const navigateAfterSwitchSession = () => navigate(afterSwitchSessionUrl); + const userProfileMode = !!ctx.userProfileUrl && !ctx.userProfileMode ? 'navigation' : ctx.userProfileMode; + return { ...ctx, componentName, @@ -252,6 +254,7 @@ export const useUserButtonContext = () => { afterMultiSessionSingleSignOutUrl, afterSignOutUrl, afterSwitchSessionUrl, + userProfileMode: userProfileMode || 'modal', }; }; @@ -310,11 +313,17 @@ export const useOrganizationSwitcherContext = () => { const navigateAfterSelectPersonal = (user: UserResource) => navigateAfterSelectOrganizationOrPersonal({ user }); + const organizationProfileMode = + !!ctx.organizationProfileUrl && !ctx.organizationProfileMode ? 'navigation' : ctx.organizationProfileMode; + + const createOrganizationMode = + !!ctx.createOrganizationUrl && !ctx.createOrganizationMode ? 'navigation' : ctx.createOrganizationMode; + return { ...ctx, hidePersonal: ctx.hidePersonal || false, - organizationProfileMode: ctx.organizationProfileMode || 'modal', - createOrganizationMode: ctx.createOrganizationMode || 'modal', + organizationProfileMode: organizationProfileMode || 'modal', + createOrganizationMode: createOrganizationMode || 'modal', afterCreateOrganizationUrl, afterLeaveOrganizationUrl, navigateOrganizationProfile, diff --git a/packages/clerk-js/src/ui.retheme/portal/index.tsx b/packages/clerk-js/src/ui.retheme/portal/index.tsx index b47c3016df7..7ecac34394e 100644 --- a/packages/clerk-js/src/ui.retheme/portal/index.tsx +++ b/packages/clerk-js/src/ui.retheme/portal/index.tsx @@ -1,8 +1,10 @@ +import type { RoutingOptions } from '@clerk/types'; import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants'; import { clerkErrorPathRouterMissingPath } from '../../core/errors'; +import { normalizeRoutingOptions } from '../../utils/authPropHelpers'; import { ComponentContext } from '../contexts'; import { HashRouter, PathRouter } from '../router'; import type { AvailableComponentCtx } from '../types'; @@ -11,28 +13,29 @@ type PortalProps | React.ComponentClass; // Aligning this with props attributes of ComponentControls - props?: PropsType & { path?: string; routing?: string }; + props: PropsType & RoutingOptions; } & Pick; export class Portal extends React.PureComponent> { render(): React.ReactPortal { const { props, component, componentName, node } = this.props; + const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props.routing, path: props.path }) }; const el = ( - - {React.createElement(component, props)} + + {React.createElement(component, normalizedProps)} ); - if (props?.routing === 'path') { - if (!props?.path) { + if (normalizedProps?.routing === 'path') { + if (!normalizedProps?.path) { clerkErrorPathRouterMissingPath(componentName); } return ReactDOM.createPortal( {el} , diff --git a/packages/clerk-js/src/ui/common/__tests__/authPropHelpers.test.ts b/packages/clerk-js/src/ui/common/__tests__/authPropHelpers.test.ts new file mode 100644 index 00000000000..7cc2241ca4c --- /dev/null +++ b/packages/clerk-js/src/ui/common/__tests__/authPropHelpers.test.ts @@ -0,0 +1,15 @@ +import { normalizeRoutingOptions } from '../../../utils/authPropHelpers'; + +describe('auth prop helpers', () => { + describe('normalizeRoutingOptions', () => { + it("returns routing: 'path' if path was provided and no routing was provided", () => { + expect(normalizeRoutingOptions({ path: 'test' })).toEqual({ routing: 'path', path: 'test' }); + }); + + it('returns the same options if routing was provided (any routing) and path was provided (avoid breaking integrations)', () => { + expect(normalizeRoutingOptions({ routing: 'path', path: 'test' })).toEqual({ routing: 'path', path: 'test' }); + expect(normalizeRoutingOptions({ routing: 'hash', path: 'test' })).toEqual({ routing: 'hash', path: 'test' }); + expect(normalizeRoutingOptions({ routing: 'virtual' })).toEqual({ routing: 'virtual' }); + }); + }); +}); diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx index 69c87f8753a..981879f92bd 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx @@ -1,4 +1,4 @@ -import type { CreateOrganizationProps } from '@clerk/types'; +import type { CreateOrganizationModalProps } from '@clerk/types'; import { withOrganizationsEnabledGuard } from '../../common'; import { ComponentContext, withCoreUserGuard } from '../../contexts'; @@ -38,7 +38,7 @@ export const CreateOrganization = withOrganizationsEnabledGuard( { mode: 'redirect' }, ); -export const CreateOrganizationModal = (props: CreateOrganizationProps): JSX.Element => { +export const CreateOrganizationModal = (props: CreateOrganizationModalProps): JSX.Element => { const createOrganizationProps: CreateOrganizationCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx index 425bd5a9a64..6b856730520 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx @@ -1,4 +1,4 @@ -import type { OrganizationProfileProps } from '@clerk/types'; +import type { OrganizationProfileModalProps, OrganizationProfileProps } from '@clerk/types'; import React from 'react'; import { withOrganizationsEnabledGuard, withRedirectToHomeOrganizationGuard } from '../../common'; @@ -48,7 +48,7 @@ export const OrganizationProfile = withRedirectToHomeOrganizationGuard( }), ); -export const OrganizationProfileModal = (props: OrganizationProfileProps): JSX.Element => { +export const OrganizationProfileModal = (props: OrganizationProfileModalProps): JSX.Element => { const organizationProfileProps: OrganizationProfileCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 8bb09cc7df7..424de26d85a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -1,4 +1,4 @@ -import type { SignInProps } from '@clerk/types'; +import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; import { SignInEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -74,7 +74,7 @@ SignInRoutes.displayName = 'SignIn'; export const SignIn: React.ComponentType = withCoreSessionSwitchGuard(SignInRoutes); -export const SignInModal = (props: SignInProps): JSX.Element => { +export const SignInModal = (props: SignInModalProps): JSX.Element => { const signInProps = { signUpUrl: `/${VIRTUAL_ROUTER_BASE_PATH}/sign-up`, ...props, diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index b99a84e09f5..d33dce3d241 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -1,4 +1,4 @@ -import type { SignUpProps } from '@clerk/types'; +import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -87,8 +87,8 @@ SignUpRoutes.displayName = 'SignUp'; export const SignUp: React.ComponentType = withCoreSessionSwitchGuard(SignUpRoutes); -export const SignUpModal = (props: SignUpProps): JSX.Element => { - const signUpProps: SignUpProps = { +export const SignUpModal = (props: SignUpModalProps): JSX.Element => { + const signUpProps = { signInUrl: `/${VIRTUAL_ROUTER_BASE_PATH}/sign-in`, ...props, }; @@ -97,8 +97,8 @@ export const SignUpModal = (props: SignUpProps): JSX.Element => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx index 2f7d13ed8b2..b9244243915 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfile.tsx @@ -1,4 +1,4 @@ -import type { UserProfileProps } from '@clerk/types'; +import type { UserProfileModalProps, UserProfileProps } from '@clerk/types'; import React from 'react'; import { withRedirectToHomeUserGuard } from '../../common'; @@ -42,7 +42,7 @@ const AuthenticatedRoutes = withCoreUserGuard(() => { export const UserProfile = withRedirectToHomeUserGuard(withCardStateProvider(_UserProfile)); -export const UserProfileModal = (props: UserProfileProps): JSX.Element => { +export const UserProfileModal = (props: UserProfileModalProps): JSX.Element => { const userProfileProps: UserProfileCtx = { ...props, routing: 'virtual', diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 1d1ce8a0840..d2815dc85a7 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -241,6 +241,8 @@ export const useUserButtonContext = () => { const afterSwitchSessionUrl = ctx.afterSwitchSessionUrl || displayConfig.afterSwitchSessionUrl; const navigateAfterSwitchSession = () => navigate(afterSwitchSessionUrl); + const userProfileMode = !!ctx.userProfileUrl && !ctx.userProfileMode ? 'navigation' : ctx.userProfileMode; + return { ...ctx, componentName, @@ -252,6 +254,7 @@ export const useUserButtonContext = () => { afterMultiSessionSingleSignOutUrl, afterSignOutUrl, afterSwitchSessionUrl, + userProfileMode: userProfileMode || 'modal', }; }; @@ -310,11 +313,17 @@ export const useOrganizationSwitcherContext = () => { const navigateAfterSelectPersonal = (user: UserResource) => navigateAfterSelectOrganizationOrPersonal({ user }); + const organizationProfileMode = + !!ctx.organizationProfileUrl && !ctx.organizationProfileMode ? 'navigation' : ctx.organizationProfileMode; + + const createOrganizationMode = + !!ctx.createOrganizationUrl && !ctx.createOrganizationMode ? 'navigation' : ctx.createOrganizationMode; + return { ...ctx, hidePersonal: ctx.hidePersonal || false, - organizationProfileMode: ctx.organizationProfileMode || 'modal', - createOrganizationMode: ctx.createOrganizationMode || 'modal', + organizationProfileMode: organizationProfileMode || 'modal', + createOrganizationMode: createOrganizationMode || 'modal', afterCreateOrganizationUrl, afterLeaveOrganizationUrl, navigateOrganizationProfile, diff --git a/packages/clerk-js/src/ui/portal/index.tsx b/packages/clerk-js/src/ui/portal/index.tsx index b47c3016df7..7ecac34394e 100644 --- a/packages/clerk-js/src/ui/portal/index.tsx +++ b/packages/clerk-js/src/ui/portal/index.tsx @@ -1,8 +1,10 @@ +import type { RoutingOptions } from '@clerk/types'; import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants'; import { clerkErrorPathRouterMissingPath } from '../../core/errors'; +import { normalizeRoutingOptions } from '../../utils/authPropHelpers'; import { ComponentContext } from '../contexts'; import { HashRouter, PathRouter } from '../router'; import type { AvailableComponentCtx } from '../types'; @@ -11,28 +13,29 @@ type PortalProps | React.ComponentClass; // Aligning this with props attributes of ComponentControls - props?: PropsType & { path?: string; routing?: string }; + props: PropsType & RoutingOptions; } & Pick; export class Portal extends React.PureComponent> { render(): React.ReactPortal { const { props, component, componentName, node } = this.props; + const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props.routing, path: props.path }) }; const el = ( - - {React.createElement(component, props)} + + {React.createElement(component, normalizedProps)} ); - if (props?.routing === 'path') { - if (!props?.path) { + if (normalizedProps?.routing === 'path') { + if (!normalizedProps?.path) { clerkErrorPathRouterMissingPath(componentName); } return ReactDOM.createPortal( {el} , diff --git a/packages/clerk-js/src/utils/authPropHelpers.ts b/packages/clerk-js/src/utils/authPropHelpers.ts index 145275d856d..e2ebff85274 100644 --- a/packages/clerk-js/src/utils/authPropHelpers.ts +++ b/packages/clerk-js/src/utils/authPropHelpers.ts @@ -1,5 +1,5 @@ import { camelToSnake } from '@clerk/shared'; -import type { ClerkOptions, DisplayConfigResource } from '@clerk/types'; +import type { ClerkOptions, DisplayConfigResource, RoutingOptions, RoutingStrategy } from '@clerk/types'; import type { ParsedQs } from 'qs'; import qs from 'qs'; @@ -112,3 +112,17 @@ export const buildAuthQueryString = (data: BuildAuthQueryStringArgs): string | n } return Object.keys(query).length === 0 ? null : qs.stringify(query); }; + +export const normalizeRoutingOptions = ({ + routing, + path, +}: { + routing?: RoutingStrategy; + path?: string; +}): RoutingOptions => { + if (!!path && !routing) { + return { routing: 'path', path }; + } + + return { routing, path } as RoutingOptions; +}; diff --git a/packages/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx index b6d09addf2d..12aeeb0a259 100644 --- a/packages/nextjs/src/client-boundary/uiComponents.tsx +++ b/packages/nextjs/src/client-boundary/uiComponents.tsx @@ -7,42 +7,46 @@ import React from 'react'; import { useClerkNextOptions } from './NextOptionsContext'; export { - UserProfile, - UserButton, - OrganizationSwitcher, - OrganizationProfile, CreateOrganization, + OrganizationList, + OrganizationProfile, + OrganizationSwitcher, SignInButton, - SignUpButton, - SignOutButton, SignInWithMetamaskButton, - OrganizationList, + SignOutButton, + SignUpButton, + UserButton, + UserProfile, } from '@clerk/clerk-react'; export const SignIn = (props: SignInProps) => { const { signInUrl: repoLevelSignInUrl } = useClerkNextOptions(); - if (repoLevelSignInUrl) { - return ( - - ); + const path = props.path || repoLevelSignInUrl; + + if (path) { + ; } + return ; }; export const SignUp = (props: SignUpProps) => { const { signUpUrl: repoLevelSignUpUrl } = useClerkNextOptions(); - if (repoLevelSignUpUrl) { + const path = props.path || repoLevelSignUpUrl; + + if (path) { return ( ); } + return ; }; diff --git a/packages/react/src/components/__tests__/OrganizationProfile.test.tsx b/packages/react/src/components/__tests__/OrganizationProfile.test.tsx new file mode 100644 index 00000000000..93bb2eb123e --- /dev/null +++ b/packages/react/src/components/__tests__/OrganizationProfile.test.tsx @@ -0,0 +1,39 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { OrganizationProfile } from '..'; + +export type OrganizationProfileComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('has path filled', () => { + expectTypeOf({ path: '/org' }).toMatchTypeOf(); + }); + + test('has path filled and routing has path as a value', () => { + expectTypeOf({ + path: '/org', + routing: 'path' as const, + }).toMatchTypeOf(); + }); + + test('when path is filled, routing must only have path as value', () => { + expectTypeOf({ + path: '/org', + routing: 'virtual' as const, + }).not.toMatchTypeOf(); + + expectTypeOf({ + path: '/org', + routing: 'hash' as const, + }).not.toMatchTypeOf(); + }); + + test('when routing is hash or virtual path must be present', () => { + expectTypeOf({ + routing: 'hash' as const, + }).toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/__tests__/OrganizationSwitcher.test.tsx b/packages/react/src/components/__tests__/OrganizationSwitcher.test.tsx new file mode 100644 index 00000000000..75156cfd4ff --- /dev/null +++ b/packages/react/src/components/__tests__/OrganizationSwitcher.test.tsx @@ -0,0 +1,42 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { OrganizationSwitcher } from '..'; + +export type OrganizationSwitcherComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('createOrganizationUrl is a string', () => { + expectTypeOf({ + createOrganizationUrl: '/', + }).toMatchTypeOf(); + }); + + test('createOrganizationUrl is a string and createOrganizationMode is navigation', () => { + expectTypeOf({ + createOrganizationUrl: '/', + createOrganizationMode: 'navigation' as const, + }).toMatchTypeOf(); + }); + + test('createOrganizationUrl is a string and createOrganizationMode is not modal', () => { + expectTypeOf({ + createOrganizationUrl: '/', + createOrganizationMode: 'modal' as const, + }).not.toMatchTypeOf(); + }); + + test('createOrganizationMode is modal and path must not been present', () => { + expectTypeOf({ + createOrganizationMode: 'modal' as const, + }).toMatchTypeOf(); + }); + + test('createOrganizationMode is navigation and path is not present', () => { + expectTypeOf({ + createOrganizationMode: 'navigation' as const, + }).not.toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/__tests__/SignIn.test.tsx b/packages/react/src/components/__tests__/SignIn.test.tsx new file mode 100644 index 00000000000..a3044c496e8 --- /dev/null +++ b/packages/react/src/components/__tests__/SignIn.test.tsx @@ -0,0 +1,39 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { SignIn } from '..'; + +export type SignInComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('has path filled', () => { + expectTypeOf({ path: '/sign-in' }).toMatchTypeOf(); + }); + + test('has path filled and routing has path as a value', () => { + expectTypeOf({ + path: '/sign-in', + routing: 'path' as const, + }).toMatchTypeOf(); + }); + + test('when path is filled, routing must only have path as value', () => { + expectTypeOf({ + path: '/sign-in', + routing: 'virtual' as const, + }).not.toMatchTypeOf(); + + expectTypeOf({ + path: '/sign-in', + routing: 'hash' as const, + }).not.toMatchTypeOf(); + }); + + test('when routing is hash or virtual path must be present', () => { + expectTypeOf({ + routing: 'hash' as const, + }).toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/__tests__/SignUp.test.tsx b/packages/react/src/components/__tests__/SignUp.test.tsx new file mode 100644 index 00000000000..c1e6a84985c --- /dev/null +++ b/packages/react/src/components/__tests__/SignUp.test.tsx @@ -0,0 +1,39 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { SignUp } from '..'; + +export type SignUpComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('has path filled', () => { + expectTypeOf({ path: '/sign-up' }).toMatchTypeOf(); + }); + + test('has path filled and routing has path as a value', () => { + expectTypeOf({ + path: '/sign-up', + routing: 'path' as const, + }).toMatchTypeOf(); + }); + + test('when path is filled, routing must only have path as value', () => { + expectTypeOf({ + path: '/sign-up', + routing: 'virtual' as const, + }).not.toMatchTypeOf(); + + expectTypeOf({ + path: '/sign-up', + routing: 'hash' as const, + }).not.toMatchTypeOf(); + }); + + test('when routing is hash or virtual path must be present', () => { + expectTypeOf({ + routing: 'hash' as const, + }).toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/__tests__/UserButton.test.tsx b/packages/react/src/components/__tests__/UserButton.test.tsx new file mode 100644 index 00000000000..6051df91cf8 --- /dev/null +++ b/packages/react/src/components/__tests__/UserButton.test.tsx @@ -0,0 +1,46 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { UserButton } from '..'; + +export type UserButtonComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('userProfileUrl is a string', () => { + expectTypeOf({ userProfileUrl: '/' }).toMatchTypeOf(); + expectTypeOf<{ + userProfileUrl: string; + }>().toMatchTypeOf(); + }); + + test('userProfileUrl url is a string and userProfileMode is navigation', () => { + expectTypeOf({ + userProfileUrl: '/', + userProfileMode: 'navigation' as const, + }).toMatchTypeOf(); + expectTypeOf<{ + userProfileUrl: string; + }>().toMatchTypeOf(); + }); + + test('that when userProfileMode is navigation that userProfileUrl is filled', () => { + expectTypeOf({ + userProfileMode: 'navigation' as const, + }).not.toMatchTypeOf(); + }); + + test('that when userProfileMode is modal that userProfileUrl is not filled', () => { + expectTypeOf({ + userProfileMode: 'modal' as const, + userProfileUrl: '/', + }).not.toMatchTypeOf(); + }); + + test('userProfileMode is modal', () => { + expectTypeOf({ + userProfileMode: 'modal' as const, + }).toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/__tests__/UserProfile.test.tsx b/packages/react/src/components/__tests__/UserProfile.test.tsx new file mode 100644 index 00000000000..71e5378ee27 --- /dev/null +++ b/packages/react/src/components/__tests__/UserProfile.test.tsx @@ -0,0 +1,39 @@ +import { expectTypeOf } from 'expect-type'; +import type React from 'react'; + +import type { UserProfile } from '..'; + +export type UserProfileComponentProps = React.ComponentProps; + +describe('', () => { + describe('Type tests', () => { + test('has path filled', () => { + expectTypeOf({ path: '/profile' }).toMatchTypeOf(); + }); + + test('has path filled and routing has path as a value', () => { + expectTypeOf({ + path: '/profile', + routing: 'path' as const, + }).toMatchTypeOf(); + }); + + test('when path is filled, routing must only have path as value', () => { + expectTypeOf({ + path: '/profile', + routing: 'virtual' as const, + }).not.toMatchTypeOf(); + + expectTypeOf({ + path: '/profile', + routing: 'hash' as const, + }).not.toMatchTypeOf(); + }); + + test('when routing is hash or virtual path must be present', () => { + expectTypeOf({ + routing: 'hash' as const, + }).toMatchTypeOf(); + }); + }); +}); diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 925092650b8..2a8f23165d7 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -8,6 +8,7 @@ import type { SignUpProps, UserButtonProps, UserProfileProps, + Without, } from '@clerk/types'; import type { PropsWithChildren } from 'react'; import React, { createElement } from 'react'; @@ -39,7 +40,7 @@ type UserButtonExportType = typeof _UserButton & { UserProfileLink: typeof UserProfileLink; }; -type UserButtonPropsWithoutCustomPages = Omit & { +type UserButtonPropsWithoutCustomPages = Without & { userProfileProps?: Pick; }; @@ -53,7 +54,7 @@ type OrganizationSwitcherExportType = typeof _OrganizationSwitcher & { OrganizationProfileLink: typeof OrganizationProfileLink; }; -type OrganizationSwitcherPropsWithoutCustomPages = Omit & { +type OrganizationSwitcherPropsWithoutCustomPages = Without & { organizationProfileProps?: Pick; }; @@ -152,7 +153,7 @@ export function UserProfileLink({ children }: PropsWithChildren>>) => { + ({ clerk, ...props }: WithClerkProp>>) => { const { customPages, customPagesPortals } = useUserProfileCustomPages(props.children); return ( >>) => { + ({ clerk, ...props }: WithClerkProp>>) => { const { customPages, customPagesPortals } = useOrganizationProfileCustomPages(props.children); return ( ( ) => { displayName = displayName || Component.displayName || Component.name || 'Component'; Component.displayName = displayName; - const HOC = (props: Omit) => { + const HOC = (props: Without) => { const clerk = useIsomorphicClerkContext(); if (!clerk.loaded) { diff --git a/packages/remix/src/client/uiComponents.tsx b/packages/remix/src/client/uiComponents.tsx index 1150a74be2d..ff0a09d54c2 100644 --- a/packages/remix/src/client/uiComponents.tsx +++ b/packages/remix/src/client/uiComponents.tsx @@ -6,26 +6,30 @@ import { useClerkRemixOptions } from './RemixOptionsContext'; export const SignIn = (props: SignInProps) => { const { signInUrl } = useClerkRemixOptions(); - if (signInUrl) { + const path = props.path || signInUrl; + + if (path) { return ( ); } + return ; }; export const SignUp = (props: SignUpProps) => { const { signUpUrl } = useClerkRemixOptions(); - if (signUpUrl) { + const path = props.path || signUpUrl; + + if (path) { return ( ); } diff --git a/packages/types/src/clerk.retheme.ts b/packages/types/src/clerk.retheme.ts index 862342be61a..d27eb8768b0 100644 --- a/packages/types/src/clerk.retheme.ts +++ b/packages/types/src/clerk.retheme.ts @@ -536,6 +536,8 @@ export interface Resources { export type RoutingStrategy = 'path' | 'hash' | 'virtual'; +export type WithoutRouting = Omit; + export type SignInInitialValues = { emailAddress?: string; phoneNumber?: string; @@ -608,15 +610,11 @@ export type SetActiveParams = { export type SetActive = (params: SetActiveParams) => Promise; -export type SignInProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/sign in' - */ - path?: string; +export type RoutingOptions = + | { path: string | undefined; routing?: Extract } + | { path?: never; routing?: Extract }; + +export type SignInProps = RoutingOptions & { /** * Full URL or path to for the sign up process. * Used to fill the "Sign up" link in the SignUp component. @@ -634,15 +632,9 @@ export type SignInProps = { initialValues?: SignInInitialValues; } & RedirectOptions; -export type SignUpProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/sign up' - */ - path?: string; +export type SignInModalProps = WithoutRouting; + +export type SignUpProps = RoutingOptions & { /** * Full URL or path to for the sign in process. * Used to fill the "Sign in" link in the SignUp component. @@ -665,15 +657,9 @@ export type SignUpProps = { initialValues?: SignUpInitialValues; } & RedirectOptions; -export type UserProfileProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type SignUpModalProps = WithoutRouting; + +export type UserProfileProps = RoutingOptions & { /** * Customisation options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` @@ -691,15 +677,9 @@ export type UserProfileProps = { customPages?: CustomPage[]; }; -export type OrganizationProfileProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type UserProfileModalProps = WithoutRouting; + +export type OrganizationProfileProps = RoutingOptions & { /** * Full URL or path to navigate to after the user leaves the currently active organization. * @default undefined @@ -717,15 +697,9 @@ export type OrganizationProfileProps = { customPages?: CustomPage[]; }; -export type CreateOrganizationProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type OrganizationProfileModalProps = WithoutRouting; + +export type CreateOrganizationProps = RoutingOptions & { /** * Full URL or path to navigate after creating a new organization. * @default undefined @@ -747,7 +721,20 @@ export type CreateOrganizationProps = { appearance?: CreateOrganizationTheme; }; -export type UserButtonProps = { +export type CreateOrganizationModalProps = WithoutRouting; + +type UserProfileMode = 'modal' | 'navigation'; +type UserButtonProfileMode = + | { + userProfileUrl?: never; + userProfileMode?: Extract; + } + | { + userProfileUrl: string; + userProfileMode?: Extract; + }; + +export type UserButtonProps = UserButtonProfileMode & { /** * Controls if the username is displayed next to the trigger button */ @@ -765,11 +752,6 @@ export type UserButtonProps = { * This option applies to multi-session applications. */ afterMultiSessionSingleSignOutUrl?: string; - /** - * Full URL or path leading to the - * account management interface. - */ - userProfileUrl?: string; /** * Full URL or path to navigate on "Add another account" action. * Multi-session mode only. @@ -780,13 +762,6 @@ export type UserButtonProps = { * Multi-session mode only. */ afterSwitchSessionUrl?: string; - /** - * Controls whether clicking the "Manage your account" button will cause - * the UserProfile component to open as a modal, or if the browser will navigate - * to the `userProfileUrl` where UserProfile is mounted as a page. - * @default 'modal' - */ - userProfileMode?: 'modal' | 'navigation'; /** * Customisation options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` @@ -807,83 +782,74 @@ type PrimitiveKeys = { type LooseExtractedParams = Autocomplete<`:${T}`>; -export type OrganizationSwitcherProps = { - /** +type OrganizationProfileMode = + | { organizationProfileUrl: string; organizationProfileMode?: 'navigation' } + | { organizationProfileUrl?: never; organizationProfileMode?: 'modal' }; + +type CreateOrganizationMode = + | { createOrganizationUrl: string; createOrganizationMode?: 'navigation' } + | { createOrganizationUrl?: never; createOrganizationMode?: 'modal' }; + +export type OrganizationSwitcherProps = CreateOrganizationMode & + OrganizationProfileMode & { + /** Controls the default state of the OrganizationSwitcher */ - defaultOpen?: boolean; - /** - * By default, users can switch between organization and their personal account. - * This option controls whether OrganizationSwitcher will include the user's personal account - * in the organization list. Setting this to `false` will hide the personal account entry, - * and users will only be able to switch between organizations. - * @default true - */ - hidePersonal?: boolean; - /** - * Full URL or path to navigate after creating a new organization. - * @default undefined - */ - afterCreateOrganizationUrl?: - | ((organization: OrganizationResource) => string) - | LooseExtractedParams>; - /** - * Full URL or path to navigate after a successful organization selection. - * Accepts a function that returns URL or path - * @default undefined` - */ - afterSelectOrganizationUrl?: - | ((organization: OrganizationResource) => string) - | LooseExtractedParams>; - - /** - * Full URL or path to navigate after a successful selection of personal workspace. - * Accepts a function that returns URL or path - * @default undefined` - */ - afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; - /** - * Full URL or path to navigate to after the user leaves the currently active organization. - * @default undefined - */ - afterLeaveOrganizationUrl?: string; - /** - * Controls whether clicking the "Manage organization" button will cause - * the OrganizationProfile component to open as a modal, or if the browser will navigate - * to the `organizationProfileUrl` where OrganizationProfile is mounted as a page. - * @default modal - */ - organizationProfileMode?: 'modal' | 'navigation'; - /** - * Controls whether clicking the "Create organization" button will cause - * the CreateOrganization component to open as a modal, or if the browser will navigate - * to the `createOrganizationUrl` where CreateOrganization is mounted as a page. - * @default modal - */ - createOrganizationMode?: 'modal' | 'navigation'; - /** - * Full URL or path where the component is mounted. - * @default undefined - */ - organizationProfileUrl?: string; - /** - * Full URL or path where the component is mounted. - * @default undefined - */ - createOrganizationUrl?: string; - /** - * Customisation options to fully match the Clerk components to your own brand. - * These options serve as overrides and will be merged with the global `appearance` - * prop of ClerkProvided (if one is provided) - */ - appearance?: OrganizationSwitcherTheme; + defaultOpen?: boolean; + /** + * By default, users can switch between organization and their personal account. + * This option controls whether OrganizationSwitcher will include the user's personal account + * in the organization list. Setting this to `false` will hide the personal account entry, + * and users will only be able to switch between organizations. + * @default true + */ + hidePersonal?: boolean; + /** + * Full URL or path to navigate after a successful organization switch. + * @default undefined + * @deprecated use `afterSelectOrganizationUrl` or `afterSelectPersonalUrl` + */ + afterSwitchOrganizationUrl?: string; + /** + * Full URL or path to navigate after creating a new organization. + * @default undefined + */ + afterCreateOrganizationUrl?: + | ((organization: OrganizationResource) => string) + | LooseExtractedParams>; + /** + * Full URL or path to navigate after a successful organization selection. + * Accepts a function that returns URL or path + * @default undefined` + */ + afterSelectOrganizationUrl?: + | ((organization: OrganizationResource) => string) + | LooseExtractedParams>; + + /** + * Full URL or path to navigate after a successful selection of personal workspace. + * Accepts a function that returns URL or path + * @default undefined + */ + afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; + /** + * Full URL or path to navigate to after the user leaves the currently active organization. + * @default undefined + */ + afterLeaveOrganizationUrl?: string; + /** + * Customisation options to fully match the Clerk components to your own brand. + * These options serve as overrides and will be merged with the global `appearance` + * prop of ClerkProvided (if one is provided) + */ + appearance?: OrganizationSwitcherTheme; - /* - * Specify options for the underlying component. - * e.g. - */ - organizationProfileProps?: Pick; -}; + /* + * Specify options for the underlying component. + * e.g. + */ + organizationProfileProps?: Pick; + }; export type OrganizationListProps = { /** diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index aa6a23d4e29..8392899c7ee 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -548,6 +548,8 @@ export interface Resources { export type RoutingStrategy = 'path' | 'hash' | 'virtual'; +export type WithoutRouting = Omit; + export type SignInInitialValues = { emailAddress?: string; phoneNumber?: string; @@ -620,15 +622,11 @@ export type SetActiveParams = { export type SetActive = (params: SetActiveParams) => Promise; -export type SignInProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/sign in' - */ - path?: string; +export type RoutingOptions = + | { path: string | undefined; routing?: Extract } + | { path?: never; routing?: Extract }; + +export type SignInProps = RoutingOptions & { /** * Full URL or path to for the sign up process. * Used to fill the "Sign up" link in the SignUp component. @@ -646,15 +644,9 @@ export type SignInProps = { initialValues?: SignInInitialValues; } & RedirectOptions; -export type SignUpProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/sign up' - */ - path?: string; +export type SignInModalProps = WithoutRouting; + +export type SignUpProps = RoutingOptions & { /** * Full URL or path to for the sign in process. * Used to fill the "Sign in" link in the SignUp component. @@ -677,15 +669,9 @@ export type SignUpProps = { initialValues?: SignUpInitialValues; } & RedirectOptions; -export type UserProfileProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type SignUpModalProps = WithoutRouting; + +export type UserProfileProps = RoutingOptions & { /** * Customisation options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` @@ -703,15 +689,9 @@ export type UserProfileProps = { customPages?: CustomPage[]; }; -export type OrganizationProfileProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type UserProfileModalProps = WithoutRouting; + +export type OrganizationProfileProps = RoutingOptions & { /** * Full URL or path to navigate to after the user leaves the currently active organization. * @default undefined @@ -729,15 +709,9 @@ export type OrganizationProfileProps = { customPages?: CustomPage[]; }; -export type CreateOrganizationProps = { - /* - * Page routing strategy - */ - routing?: RoutingStrategy; - /* - * Root URL where the component is mounted on, eg: '/user' - */ - path?: string; +export type OrganizationProfileModalProps = WithoutRouting; + +export type CreateOrganizationProps = RoutingOptions & { /** * Full URL or path to navigate after creating a new organization. * @default undefined @@ -759,7 +733,20 @@ export type CreateOrganizationProps = { appearance?: CreateOrganizationTheme; }; -export type UserButtonProps = { +export type CreateOrganizationModalProps = WithoutRouting; + +type UserProfileMode = 'modal' | 'navigation'; +type UserButtonProfileMode = + | { + userProfileUrl?: never; + userProfileMode?: Extract; + } + | { + userProfileUrl: string; + userProfileMode?: Extract; + }; + +export type UserButtonProps = UserButtonProfileMode & { /** * Controls if the username is displayed next to the trigger button */ @@ -777,11 +764,6 @@ export type UserButtonProps = { * This option applies to multi-session applications. */ afterMultiSessionSingleSignOutUrl?: string; - /** - * Full URL or path leading to the - * account management interface. - */ - userProfileUrl?: string; /** * Full URL or path to navigate on "Add another account" action. * Multi-session mode only. @@ -792,13 +774,6 @@ export type UserButtonProps = { * Multi-session mode only. */ afterSwitchSessionUrl?: string; - /** - * Controls whether clicking the "Manage your account" button will cause - * the UserProfile component to open as a modal, or if the browser will navigate - * to the `userProfileUrl` where UserProfile is mounted as a page. - * @default 'modal' - */ - userProfileMode?: 'modal' | 'navigation'; /** * Customisation options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` @@ -819,83 +794,74 @@ type PrimitiveKeys = { type LooseExtractedParams = Autocomplete<`:${T}`>; -export type OrganizationSwitcherProps = { - /** +type OrganizationProfileMode = + | { organizationProfileUrl: string; organizationProfileMode?: 'navigation' } + | { organizationProfileUrl?: never; organizationProfileMode?: 'modal' }; + +type CreateOrganizationMode = + | { createOrganizationUrl: string; createOrganizationMode?: 'navigation' } + | { createOrganizationUrl?: never; createOrganizationMode?: 'modal' }; + +export type OrganizationSwitcherProps = CreateOrganizationMode & + OrganizationProfileMode & { + /** Controls the default state of the OrganizationSwitcher */ - defaultOpen?: boolean; - /** - * By default, users can switch between organization and their personal account. - * This option controls whether OrganizationSwitcher will include the user's personal account - * in the organization list. Setting this to `false` will hide the personal account entry, - * and users will only be able to switch between organizations. - * @default true - */ - hidePersonal?: boolean; - /** - * Full URL or path to navigate after creating a new organization. - * @default undefined - */ - afterCreateOrganizationUrl?: - | ((organization: OrganizationResource) => string) - | LooseExtractedParams>; - /** - * Full URL or path to navigate after a successful organization selection. - * Accepts a function that returns URL or path - * @default undefined` - */ - afterSelectOrganizationUrl?: - | ((organization: OrganizationResource) => string) - | LooseExtractedParams>; - - /** - * Full URL or path to navigate after a successful selection of personal workspace. - * Accepts a function that returns URL or path - * @default undefined` - */ - afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; - /** - * Full URL or path to navigate to after the user leaves the currently active organization. - * @default undefined - */ - afterLeaveOrganizationUrl?: string; - /** - * Controls whether clicking the "Manage organization" button will cause - * the OrganizationProfile component to open as a modal, or if the browser will navigate - * to the `organizationProfileUrl` where OrganizationProfile is mounted as a page. - * @default modal - */ - organizationProfileMode?: 'modal' | 'navigation'; - /** - * Controls whether clicking the "Create organization" button will cause - * the CreateOrganization component to open as a modal, or if the browser will navigate - * to the `createOrganizationUrl` where CreateOrganization is mounted as a page. - * @default modal - */ - createOrganizationMode?: 'modal' | 'navigation'; - /** - * Full URL or path where the component is mounted. - * @default undefined - */ - organizationProfileUrl?: string; - /** - * Full URL or path where the component is mounted. - * @default undefined - */ - createOrganizationUrl?: string; - /** - * Customisation options to fully match the Clerk components to your own brand. - * These options serve as overrides and will be merged with the global `appearance` - * prop of ClerkProvided (if one is provided) - */ - appearance?: OrganizationSwitcherTheme; + defaultOpen?: boolean; + /** + * By default, users can switch between organization and their personal account. + * This option controls whether OrganizationSwitcher will include the user's personal account + * in the organization list. Setting this to `false` will hide the personal account entry, + * and users will only be able to switch between organizations. + * @default true + */ + hidePersonal?: boolean; + /** + * Full URL or path to navigate after a successful organization switch. + * @default undefined + * @deprecated use `afterSelectOrganizationUrl` or `afterSelectPersonalUrl` + */ + afterSwitchOrganizationUrl?: string; + /** + * Full URL or path to navigate after creating a new organization. + * @default undefined + */ + afterCreateOrganizationUrl?: + | ((organization: OrganizationResource) => string) + | LooseExtractedParams>; + /** + * Full URL or path to navigate after a successful organization selection. + * Accepts a function that returns URL or path + * @default undefined` + */ + afterSelectOrganizationUrl?: + | ((organization: OrganizationResource) => string) + | LooseExtractedParams>; + + /** + * Full URL or path to navigate after a successful selection of personal workspace. + * Accepts a function that returns URL or path + * @default undefined + */ + afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; + /** + * Full URL or path to navigate to after the user leaves the currently active organization. + * @default undefined + */ + afterLeaveOrganizationUrl?: string; + /** + * Customisation options to fully match the Clerk components to your own brand. + * These options serve as overrides and will be merged with the global `appearance` + * prop of ClerkProvided (if one is provided) + */ + appearance?: OrganizationSwitcherTheme; - /* - * Specify options for the underlying component. - * e.g. - */ - organizationProfileProps?: Pick; -}; + /* + * Specify options for the underlying component. + * e.g. + */ + organizationProfileProps?: Pick; + }; export type OrganizationListProps = { /** diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index f2e344ecf69..6f79f84cac0 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -89,3 +89,10 @@ export type Serializable = { * or type of `T` */ export type Autocomplete = U | (T & Record); + +/** + * Omit without union flattening + * */ +export type Without = { + [P in keyof T as Exclude]: T[P]; +}; diff --git a/playground/nextjs/pages/create-organization/[[...index]].tsx b/playground/nextjs/pages/create-organization/[[...index]].tsx index 1b2516bc39e..774947eba0c 100644 --- a/playground/nextjs/pages/create-organization/[[...index]].tsx +++ b/playground/nextjs/pages/create-organization/[[...index]].tsx @@ -1,6 +1,5 @@ import { CreateOrganization } from '@clerk/nextjs'; import type { NextPage } from 'next'; -import React from 'react'; const CreateOrganizationPage: NextPage = () => { return ( diff --git a/playground/nextjs/pages/sign-in/[[...index]].tsx b/playground/nextjs/pages/sign-in/[[...index]].tsx index 985999a57c5..f95e33f06d6 100644 --- a/playground/nextjs/pages/sign-in/[[...index]].tsx +++ b/playground/nextjs/pages/sign-in/[[...index]].tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { SignIn } from '@clerk/nextjs'; import type { NextPage } from 'next';