Skip to content

Commit

Permalink
feat(clerk-js,clerk-react,types): Introduce Custom Pages in UserProfile
Browse files Browse the repository at this point in the history
  • Loading branch information
anagstef committed Oct 3, 2023
1 parent c7c6912 commit 50b2b9c
Show file tree
Hide file tree
Showing 20 changed files with 976 additions and 124 deletions.
7 changes: 7 additions & 0 deletions .changeset/proud-ways-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

Introduce Custom Pages in UserProfile
Original file line number Diff line number Diff line change
@@ -1,51 +1,25 @@
import React from 'react';

import type { NavbarRoute } from '../../elements';
import { useUserProfileContext } from '../../contexts';
import { Breadcrumbs, NavBar, NavbarContextProvider } from '../../elements';
import { TickShield, User } from '../../icons';
import { localizationKeys } from '../../localization';
import type { PropsOfComponent } from '../../styledSystem';

const userProfileRoutes: NavbarRoute[] = [
{
name: localizationKeys('userProfile.start.headerTitle__account'),
id: 'account',
icon: User,
path: '/',
},
{
name: localizationKeys('userProfile.start.headerTitle__security'),
id: 'security',
icon: TickShield,
path: '',
},
];
import { pageToRootNavbarRouteMap } from '../../utils';

export const UserProfileNavbar = (
props: React.PropsWithChildren<Pick<PropsOfComponent<typeof NavBar>, 'contentRef'>>,
) => {
const { pages } = useUserProfileContext();
return (
<NavbarContextProvider>
<NavBar
routes={userProfileRoutes}
routes={pages.routes}
contentRef={props.contentRef}
/>
{props.children}
</NavbarContextProvider>
);
};

const pageToRootNavbarRouteMap = {
profile: userProfileRoutes.find(r => r.id === 'account'),
'email-address': userProfileRoutes.find(r => r.id === 'account'),
'phone-number': userProfileRoutes.find(r => r.id === 'account'),
'connected-account': userProfileRoutes.find(r => r.id === 'account'),
'web3-wallet': userProfileRoutes.find(r => r.id === 'account'),
username: userProfileRoutes.find(r => r.id === 'account'),
'multi-factor': userProfileRoutes.find(r => r.id === 'security'),
password: userProfileRoutes.find(r => r.id === 'security'),
};

