@@ -18,7 +33,11 @@ export default function SignInPage() {
Welcome back, we are lucky to have you.
-
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
index c6d49f891..dbbbcdba9 100644
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link';
import { redirect } from 'next/navigation';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
+import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { SignUpForm } from '~/components/forms/signup';
@@ -10,11 +11,24 @@ export const metadata: Metadata = {
title: 'Sign Up',
};
-export default function SignUpPage() {
+type SignUpPageProps = {
+ searchParams: {
+ email?: string;
+ };
+};
+
+export default function SignUpPage({ searchParams }: SignUpPageProps) {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
redirect('/signin');
}
+ const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
+ const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
+
+ if (!email && rawEmail) {
+ redirect('/signup');
+ }
+
return (
Create a new account
@@ -24,7 +38,11 @@ export default function SignUpPage() {
signing is within your grasp.
-
+
Already have an account?{' '}
diff --git a/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
new file mode 100644
index 000000000..634416fe3
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
@@ -0,0 +1,121 @@
+import Link from 'next/link';
+
+import { DateTime } from 'luxon';
+
+import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
+import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
+import { getTeamById } from '@documenso/lib/server-only/team/get-team';
+import { prisma } from '@documenso/prisma';
+import { TeamMemberInviteStatus } from '@documenso/prisma/client';
+import { Button } from '@documenso/ui/primitives/button';
+
+type AcceptInvitationPageProps = {
+ params: {
+ token: string;
+ };
+};
+
+export default async function AcceptInvitationPage({
+ params: { token },
+}: AcceptInvitationPageProps) {
+ const session = await getServerComponentSession();
+
+ const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
+ where: {
+ token,
+ },
+ });
+
+ if (!teamMemberInvite) {
+ return (
+
+
Invalid token
+
+
+ This token is invalid or has expired. Please contact your team for a new invitation.
+
+
+
+
+ );
+ }
+
+ const team = await getTeamById({ teamId: teamMemberInvite.teamId });
+
+ const user = await prisma.user.findFirst({
+ where: {
+ email: {
+ equals: teamMemberInvite.email,
+ mode: 'insensitive',
+ },
+ },
+ });
+
+ // Directly convert the team member invite to a team member if they already have an account.
+ if (user) {
+ await acceptTeamInvitation({ userId: user.id, teamId: team.id });
+ }
+
+ // For users who do not exist yet, set the team invite status to accepted, which is checked during
+ // user creation to determine if we should add the user to the team at that time.
+ if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.ACCEPTED) {
+ await prisma.teamMemberInvite.update({
+ where: {
+ id: teamMemberInvite.id,
+ },
+ data: {
+ status: TeamMemberInviteStatus.ACCEPTED,
+ },
+ });
+ }
+
+ const email = encryptSecondaryData({
+ data: teamMemberInvite.email,
+ expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
+ });
+
+ if (!user) {
+ return (
+
+
Team invitation
+
+
+ You have been invited by {team.name} to join their team.
+
+
+
+ To accept this invitation you must create an account.
+
+
+
+
+ );
+ }
+
+ const isSessionUserTheInvitedUser = user.id === session.user?.id;
+
+ return (
+
+
Invitation accepted!
+
+
+ You have accepted an invitation from {team.name} to join their team.
+
+
+ {isSessionUserTheInvitedUser ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
new file mode 100644
index 000000000..53ad4461b
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
@@ -0,0 +1,89 @@
+import Link from 'next/link';
+
+import { isTokenExpired } from '@documenso/lib/utils/token-verification';
+import { prisma } from '@documenso/prisma';
+import { Button } from '@documenso/ui/primitives/button';
+
+type VerifyTeamEmailPageProps = {
+ params: {
+ token: string;
+ };
+};
+
+export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
+ const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
+ where: {
+ token,
+ },
+ include: {
+ team: true,
+ },
+ });
+
+ if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
+ return (
+
+
Invalid link
+
+
+ This link is invalid or has expired. Please contact your team to resend a verification.
+
+
+
+
+ );
+ }
+
+ const { team } = teamEmailVerification;
+
+ let isTeamEmailVerificationError = false;
+
+ try {
+ await prisma.$transaction([
+ prisma.teamEmailVerification.deleteMany({
+ where: {
+ teamId: team.id,
+ },
+ }),
+ prisma.teamEmail.create({
+ data: {
+ teamId: team.id,
+ email: teamEmailVerification.email,
+ name: teamEmailVerification.name,
+ },
+ }),
+ ]);
+ } catch (e) {
+ console.error(e);
+ isTeamEmailVerificationError = true;
+ }
+
+ if (isTeamEmailVerificationError) {
+ return (
+
+
Team email verification
+
+
+ Something went wrong while attempting to verify your email address for{' '}
+ {team.name}. Please try again later.
+
+
+ );
+ }
+
+ return (
+
+
Team email verified!
+
+
+ You have verified your email address for {team.name}.
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
new file mode 100644
index 000000000..819b7e970
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
@@ -0,0 +1,80 @@
+import Link from 'next/link';
+
+import { transferTeamOwnership } from '@documenso/lib/server-only/team/transfer-team-ownership';
+import { isTokenExpired } from '@documenso/lib/utils/token-verification';
+import { prisma } from '@documenso/prisma';
+import { Button } from '@documenso/ui/primitives/button';
+
+type VerifyTeamTransferPage = {
+ params: {
+ token: string;
+ };
+};
+
+export default async function VerifyTeamTransferPage({
+ params: { token },
+}: VerifyTeamTransferPage) {
+ const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
+ where: {
+ token,
+ },
+ include: {
+ team: true,
+ },
+ });
+
+ if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
+ return (
+
+
Invalid link
+
+
+ This link is invalid or has expired. Please contact your team to resend a transfer
+ request.
+
+
+
+
+ );
+ }
+
+ const { team } = teamTransferVerification;
+
+ let isTransferError = false;
+
+ try {
+ await transferTeamOwnership({ token });
+ } catch (e) {
+ console.error(e);
+ isTransferError = true;
+ }
+
+ if (isTransferError) {
+ return (
+
+
Team ownership transfer
+
+
+ Something went wrong while attempting to transfer the ownership of team{' '}
+ {team.name} to your. Please try again later or contact support.
+
+
+ );
+ }
+
+ return (
+
+
Team ownership transferred!
+
+
+ The ownership of team {team.name} has been successfully transferred to you.
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx
index 0312a96d2..3fe42a4c4 100644
--- a/apps/web/src/components/(dashboard)/common/command-menu.tsx
+++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx
@@ -197,20 +197,22 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
)}
{!currentPage && (
<>
-
+
-
+
-
+
-
- addPage('theme')}>Change theme
+
+ addPage('theme')}>
+ Change theme
+
{searchResults.length > 0 && (
-
+
)}
@@ -231,6 +233,7 @@ const Commands = ({
}) => {
return pages.map((page, idx) => (
push(page.path)}
@@ -255,7 +258,7 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
setTheme(theme.theme)}
- className="mx-2 first:mt-2 last:mb-2"
+ className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
>
{theme.label}
diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
index e04bc2818..2b11c4be2 100644
--- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
@@ -4,10 +4,11 @@ import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
-import { usePathname } from 'next/navigation';
+import { useParams, usePathname } from 'next/navigation';
import { Search } from 'lucide-react';
+import { getRootHref } from '@documenso/lib/utils/params';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -28,10 +29,13 @@ export type DesktopNavProps = HTMLAttributes;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const pathname = usePathname();
+ const params = useParams();
const [open, setOpen] = useState(false);
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
+ const rootHref = getRootHref(params, { returnEmptyRootString: true });
+
useEffect(() => {
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown';
const isMacOS = /Macintosh|Mac\s+OS\s+X/i.test(userAgent);
@@ -48,20 +52,24 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
{...props}
>
- {navigationLinks.map(({ href, label }) => (
-
- {label}
-
- ))}
+ {navigationLinks
+ .filter(({ href }) => href !== '/templates' || rootHref === '') // Remove templates for team pages.
+ .map(({ href, label }) => (
+
+ {label}
+
+ ))}
diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx
index ba35671e6..753f5fb11 100644
--- a/apps/web/src/components/(dashboard)/layout/header.tsx
+++ b/apps/web/src/components/(dashboard)/layout/header.tsx
@@ -1,23 +1,34 @@
'use client';
-import type { HTMLAttributes } from 'react';
-import { useEffect, useState } from 'react';
+import { type HTMLAttributes, useEffect, useState } from 'react';
import Link from 'next/link';
+import { useParams } from 'next/navigation';
+import { MenuIcon, SearchIcon } from 'lucide-react';
+
+import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
+import { getRootHref } from '@documenso/lib/utils/params';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Logo } from '~/components/branding/logo';
+import { CommandMenu } from '../common/command-menu';
import { DesktopNav } from './desktop-nav';
-import { ProfileDropdown } from './profile-dropdown';
+import { MenuSwitcher } from './menu-switcher';
+import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes & {
user: User;
+ teams: GetTeamsResponse;
};
-export const Header = ({ className, user, ...props }: HeaderProps) => {
+export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
+ const params = useParams();
+
+ const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
+ const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
@@ -41,8 +52,8 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
>
@@ -50,11 +61,24 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
- {/*
*/}
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
new file mode 100644
index 000000000..35a05baf2
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
@@ -0,0 +1,214 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
+import { signOut } from 'next-auth/react';
+
+import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
+import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
+import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
+import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
+import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
+import type { User } from '@documenso/prisma/client';
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { AvatarWithText } from '@documenso/ui/primitives/avatar';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@documenso/ui/primitives/dropdown-menu';
+
+export type MenuSwitcherProps = {
+ user: User;
+ teams: GetTeamsResponse;
+};
+
+export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
+ const pathname = usePathname();
+
+ const isUserAdmin = isAdmin(user);
+
+ const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
+ initialData: initialTeamsData,
+ });
+
+ const teams = teamsQueryResult && teamsQueryResult.length > 0 ? teamsQueryResult : null;
+
+ const isPathTeamUrl = (teamUrl: string) => {
+ if (!pathname || !pathname.startsWith(`/t/`)) {
+ return false;
+ }
+
+ return pathname.split('/')[2] === teamUrl;
+ };
+
+ const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
+
+ const formatAvatarFallback = (teamName?: string) => {
+ if (teamName !== undefined) {
+ return teamName.slice(0, 1).toUpperCase();
+ }
+
+ return user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase();
+ };
+
+ const formatSecondaryAvatarText = (team?: typeof selectedTeam) => {
+ if (!team) {
+ return 'Personal Account';
+ }
+
+ if (team.ownerUserId === user.id) {
+ return 'Owner';
+ }
+
+ return TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role];
+ };
+
+ return (
+
+
+
+
+
+
+ {teams ? (
+ <>
+ Personal
+
+
+
+
+ )
+ }
+ />
+
+
+
+
+
+
+
+
Teams
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {teams.map((team) => (
+
+
+
+ )
+ }
+ />
+
+
+ ))}
+ >
+ ) : (
+
+
+ Create team
+
+
+
+ )}
+
+
+
+ {isUserAdmin && (
+
+ Admin panel
+
+ )}
+
+
+ User settings
+
+
+ {selectedTeam &&
+ canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
+
+ Team settings
+
+ )}
+
+
+ signOut({
+ callbackUrl: '/',
+ })
+ }
+ >
+ Sign Out
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/layout/mobile-nav.tsx b/apps/web/src/components/(dashboard)/layout/mobile-nav.tsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx b/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx
new file mode 100644
index 000000000..7142de5dc
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+
+import { signOut } from 'next-auth/react';
+
+import LogoImage from '@documenso/assets/logo.png';
+import { getRootHref } from '@documenso/lib/utils/params';
+import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
+import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
+
+export type MobileNavigationProps = {
+ isMenuOpen: boolean;
+ onMenuOpenChange?: (_value: boolean) => void;
+};
+
+export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
+ const params = useParams();
+
+ const handleMenuItemClick = () => {
+ onMenuOpenChange?.(false);
+ };
+
+ const rootHref = getRootHref(params, { returnEmptyRootString: true });
+
+ const menuNavigationLinks = [
+ {
+ href: `${rootHref}/documents`,
+ text: 'Documents',
+ },
+ {
+ href: `${rootHref}/templates`,
+ text: 'Templates',
+ },
+ {
+ href: '/settings/teams',
+ text: 'Teams',
+ },
+ {
+ href: '/settings/profile',
+ text: 'Settings',
+ },
+ ].filter(({ text, href }) => text !== 'Templates' || href === '/templates'); // Filter out templates for teams.
+
+ return (
+
+
+
+
+
+
+
+ {menuNavigationLinks.map(({ href, text }) => (
+ handleMenuItemClick()}
+ >
+ {text}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ © {new Date().getFullYear()} Documenso, Inc. All rights reserved.
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
deleted file mode 100644
index f2432c071..000000000
--- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import {
- CreditCard,
- FileSpreadsheet,
- Lock,
- LogOut,
- User as LucideUser,
- Monitor,
- Moon,
- Palette,
- Sun,
- UserCog,
-} from 'lucide-react';
-import { signOut } from 'next-auth/react';
-import { useTheme } from 'next-themes';
-import { LuGithub } from 'react-icons/lu';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { User } from '@documenso/prisma/client';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuPortal,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-
-export type ProfileDropdownProps = {
- user: User;
-};
-
-export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
- const { getFlag } = useFeatureFlags();
- const { theme, setTheme } = useTheme();
- const isUserAdmin = isAdmin(user);
-
- const isBillingEnabled = getFlag('app_billing');
-
- const avatarFallback = user.name
- ? recipientInitials(user.name)
- : user.email.slice(0, 1).toUpperCase();
-
- return (
-
-
-
-
-
-
- Account
-
- {isUserAdmin && (
- <>
-
-
-
- Admin
-
-
-
-
- >
- )}
-
-
-
-
- Profile
-
-
-
-
-
-
- Security
-
-
-
- {isBillingEnabled && (
-
-
-
- Billing
-
-
- )}
-
-
-
-
-
- Templates
-
-
-
-
-
-
-
- Themes
-
-
-
-
-
- Light
-
-
-
- Dark
-
-
-
- System
-
-
-
-
-
-
-
-
-
- Star on Github
-
-
-
-
-
-
- void signOut({
- callbackUrl: '/',
- })
- }
- >
-
- Sign Out
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx b/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
index caeb780d0..a49e2f284 100644
--- a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
+++ b/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
@@ -21,9 +21,9 @@ export const PeriodSelector = () => {
const router = useRouter();
const period = useMemo(() => {
- const p = searchParams?.get('period') ?? '';
+ const p = searchParams?.get('period') ?? 'all';
- return isPeriodSelectorValue(p) ? p : '';
+ return isPeriodSelectorValue(p) ? p : 'all';
}, [searchParams]);
const onPeriodChange = (newPeriod: string) => {
@@ -35,7 +35,7 @@ export const PeriodSelector = () => {
params.set('period', newPeriod);
- if (newPeriod === '') {
+ if (newPeriod === '' || newPeriod === 'all') {
params.delete('period');
}
@@ -49,7 +49,7 @@ export const PeriodSelector = () => {
- All Time
+ All Time
Last 7 days
Last 14 days
Last 30 days
diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
index f4b2aae5e..c7ab61d8a 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
@@ -1,11 +1,11 @@
'use client';
-import { HTMLAttributes } from 'react';
+import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { CreditCard, Lock, User } from 'lucide-react';
+import { CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@@ -35,6 +35,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+
+
+
+
+
+
+
+