export const UserProfileBreadcrumbs = (props: Pick<PropsOfComponent<typeof Breadcrumbs>, 'title'>) => {
return (
<Breadcrumbs
Expand Down
186 changes: 116 additions & 70 deletions packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useUserProfileContext } from '../../contexts';
import { ProfileCardContent } from '../../elements';
import { Route, Switch } from '../../router';
import type { PropsOfComponent } from '../../styledSystem';
import { ExternalElementMounter } from '../../utils';
import { ConnectedAccountsPage } from './ConnectedAccountsPage';
import { DeletePage } from './DeletePage';
import { EmailPage } from './EmailPage';
Expand All @@ -22,90 +24,134 @@ import { UsernamePage } from './UsernamePage';
import { Web3Page } from './Web3Page';

export const UserProfileRoutes = (props: PropsOfComponent<typeof ProfileCardContent>) => {
const { pages } = useUserProfileContext();
return (
<ProfileCardContent contentRef={props.contentRef}>
<Route index>
<RootPage />
</Route>
<Route path='profile'>
<ProfilePage />
</Route>
<Route path='email-address'>
<Switch>
<Route path=':id/remove'>
<RemoveEmailPage />
<Switch>
{/* Custom Pages */}
{pages.contents?.map((customPage, index) => (
<Route
index={!pages.isAccountPageRoot && index === 0}
path={!pages.isAccountPageRoot && index === 0 ? undefined : customPage.url}
key={`custom-page-${index}`}
>
<ExternalElementMounter
mount={customPage.mount}
unmount={customPage.unmount}
/>
</Route>
<Route path=':id'>
<EmailPage />
))}
<Route path={pages.isAccountPageRoot ? undefined : 'account'}>
<Route
path='profile'
flowStart
>
<ProfilePage />
</Route>
<Route index>
<EmailPage />
</Route>
</Switch>
</Route>
<Route path='phone-number'>
<Switch>
<Route path=':id/remove'>
<RemovePhonePage />
</Route>
<Route path=':id'>
<PhonePage />
</Route>
<Route index>
<PhonePage />
</Route>
</Switch>
</Route>
<Route path='multi-factor'>
<Switch>
<Route path='totp/remove'>
<RemoveMfaTOTPPage />
<Route
path='email-address'
flowStart
>
<Switch>
<Route path=':id/remove'>
<RemoveEmailPage />
</Route>
<Route path=':id'>
<EmailPage />
</Route>
<Route index>
<EmailPage />
</Route>
</Switch>
</Route>
<Route path='backup_code/add'>
<MfaBackupCodeCreatePage />
<Route
path='phone-number'
flowStart
>
<Switch>
<Route path=':id/remove'>
<RemovePhonePage />
</Route>
<Route path=':id'>
<PhonePage />
</Route>
<Route index>
<PhonePage />
</Route>
</Switch>
</Route>
<Route path=':id/remove'>
<RemoveMfaPhoneCodePage />
<Route
path='multi-factor'
flowStart
>
<Switch>
<Route path='totp/remove'>
<RemoveMfaTOTPPage />
</Route>
<Route path='backup_code/add'>
<MfaBackupCodeCreatePage />
</Route>
<Route path=':id/remove'>
<RemoveMfaPhoneCodePage />
</Route>
<Route path=':id'>
<MfaPage />
</Route>
<Route index>
<MfaPage />
</Route>
</Switch>
</Route>
<Route path=':id'>
<MfaPage />
<Route
path='connected-account'
flowStart
>
<Switch>
<Route path=':id/remove'>
<RemoveConnectedAccountPage />
</Route>
<Route index>
<ConnectedAccountsPage />
</Route>
</Switch>
</Route>
<Route index>
<MfaPage />
<Route
path='web3-wallet'
flowStart
>
<Switch>
<Route path=':id/remove'>
<RemoveWeb3WalletPage />
</Route>
<Route index>
<Web3Page />
</Route>
</Switch>
</Route>
</Switch>
</Route>
<Route path='connected-account'>
<Switch>
<Route path=':id/remove'>
<RemoveConnectedAccountPage />
<Route
path='username'
flowStart
>
<UsernamePage />
</Route>
<Route index>
<ConnectedAccountsPage />
{/*<Route path='security'>*/}
<Route
path='password'
flowStart
>
<PasswordPage />
</Route>
</Switch>
</Route>
<Route path='web3-wallet'>
<Switch>
<Route path=':id/remove'>
<RemoveWeb3WalletPage />
<Route
path='delete'
flowStart
>
<DeletePage />
</Route>
<Route index>
<Web3Page />
<RootPage />
</Route>
</Switch>
</Route>
<Route path='username'>
<UsernamePage />
</Route>
{/*<Route path='security'>*/}
<Route path='password'>
<PasswordPage />
</Route>
{/*</Route>*/}
<Route path='delete'>
<DeletePage />
</Route>
</Route>
</Switch>
</ProfileCardContent>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { describe, it } from '@jest/globals';
import React from 'react';

import { bindCreateFixtures, render, screen } from '../../../../testUtils';
import type { CustomPage } from '../../../utils';
import { UserProfile } from '../UserProfile';

const { createFixtures } = bindCreateFixtures('SignIn');
const { createFixtures } = bindCreateFixtures('UserProfile');

describe('UserProfile', () => {
describe('Navigation', () => {
Expand All @@ -19,5 +20,39 @@ describe('UserProfile', () => {
const securityElements = screen.getAllByText(/Security/i);
expect(securityElements.some(el => el.tagName.toUpperCase() === 'BUTTON')).toBe(true);
});

it('includes custom nav items', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
});

const customPages: CustomPage[] = [
{
label: 'Custom1',
url: 'custom1',
mount: () => undefined,
unmount: () => undefined,
mountIcon: () => undefined,
unmountIcon: () => undefined,
},
{
label: 'ExternalLink',
url: '/link',
mountIcon: () => undefined,
unmountIcon: () => undefined,
},
];

props.setProps({ customPages });
render(<UserProfile />, { wrapper });
const accountElements = screen.getAllByText(/Account/i);
expect(accountElements.some(el => el.tagName.toUpperCase() === 'BUTTON')).toBe(true);
const securityElements = screen.getAllByText(/Security/i);
expect(securityElements.some(el => el.tagName.toUpperCase() === 'BUTTON')).toBe(true);
const customElements = screen.getAllByText(/Custom1/i);
expect(customElements.some(el => el.tagName.toUpperCase() === 'BUTTON')).toBe(true);
const externalElements = screen.getAllByText(/ExternalLink/i);
expect(externalElements.some(el => el.tagName.toUpperCase() === 'BUTTON')).toBe(true);
});
});
});
18 changes: 14 additions & 4 deletions packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, { useMemo } from 'react';
import { SIGN_IN_INITIAL_VALUE_KEYS, SIGN_UP_INITIAL_VALUE_KEYS } from '../../core/constants';
import { buildAuthQueryString, buildURL, createDynamicParamParser, pickRedirectionProp } from '../../utils';
import { useCoreClerk, useEnvironment, useOptions } from '../contexts';
import type { NavbarRoute } from '../elements';
import type { ParsedQs } from '../router';
import { useRouter } from '../router';
import type {
Expand All @@ -18,6 +19,8 @@ import type {
UserButtonCtx,
UserProfileCtx,
} from '../types';
import type { CustomPageContent } from '../utils';
import { createCustomPages } from '../utils';

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

Expand Down Expand Up @@ -184,24 +187,31 @@ export const useSignInContext = (): SignInContextType => {
};
};

type PagesType = {
routes: NavbarRoute[];
contents: CustomPageContent[];
isAccountPageRoot: boolean;
};

export type UserProfileContextType = UserProfileCtx & {
queryParams: ParsedQs;
authQueryString: string | null;
pages: PagesType;
};

// UserProfile does not accept any props except for
// `routing` and `path`
// TODO: remove if not needed during the components v2 overhaul
export const useUserProfileContext = (): UserProfileContextType => {
const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as UserProfileCtx;
const { componentName, customPages, ...ctx } = (React.useContext(ComponentContext) || {}) as UserProfileCtx;
const { queryParams } = useRouter();

if (componentName !== 'UserProfile') {
throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.');
}

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

return {
...ctx,
pages,
componentName,
queryParams,
authQueryString: '',
Expand Down
Loading

0 comments on commit 50b2b9c

Please sign in to comment.