feat: migrate nextjs to rr7

This commit is contained in:
David Nguyen
2025-01-02 15:33:37 +11:00
parent 9183f668d3
commit 383b5f78f0
898 changed files with 31175 additions and 24615 deletions

View File

@ -5,6 +5,7 @@ module.exports = {
rules: { rules: {
'@next/next/no-img-element': 'off', '@next/next/no-img-element': 'off',
'no-unreachable': 'error', 'no-unreachable': 'error',
'react-hooks/exhaustive-deps': 'off',
}, },
settings: { settings: {
next: { next: {

28
apps/remix/.bin/build.sh Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Exit on error.
set -eo pipefail
cd "$(dirname "$0")/.."
start_time=$(date +%s)
echo "[Build]: Extracting and compiling translations"
npm run translate --prefix ../../
echo "[Build]: Building app"
npm run build:app
echo "[Build]: Building server"
npm run build:server
# Copy over the entry point for the server.
cp server/main.js build/server/main.js
# Copy over all web.js translations
cp -r ../../packages/lib/translations build/server/hono/packages/lib/translations
# Time taken
end_time=$(date +%s)
echo "[Build]: Done in $((end_time - start_time)) seconds"

4
apps/remix/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

9
apps/remix/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/node_modules/
# React Router
/.react-router/
/build/
# Vite
vite.config.*.timestamp*

22
apps/remix/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci
FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev
FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build
FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]

25
apps/remix/Dockerfile.bun Normal file
View File

@ -0,0 +1,25 @@
FROM oven/bun:1 AS dependencies-env
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --production
FROM dependencies-env AS build-env
COPY ./package.json bun.lockb /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN bun run build
FROM dependencies-env
COPY ./package.json bun.lockb /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["bun", "run", "start"]

View File

@ -0,0 +1,26 @@
FROM node:20-alpine AS dependencies-env
RUN npm i -g pnpm
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --prod --frozen-lockfile
FROM dependencies-env AS build-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN pnpm build
FROM dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["pnpm", "start"]

100
apps/remix/README.md Normal file
View File

@ -0,0 +1,100 @@
# Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
## Features
- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)
## Getting Started
### Installation
Install the dependencies:
```bash
npm install
```
### Development
Start the development server with HMR:
```bash
npm run dev
```
Your application will be available at `http://localhost:5173`.
## Building for Production
Create a production build:
```bash
npm run build
```
## Deployment
### Docker Deployment
This template includes three Dockerfiles optimized for different package managers:
- `Dockerfile` - for npm
- `Dockerfile.pnpm` - for pnpm
- `Dockerfile.bun` - for bun
To build and run using Docker:
```bash
# For npm
docker build -t my-app .
# For pnpm
docker build -f Dockerfile.pnpm -t my-app .
# For bun
docker build -f Dockerfile.bun -t my-app .
# Run the container
docker run -p 3000:3000 my-app
```
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### DIY Deployment
If you're familiar with deploying Node applications, the built-in app server is production-ready.
Make sure to deploy the output of `npm run build`
```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```
## Styling
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
---
Built with ❤️ using React Router.

24
apps/remix/app/app.css Normal file
View File

@ -0,0 +1,24 @@
@import '@documenso/ui/styles/theme.css';
@font-face {
font-family: 'Inter';
src: url('/public/fonts/inter-regular.ttf') format('ttf');
/* font-weight: 400;
font-style: normal;
font-display: swap; */
}
@font-face {
font-family: 'Caveat';
src: url('/public/fonts/caveat.ttf') format('ttf');
/* font-weight: 400;
font-style: normal;
font-display: swap; */
}
@layer base {
:root {
--font-sans: 'Inter';
--font-signature: 'Caveat';
}
}

View File

@ -1,12 +1,11 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import type { User } from '@documenso/prisma/client'; import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -23,12 +22,13 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteAccountDialogProps = { export type AccountDeleteDialogProps = {
className?: string; className?: string;
user: User;
}; };
export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => { export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
const { user } = useSession();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -49,7 +49,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
duration: 5000, duration: 5000,
}); });
return await signOut({ callbackUrl: '/' }); return await authClient.signOut();
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
@ -118,7 +118,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
</DialogHeader> </DialogHeader>
{!hasTwoFactorAuthentication && ( {!hasTwoFactorAuthentication && (
<div className="mt-4"> <div>
<Label> <Label>
<Trans> <Trans>
Please type{' '} Please type{' '}

View File

@ -1,13 +1,11 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Document } from '@prisma/client';
import { useNavigate } from 'react-router';
import type { Document } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -23,15 +21,15 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type SuperDeleteDocumentDialogProps = { export type AdminDocumentDeleteDialogProps = {
document: Document; document: Document;
}; };
export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => { export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const navigate = useNavigate();
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
@ -52,7 +50,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
duration: 5000, duration: 5000,
}); });
router.push('/admin/documents'); await navigate('/admin/documents');
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -1,15 +1,13 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { User } from '@prisma/client';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -25,17 +23,15 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteUserDialogProps = { export type AdminUserDeleteDialogProps = {
className?: string; className?: string;
user: User; user: User;
}; };
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => { export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate();
const router = useRouter();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isPending: isDeletingUser } = const { mutateAsync: deleteUser, isPending: isDeletingUser } =
@ -47,13 +43,13 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
id: user.id, id: user.id,
}); });
await navigate('/admin/users');
toast({ toast({
title: _(msg`Account deleted`), title: _(msg`Account deleted`),
description: _(msg`The account has been deleted successfully.`), description: _(msg`The account has been deleted successfully.`),
duration: 5000, duration: 5000,
}); });
router.push('/admin/users');
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);

View File

@ -1,13 +1,12 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { User } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -23,12 +22,15 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DisableUserDialogProps = { export type AdminUserDisableDialogProps = {
className?: string; className?: string;
userToDisable: User; userToDisable: User;
}; };
export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => { export const AdminUserDisableDialog = ({
className,
userToDisable,
}: AdminUserDisableDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();

View File

@ -1,13 +1,12 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { User } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -23,12 +22,12 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type EnableUserDialogProps = { export type AdminUserEnableDialogProps = {
className?: string; className?: string;
userToEnable: User; userToEnable: User;
}; };
export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => { export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -1,13 +1,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -22,26 +21,26 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type DeleteDocumentDialogProps = { type DocumentDeleteDialogProps = {
id: number; id: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
onDelete?: () => Promise<void> | void;
status: DocumentStatus; status: DocumentStatus;
documentTitle: string; documentTitle: string;
teamId?: number; teamId?: number;
canManageDocument: boolean; canManageDocument: boolean;
}; };
export const DeleteDocumentDialog = ({ export const DocumentDeleteDialog = ({
id, id,
open, open,
onOpenChange, onOpenChange,
onDelete,
status, status,
documentTitle, documentTitle,
canManageDocument, canManageDocument,
}: DeleteDocumentDialogProps) => { }: DocumentDeleteDialogProps) => {
const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { refreshLimits } = useLimits(); const { refreshLimits } = useLimits();
const { _ } = useLingui(); const { _ } = useLingui();
@ -52,8 +51,7 @@ export const DeleteDocumentDialog = ({
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({ const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
onSuccess: () => { onSuccess: async () => {
router.refresh();
void refreshLimits(); void refreshLimits();
toast({ toast({
@ -62,8 +60,18 @@ export const DeleteDocumentDialog = ({
duration: 5000, duration: 5000,
}); });
await onDelete?.();
onOpenChange(false); onOpenChange(false);
}, },
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`This document could not be deleted at this time. Please try again.`),
variant: 'destructive',
duration: 7500,
});
},
}); });
useEffect(() => { useEffect(() => {
@ -73,19 +81,6 @@ export const DeleteDocumentDialog = ({
} }
}, [open, status]); }, [open, status]);
const onDelete = async () => {
try {
await deleteDocument({ documentId: id });
} catch {
toast({
title: _(msg`Something went wrong`),
description: _(msg`This document could not be deleted at this time. Please try again.`),
variant: 'destructive',
duration: 7500,
});
}
};
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value); setInputValue(event.target.value);
setIsDeleteEnabled(event.target.value === _(deleteMessage)); setIsDeleteEnabled(event.target.value === _(deleteMessage));
@ -194,7 +189,7 @@ export const DeleteDocumentDialog = ({
<Button <Button
type="button" type="button"
loading={isPending} loading={isPending}
onClick={onDelete} onClick={() => void deleteDocument({ documentId: id })}
disabled={!isDeleteEnabled && canManageDocument} disabled={!isDeleteEnabled && canManageDocument}
variant="destructive" variant="destructive"
> >

View File

@ -1,10 +1,9 @@
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useNavigate } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -17,27 +16,34 @@ import {
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type DuplicateDocumentDialogProps = { import { useOptionalCurrentTeam } from '~/providers/team';
type DocumentDuplicateDialogProps = {
id: number; id: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
team?: Pick<Team, 'id' | 'url'>;
}; };
export const DuplicateDocumentDialog = ({ export const DocumentDuplicateDialog = ({
id, id,
open, open,
onOpenChange, onOpenChange,
team, }: DocumentDuplicateDialogProps) => {
}: DuplicateDocumentDialogProps) => { const navigate = useNavigate();
const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({ const team = useOptionalCurrentTeam();
documentId: id,
}); const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
{
documentId: id,
},
{
enabled: open === true,
},
);
const documentData = document?.documentData const documentData = document?.documentData
? { ? {
@ -50,15 +56,14 @@ export const DuplicateDocumentDialog = ({
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } = const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
trpcReact.document.duplicateDocument.useMutation({ trpcReact.document.duplicateDocument.useMutation({
onSuccess: ({ documentId }) => { onSuccess: async ({ documentId }) => {
router.push(`${documentsPath}/${documentId}/edit`);
toast({ toast({
title: _(msg`Document Duplicated`), title: _(msg`Document Duplicated`),
description: _(msg`Your document has been successfully duplicated.`), description: _(msg`Your document has been successfully duplicated.`),
duration: 5000, duration: 5000,
}); });
await navigate(`${documentsPath}/${documentId}/edit`);
onOpenChange(false); onOpenChange(false);
}, },
}); });

View File

@ -1,11 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -26,30 +25,28 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type MoveDocumentDialogProps = { type DocumentMoveDialogProps = {
documentId: number; documentId: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
}; };
export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => { export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentMoveDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({ const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh();
toast({ toast({
title: _(msg`Document moved`), title: _(msg`Document moved`),
description: _(msg`The document has been successfully moved to the selected team.`), description: _(msg`The document has been successfully moved to the selected team.`),
duration: 5000, duration: 5000,
}); });
onOpenChange(false); onOpenChange(false);
}, },
onError: (error) => { onError: (error) => {
@ -97,9 +94,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
{team.avatarImageId && ( {team.avatarImageId && (
<AvatarImage <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
/>
)} )}
<AvatarFallback className="text-sm text-gray-400"> <AvatarFallback className="text-sm text-gray-400">

View File

@ -1,19 +1,18 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Team } from '@prisma/client';
import { type Document, type Recipient, SigningStatus } from '@prisma/client';
import { History } from 'lucide-react'; import { History } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import * as z from 'zod'; import * as z from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Team } from '@documenso/prisma/client';
import { type Document, type Recipient, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -37,16 +36,17 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar'; import { useOptionalCurrentTeam } from '~/providers/team';
import { StackAvatar } from '../general/stack-avatar';
const FORM_ID = 'resend-email'; const FORM_ID = 'resend-email';
export type ResendDocumentActionItemProps = { export type DocumentResendDialogProps = {
document: Document & { document: Document & {
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
recipients: Recipient[]; recipients: Recipient[];
team?: Pick<Team, 'id' | 'url'>;
}; };
export const ZResendDocumentFormSchema = z.object({ export const ZResendDocumentFormSchema = z.object({
@ -57,17 +57,15 @@ export const ZResendDocumentFormSchema = z.object({
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>; export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
export const ResendDocumentActionItem = ({ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
document, const { user } = useSession();
recipients, const team = useOptionalCurrentTeam();
team,
}: ResendDocumentActionItemProps) => {
const { data: session } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const isOwner = document.userId === session?.user?.id; const isOwner = document.userId === user.id;
const isCurrentTeamDocument = team && document.team?.url === team.url; const isCurrentTeamDocument = team && document.team?.url === team.url;
const isDisabled = const isDisabled =

View File

@ -1,10 +1,9 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { startRegistration } from '@simplewebauthn/browser'; import { startRegistration } from '@simplewebauthn/browser';
import { KeyRoundIcon } from 'lucide-react'; import { KeyRoundIcon } from 'lucide-react';
@ -38,7 +37,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreatePasskeyDialogProps = { export type PasskeyCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
onSuccess?: () => void; onSuccess?: () => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -51,7 +50,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;
const parser = new UAParser(); const parser = new UAParser();
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => { export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [formError, setFormError] = useState<string | null>(null); const [formError, setFormError] = useState<string | null>(null);

View File

@ -1,18 +1,17 @@
'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import type { Template, TemplateDirectLink } from '@prisma/client';
import { TemplateType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CheckCircle2Icon, CircleIcon } from 'lucide-react'; import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import type { Template, TemplateDirectLink } from '@documenso/prisma/client';
import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { import {
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH, MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,

View File

@ -1,7 +1,8 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Loader, TagIcon } from 'lucide-react'; import { Loader, TagIcon } from 'lucide-react';
@ -20,18 +21,18 @@ import {
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamCheckoutDialogProps = { export type TeamCheckoutCreateDialogProps = {
pendingTeamId: number | null; pendingTeamId: number | null;
onClose: () => void; onClose: () => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
const MotionCard = motion(Card); const MotionCard = motion(Card);
export const CreateTeamCheckoutDialog = ({ export const TeamCheckoutCreateDialog = ({
pendingTeamId, pendingTeamId,
onClose, onClose,
...props ...props
}: CreateTeamCheckoutDialogProps) => { }: TeamCheckoutCreateDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();

View File

@ -1,18 +1,17 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useSearchParams } from 'react-router';
import { useNavigate } from 'react-router';
import type { z } from 'zod'; import type { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema'; import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
@ -37,7 +36,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamDialogProps = { export type TeamCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -48,12 +47,12 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>; type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => { export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const navigate = useNavigate();
const searchParams = useSearchParams(); const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -80,7 +79,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
setOpen(false); setOpen(false);
if (response.paymentRequired) { if (response.paymentRequired) {
router.push(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`); await navigate(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
return; return;
} }
@ -201,7 +200,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
{!form.formState.errors.teamUrl && ( {!form.formState.errors.teamUrl && (
<span className="text-foreground/50 text-xs font-normal"> <span className="text-foreground/50 text-xs font-normal">
{field.value ? ( {field.value ? (
`${WEBAPP_BASE_URL}/t/${field.value}` `${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
) : ( ) : (
<Trans>A unique URL to identify your team</Trans> <Trans>A unique URL to identify your team</Trans>
)} )}

View File

@ -1,13 +1,11 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
@ -34,14 +32,14 @@ import { Input } from '@documenso/ui/primitives/input';
import type { Toast } from '@documenso/ui/primitives/use-toast'; import type { Toast } from '@documenso/ui/primitives/use-toast';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamDialogProps = { export type TeamDeleteDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => { export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
const router = useRouter(); const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
@ -74,9 +72,9 @@ export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialog
duration: 5000, duration: 5000,
}); });
setOpen(false); await navigate('/settings/teams');
router.push('/settings/teams'); setOpen(false);
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);

View File

@ -1,15 +1,13 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import type { z } from 'zod'; import type { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@ -36,7 +34,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type AddTeamEmailDialogProps = { export type TeamEmailAddDialogProps = {
teamId: number; teamId: number;
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -48,13 +46,12 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>; type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => { export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const form = useForm<TCreateTeamEmailFormSchema>({ const form = useForm<TCreateTeamEmailFormSchema>({
resolver: zodResolver(ZCreateTeamEmailFormSchema), resolver: zodResolver(ZCreateTeamEmailFormSchema),
@ -81,7 +78,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
duration: 5000, duration: 5000,
}); });
router.refresh(); await revalidate();
setOpen(false); setOpen(false);
} catch (err) { } catch (err) {

View File

@ -1,15 +1,13 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Prisma } from '@prisma/client';
import { useRevalidator } from 'react-router';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Prisma } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert } from '@documenso/ui/primitives/alert'; import { Alert } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@ -25,7 +23,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type RemoveTeamEmailDialogProps = { export type TeamEmailDeleteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
teamName: string; teamName: string;
team: Prisma.TeamGetPayload<{ team: Prisma.TeamGetPayload<{
@ -42,13 +40,12 @@ export type RemoveTeamEmailDialogProps = {
}>; }>;
}; };
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => { export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter();
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } = const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({ trpc.team.deleteTeamEmail.useMutation({
@ -97,7 +94,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
await deleteTeamEmailVerification({ teamId: team.id }); await deleteTeamEmailVerification({ teamId: team.id });
} }
router.refresh(); await revalidate();
}; };
return ( return (
@ -127,7 +124,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
<Alert variant="neutral" padding="tight"> <Alert variant="neutral" padding="tight">
<AvatarWithText <AvatarWithText
avatarClass="h-12 w-12" avatarClass="h-12 w-12"
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`} avatarSrc={formatAvatarUrl(team.avatarImageId)}
avatarFallback={extractInitials( avatarFallback={extractInitials(
(team.teamEmail?.name || team.emailVerification?.name) ?? '', (team.teamEmail?.name || team.emailVerification?.name) ?? '',
)} )}

View File

@ -1,17 +1,15 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TeamEmail } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import type { TeamEmail } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -34,7 +32,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamEmailDialogProps = { export type TeamEmailUpdateDialogProps = {
teamEmail: TeamEmail; teamEmail: TeamEmail;
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -45,17 +43,16 @@ const ZUpdateTeamEmailFormSchema = z.object({
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>; type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
export const UpdateTeamEmailDialog = ({ export const TeamEmailUpdateDialog = ({
teamEmail, teamEmail,
trigger, trigger,
...props ...props
}: UpdateTeamEmailDialogProps) => { }: TeamEmailUpdateDialogProps) => {
const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const form = useForm<TUpdateTeamEmailFormSchema>({ const form = useForm<TUpdateTeamEmailFormSchema>({
resolver: zodResolver(ZUpdateTeamEmailFormSchema), resolver: zodResolver(ZUpdateTeamEmailFormSchema),
@ -81,7 +78,7 @@ export const UpdateTeamEmailDialog = ({
duration: 5000, duration: 5000,
}); });
router.refresh(); await revalidate();
setOpen(false); setOpen(false);
} catch (err) { } catch (err) {

View File

@ -1,13 +1,12 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TeamMemberRole } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams'; import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import type { TeamMemberRole } from '@documenso/prisma/client'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert } from '@documenso/ui/primitives/alert'; import { Alert } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@ -23,7 +22,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type LeaveTeamDialogProps = { export type TeamLeaveDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
teamAvatarImageId?: string | null; teamAvatarImageId?: string | null;
@ -31,13 +30,13 @@ export type LeaveTeamDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const LeaveTeamDialog = ({ export const TeamLeaveDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
teamAvatarImageId, teamAvatarImageId,
role, role,
}: LeaveTeamDialogProps) => { }: TeamLeaveDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
@ -89,7 +88,7 @@ export const LeaveTeamDialog = ({
<Alert variant="neutral" padding="tight"> <Alert variant="neutral" padding="tight">
<AvatarWithText <AvatarWithText
avatarClass="h-12 w-12" avatarClass="h-12 w-12"
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${teamAvatarImageId}`} avatarSrc={formatAvatarUrl(teamAvatarImageId)}
avatarFallback={teamName.slice(0, 1).toUpperCase()} avatarFallback={teamName.slice(0, 1).toUpperCase()}
primaryText={teamName} primaryText={teamName}
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])} secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}

View File

@ -1,9 +1,8 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert } from '@documenso/ui/primitives/alert'; import { Alert } from '@documenso/ui/primitives/alert';
@ -20,7 +19,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamMemberDialogProps = { export type TeamMemberDeleteDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
teamMemberId: number; teamMemberId: number;
@ -29,14 +28,14 @@ export type DeleteTeamMemberDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const DeleteTeamMemberDialog = ({ export const TeamMemberDeleteDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
teamMemberId, teamMemberId,
teamMemberName, teamMemberName,
teamMemberEmail, teamMemberEmail,
}: DeleteTeamMemberDialogProps) => { }: TeamMemberDeleteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -1,10 +1,10 @@
'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { TeamMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react'; import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
import Papa, { type ParseResult } from 'papaparse'; import Papa, { type ParseResult } from 'papaparse';
@ -13,7 +13,6 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams'; import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { TeamMemberRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema'; import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -47,9 +46,9 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type InviteTeamMembersDialogProps = { import { useCurrentTeam } from '~/providers/team';
currentUserTeamRole: TeamMemberRole;
teamId: number; export type TeamMemberInviteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -96,12 +95,7 @@ const ZImportTeamMemberSchema = z.array(
}), }),
); );
export const InviteTeamMembersDialog = ({ export const TeamMemberInviteDialog = ({ trigger, ...props }: TeamMemberInviteDialogProps) => {
currentUserTeamRole,
teamId,
trigger,
...props
}: InviteTeamMembersDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL'); const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
@ -109,6 +103,8 @@ export const InviteTeamMembersDialog = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const team = useCurrentTeam();
const form = useForm<TInviteTeamMembersFormSchema>({ const form = useForm<TInviteTeamMembersFormSchema>({
resolver: zodResolver(ZInviteTeamMembersFormSchema), resolver: zodResolver(ZInviteTeamMembersFormSchema),
defaultValues: { defaultValues: {
@ -142,7 +138,7 @@ export const InviteTeamMembersDialog = ({
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => { const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
try { try {
await createTeamMemberInvites({ await createTeamMemberInvites({
teamId, teamId: team.id,
invitations, invitations,
}); });
@ -204,7 +200,7 @@ export const InviteTeamMembersDialog = ({
setInvitationType('INDIVIDUAL'); setInvitationType('INDIVIDUAL');
} catch (err) { } catch (err) {
console.error(err.message); console.error(err);
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@ -325,11 +321,13 @@ export const InviteTeamMembersDialog = ({
</SelectTrigger> </SelectTrigger>
<SelectContent position="popper"> <SelectContent position="popper">
{TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => ( {TEAM_MEMBER_ROLE_HIERARCHY[team.currentTeamMember.role].map(
<SelectItem key={role} value={role}> (role) => (
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role} <SelectItem key={role} value={role}>
</SelectItem> {_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
))} </SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>

View File

@ -1,17 +1,16 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { TeamMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams'; import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams'; import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { TeamMemberRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -40,7 +39,7 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamMemberDialogProps = { export type TeamMemberUpdateDialogProps = {
currentUserTeamRole: TeamMemberRole; currentUserTeamRole: TeamMemberRole;
trigger?: React.ReactNode; trigger?: React.ReactNode;
teamId: number; teamId: number;
@ -55,7 +54,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>; type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
export const UpdateTeamMemberDialog = ({ export const TeamMemberUpdateDialog = ({
currentUserTeamRole, currentUserTeamRole,
trigger, trigger,
teamId, teamId,
@ -63,7 +62,7 @@ export const UpdateTeamMemberDialog = ({
teamMemberName, teamMemberName,
teamMemberRole, teamMemberRole,
...props ...props
}: UpdateTeamMemberDialogProps) => { }: TeamMemberUpdateDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -1,14 +1,12 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@ -42,24 +40,24 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type TransferTeamDialogProps = { export type TeamTransferDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
ownerUserId: number; ownerUserId: number;
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const TransferTeamDialog = ({ export const TeamTransferDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
ownerUserId, ownerUserId,
}: TransferTeamDialogProps) => { }: TeamTransferDialogProps) => {
const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const { mutateAsync: requestTeamOwnershipTransfer } = const { mutateAsync: requestTeamOwnershipTransfer } =
trpc.team.requestTeamOwnershipTransfer.useMutation(); trpc.team.requestTeamOwnershipTransfer.useMutation();
@ -102,7 +100,7 @@ export const TransferTeamDialog = ({
clearPaymentMethods, clearPaymentMethods,
}); });
router.refresh(); await revalidate();
toast({ toast({
title: _(msg`Success`), title: _(msg`Success`),

View File

@ -1,15 +1,12 @@
'use client'; import { useState } from 'react';
import React, { useState } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { FilePlus, Loader } from 'lucide-react'; import { FilePlus, Loader } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { useNavigate } from 'react-router';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -26,21 +23,21 @@ import {
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type NewTemplateDialogProps = { type TemplateCreateDialogProps = {
teamId?: number; teamId?: number;
templateRootPath: string; templateRootPath: string;
}; };
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => { export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => {
const router = useRouter(); const navigate = useNavigate();
const { data: session } = useSession(); const { user } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation(); const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false); const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false); const [isUploadingFile, setIsUploadingFile] = useState(false);
const onFileDrop = async (file: File) => { const onFileDrop = async (file: File) => {
@ -51,15 +48,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
setIsUploadingFile(true); setIsUploadingFile(true);
try { try {
const { type, data } = await putPdfFile(file); const response = await putPdfFile(file);
const { id: templateDocumentDataId } = await createDocumentData({
type,
data,
});
const { id } = await createTemplate({ const { id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId, templateDocumentDataId: response.id,
}); });
toast({ toast({
@ -70,9 +63,9 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
duration: 5000, duration: 5000,
}); });
setShowNewTemplateDialog(false); setShowTemplateCreateDialog(false);
router.push(`${templateRootPath}/${id}/edit`); await navigate(`${templateRootPath}/${id}/edit`);
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@ -86,11 +79,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
return ( return (
<Dialog <Dialog
open={showNewTemplateDialog} open={showTemplateCreateDialog}
onOpenChange={(value) => !isUploadingFile && setShowNewTemplateDialog(value)} onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="cursor-pointer" disabled={!session?.user.emailVerified}> <Button className="cursor-pointer" disabled={!user.emailVerified}>
<FilePlus className="-ml-1 mr-2 h-4 w-4" /> <FilePlus className="-ml-1 mr-2 h-4 w-4" />
<Trans>New Template</Trans> <Trans>New Template</Trans>
</Button> </Button>

View File

@ -1,7 +1,6 @@
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -15,22 +14,25 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type DeleteTemplateDialogProps = { type TemplateDeleteDialogProps = {
id: number; id: number;
teamId?: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
onDelete?: () => Promise<void> | void;
}; };
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => { export const TemplateDeleteDialog = ({
const router = useRouter(); id,
open,
onOpenChange,
onDelete,
}: TemplateDeleteDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({ const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
onSuccess: () => { onSuccess: async () => {
router.refresh(); await onDelete?.();
toast({ toast({
title: _(msg`Template deleted`), title: _(msg`Template deleted`),

View File

@ -1,14 +1,12 @@
'use client'; import { useState } from 'react';
import React, { useState } from 'react'; import { Trans } from '@lingui/react/macro';
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
import { Trans } from '@lingui/macro';
import { LinkIcon } from 'lucide-react'; import { LinkIcon } from 'lucide-react';
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog'; import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
export type TemplateDirectLinkDialogWrapperProps = { export type TemplateDirectLinkDialogWrapperProps = {
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] }; template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };

View File

@ -1,11 +1,16 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import {
type Recipient,
RecipientRole,
type Template,
type TemplateDirectLink,
} from '@prisma/client';
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react'; import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -14,12 +19,6 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template'; import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import {
type Recipient,
RecipientRole,
type Template,
type TemplateDirectLink,
} from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
@ -65,9 +64,9 @@ export const TemplateDirectLinkDialog = ({
const { toast } = useToast(); const { toast } = useToast();
const { quota, remaining } = useLimits(); const { quota, remaining } = useLimits();
const { _ } = useLingui(); const { _ } = useLingui();
const { revalidate } = useRevalidator();
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
const router = useRouter();
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false); const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
const [token, setToken] = useState(template.directLink?.token ?? null); const [token, setToken] = useState(template.directLink?.token ?? null);
@ -86,12 +85,12 @@ export const TemplateDirectLinkDialog = ({
isPending: isCreatingTemplateDirectLink, isPending: isCreatingTemplateDirectLink,
reset: resetCreateTemplateDirectLink, reset: resetCreateTemplateDirectLink,
} = trpcReact.template.createTemplateDirectLink.useMutation({ } = trpcReact.template.createTemplateDirectLink.useMutation({
onSuccess: (data) => { onSuccess: async (data) => {
await revalidate();
setToken(data.token); setToken(data.token);
setIsEnabled(data.enabled); setIsEnabled(data.enabled);
setCurrentStep('MANAGE'); setCurrentStep('MANAGE');
router.refresh();
}, },
onError: () => { onError: () => {
setSelectedRecipientId(null); setSelectedRecipientId(null);
@ -106,7 +105,9 @@ export const TemplateDirectLinkDialog = ({
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } = const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
trpcReact.template.toggleTemplateDirectLink.useMutation({ trpcReact.template.toggleTemplateDirectLink.useMutation({
onSuccess: (data) => { onSuccess: async (data) => {
await revalidate();
const enabledDescription = msg`Direct link signing has been enabled`; const enabledDescription = msg`Direct link signing has been enabled`;
const disabledDescription = msg`Direct link signing has been disabled`; const disabledDescription = msg`Direct link signing has been disabled`;
@ -129,7 +130,9 @@ export const TemplateDirectLinkDialog = ({
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } = const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
trpcReact.template.deleteTemplateDirectLink.useMutation({ trpcReact.template.deleteTemplateDirectLink.useMutation({
onSuccess: () => { onSuccess: async () => {
await revalidate();
onOpenChange(false); onOpenChange(false);
setToken(null); setToken(null);
@ -139,7 +142,6 @@ export const TemplateDirectLinkDialog = ({
duration: 5000, duration: 5000,
}); });
router.refresh();
setToken(null); setToken(null);
}, },
onError: () => { onError: () => {
@ -231,7 +233,7 @@ export const TemplateDirectLinkDialog = ({
templates.{' '} templates.{' '}
<Link <Link
className="mt-1 block underline underline-offset-4" className="mt-1 block underline underline-offset-4"
href="/settings/billing" to="/settings/billing"
> >
Upgrade your account to continue! Upgrade your account to continue!
</Link> </Link>
@ -432,7 +434,7 @@ export const TemplateDirectLinkDialog = ({
await toggleTemplateDirectLink({ await toggleTemplateDirectLink({
templateId: template.id, templateId: template.id,
enabled: isEnabled, enabled: isEnabled,
}).catch((e) => null); }).catch(() => null);
onOpenChange(false); onOpenChange(false);
}} }}

View File

@ -1,7 +1,6 @@
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -15,28 +14,23 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type DuplicateTemplateDialogProps = { type TemplateDuplicateDialogProps = {
id: number; id: number;
teamId?: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
}; };
export const DuplicateTemplateDialog = ({ export const TemplateDuplicateDialog = ({
id, id,
open, open,
onOpenChange, onOpenChange,
}: DuplicateTemplateDialogProps) => { }: TemplateDuplicateDialogProps) => {
const router = useRouter();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: duplicateTemplate, isPending } = const { mutateAsync: duplicateTemplate, isPending } =
trpcReact.template.duplicateTemplate.useMutation({ trpcReact.template.duplicateTemplate.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh();
toast({ toast({
title: _(msg`Template duplicated`), title: _(msg`Template duplicated`),
description: _(msg`Your template has been duplicated successfully.`), description: _(msg`Your template has been duplicated successfully.`),

View File

@ -1,13 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -28,29 +27,46 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type MoveTemplateDialogProps = { type TemplateMoveDialogProps = {
templateId: number; templateId: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
onMove?: ({
templateId,
teamUrl,
}: {
templateId: number;
teamUrl: string;
}) => Promise<void> | void;
}; };
export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => { export const TemplateMoveDialog = ({
const router = useRouter(); templateId,
open,
onOpenChange,
onMove,
}: TemplateMoveDialogProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({ const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => { onSuccess: async () => {
router.refresh(); const team = teams?.find((team) => team.id === selectedTeamId);
if (team) {
await onMove?.({ templateId, teamUrl: team.url });
}
toast({ toast({
title: _(msg`Template moved`), title: _(msg`Template moved`),
description: _(msg`The template has been successfully moved to the selected team.`), description: _(msg`The template has been successfully moved to the selected team.`),
duration: 5000, duration: 5000,
}); });
onOpenChange(false); onOpenChange(false);
}, },
onError: (err) => { onError: (err) => {
@ -73,7 +89,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
}, },
}); });
const onMove = async () => { const handleOnMove = async () => {
if (!selectedTeamId) { if (!selectedTeamId) {
return; return;
} }
@ -108,9 +124,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
{team.avatarImageId && ( {team.avatarImageId && (
<AvatarImage <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
/>
)} )}
<AvatarFallback className="text-sm text-gray-400"> <AvatarFallback className="text-sm text-gray-400">
@ -130,7 +144,11 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}> <Button
onClick={handleOnMove}
loading={isPending}
disabled={!selectedTeamId || isPending}
>
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>} {isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -1,14 +1,14 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
import { InfoIcon, Plus, Upload, X } from 'lucide-react'; import { InfoIcon, Plus, Upload, X } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import * as z from 'zod'; import * as z from 'zod';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
@ -18,8 +18,6 @@ import {
} from '@documenso/lib/constants/template'; } from '@documenso/lib/constants/template';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import type { Recipient } from '@documenso/prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -94,7 +92,7 @@ const ZAddRecipientsForNewDocumentSchema = z
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>; type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
export type UseTemplateDialogProps = { export type TemplateUseDialogProps = {
templateId: number; templateId: number;
templateSigningOrder?: DocumentSigningOrder | null; templateSigningOrder?: DocumentSigningOrder | null;
recipients: Recipient[]; recipients: Recipient[];
@ -103,19 +101,19 @@ export type UseTemplateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export function UseTemplateDialog({ export function TemplateUseDialog({
recipients, recipients,
documentDistributionMethod = DocumentDistributionMethod.EMAIL, documentDistributionMethod = DocumentDistributionMethod.EMAIL,
documentRootPath, documentRootPath,
templateId, templateId,
templateSigningOrder, templateSigningOrder,
trigger, trigger,
}: UseTemplateDialogProps) { }: TemplateUseDialogProps) {
const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const form = useForm<TAddRecipientsForNewDocumentSchema>({ const form = useForm<TAddRecipientsForNewDocumentSchema>({
@ -179,7 +177,7 @@ export function UseTemplateDialog({
documentPath += '?action=view-signing-links'; documentPath += '?action=view-signing-links';
} }
router.push(documentPath); await navigate(documentPath);
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);

View File

@ -1,16 +1,13 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { ApiToken } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import type { ApiToken } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -33,35 +30,33 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTokenDialogProps = { export type TokenDeleteDialogProps = {
teamId?: number; teamId?: number;
token: Pick<ApiToken, 'id' | 'name'>; token: Pick<ApiToken, 'id' | 'name'>;
onDelete?: () => void; onDelete?: () => void;
children?: React.ReactNode; children?: React.ReactNode;
}; };
export default function DeleteTokenDialog({ export default function TokenDeleteDialog({
teamId, teamId,
token, token,
onDelete, onDelete,
children, children,
}: DeleteTokenDialogProps) { }: TokenDeleteDialogProps) {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const deleteMessage = _(msg`delete ${token.name}`); const deleteMessage = _(msg`delete ${token.name}`);
const ZDeleteTokenDialogSchema = z.object({ const ZTokenDeleteDialogSchema = z.object({
tokenName: z.literal(deleteMessage, { tokenName: z.literal(deleteMessage, {
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }), errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
}), }),
}); });
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenDialogSchema>; type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({ const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
onSuccess() { onSuccess() {
@ -70,7 +65,7 @@ export default function DeleteTokenDialog({
}); });
const form = useForm<TDeleteTokenByIdMutationSchema>({ const form = useForm<TDeleteTokenByIdMutationSchema>({
resolver: zodResolver(ZDeleteTokenDialogSchema), resolver: zodResolver(ZTokenDeleteDialogSchema),
values: { values: {
tokenName: '', tokenName: '',
}, },
@ -90,8 +85,6 @@ export default function DeleteTokenDialog({
}); });
setIsOpen(false); setIsOpen(false);
router.refresh();
} catch (error) { } catch (error) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -1,12 +1,9 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
@ -39,22 +36,20 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox'; import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true }); const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>; type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
export type CreateWebhookDialogProps = { export type WebhookCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => { export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -94,8 +89,6 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
}); });
form.reset(); form.reset();
router.refresh();
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
@ -191,7 +184,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
<Trans>Triggers</Trans> <Trans>Triggers</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<TriggerMultiSelectCombobox <WebhookMultiSelectCombobox
listValues={value} listValues={value}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onChange(values); onChange(values);

View File

@ -1,16 +1,13 @@
'use effect';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Webhook } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import type { Webhook } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -35,18 +32,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
export type DeleteWebhookDialogProps = { export type WebhookDeleteDialogProps = {
webhook: Pick<Webhook, 'id' | 'webhookUrl'>; webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
onDelete?: () => void; onDelete?: () => void;
children: React.ReactNode; children: React.ReactNode;
}; };
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => { export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -81,8 +76,6 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
}); });
setOpen(false); setOpen(false);
router.refresh();
} catch (error) { } catch (error) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -1,20 +1,23 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Logo } from '~/components/branding/logo';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
import { BrandingLogo } from '~/components/general/branding-logo';
export type EmbedAuthenticateViewProps = { export type EmbedAuthenticationRequiredProps = {
email?: string; email?: string;
returnTo: string; returnTo: string;
}; };
export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => { export const EmbedAuthenticationRequired = ({
email,
returnTo,
}: EmbedAuthenticationRequiredProps) => {
return ( return (
<div className="flex min-h-[100dvh] w-full items-center justify-center"> <div className="flex min-h-[100dvh] w-full items-center justify-center">
<div className="flex w-full max-w-md flex-col"> <div className="flex w-full max-w-md flex-col">
<Logo className="h-8" /> <BrandingLogo className="h-8" />
<Alert className="mt-8" variant="warning"> <Alert className="mt-8" variant="warning">
<AlertDescription> <AlertDescription>

View File

@ -1,21 +1,19 @@
'use client';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type DocumentData, type Field, FieldType } from '@prisma/client';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useSearchParams } from 'react-router';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
@ -31,15 +29,15 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template'; import { BrandingLogo } from '~/components/general/branding-logo';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
import { Logo } from '~/components/branding/logo'; import { injectCss } from '~/utils/css-vars';
import { EmbedClientLoading } from '../../client-loading'; import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form';
import { EmbedDocumentCompleted } from '../../completed'; import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
import { EmbedDocumentFields } from '../../document-fields'; import { EmbedClientLoading } from './embed-client-loading';
import { injectCss } from '../../util'; import { EmbedDocumentCompleted } from './embed-document-completed';
import { ZDirectTemplateEmbedDataSchema } from './schema'; import { EmbedDocumentFields } from './embed-document-fields';
export type EmbedDirectTemplateClientPageProps = { export type EmbedDirectTemplateClientPageProps = {
token: string; token: string;
@ -65,7 +63,7 @@ export const EmbedDirectTemplateClientPage = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const searchParams = useSearchParams(); const [searchParams] = useSearchParams();
const { const {
fullName, fullName,
@ -76,7 +74,7 @@ export const EmbedDirectTemplateClientPage = ({
setEmail, setEmail,
setSignature, setSignature,
setSignatureValid, setSignatureValid,
} = useRequiredSigningContext(); } = useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -496,7 +494,7 @@ export const EmbedDirectTemplateClientPage = ({
{!hidePoweredBy && ( {!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100"> <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span> <span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" /> <BrandingLogo className="ml-2 inline-block h-[14px]" />
</div> </div>
)} )}
</div> </div>

View File

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import type { Signature } from '@prisma/client';
import signingCelebration from '@documenso/assets/images/signing-celebration.png'; import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import type { Signature } from '@documenso/prisma/client';
import { SigningCard3D } from '@documenso/ui/components/signing-card'; import { SigningCard3D } from '@documenso/ui/components/signing-card';
export type EmbedDocumentCompletedPageProps = { export type EmbedDocumentCompletedPageProps = {

View File

@ -1,5 +1,5 @@
'use client'; import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
import { type Field, FieldType } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -12,8 +12,6 @@ import {
ZRadioFieldMeta, ZRadioFieldMeta,
ZTextFieldMeta, ZTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type Field, FieldType } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type { import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
@ -21,16 +19,16 @@ import type {
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field'; import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
import { DateField } from '~/app/(signing)/sign/[token]/date-field'; import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field'; import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
import { EmailField } from '~/app/(signing)/sign/[token]/email-field'; import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field'; import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
import { NameField } from '~/app/(signing)/sign/[token]/name-field'; import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
import { NumberField } from '~/app/(signing)/sign/[token]/number-field'; import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field'; import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
export type EmbedDocumentFieldsProps = { export type EmbedDocumentFieldsProps = {
recipient: Recipient; recipient: Recipient;
@ -52,7 +50,7 @@ export const EmbedDocumentFields = ({
{fields.map((field) => {fields.map((field) =>
match(field.type) match(field.type)
.with(FieldType.SIGNATURE, () => ( .with(FieldType.SIGNATURE, () => (
<SignatureField <DocumentSigningSignatureField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient} recipient={recipient}
@ -62,7 +60,7 @@ export const EmbedDocumentFields = ({
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (
<InitialsField <DocumentSigningInitialsField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient} recipient={recipient}
@ -71,7 +69,7 @@ export const EmbedDocumentFields = ({
/> />
)) ))
.with(FieldType.NAME, () => ( .with(FieldType.NAME, () => (
<NameField <DocumentSigningNameField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient} recipient={recipient}
@ -80,7 +78,7 @@ export const EmbedDocumentFields = ({
/> />
)) ))
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField <DocumentSigningDateField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient} recipient={recipient}
@ -91,7 +89,7 @@ export const EmbedDocumentFields = ({
/> />
)) ))
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField <DocumentSigningEmailField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient} recipient={recipient}
@ -106,7 +104,7 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<TextField <DocumentSigningTextField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient} recipient={recipient}
@ -122,7 +120,7 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<NumberField <DocumentSigningNumberField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient} recipient={recipient}
@ -138,7 +136,7 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<RadioField <DocumentSigningRadioField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient} recipient={recipient}
@ -154,7 +152,7 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<CheckboxField <DocumentSigningCheckboxField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient} recipient={recipient}
@ -170,7 +168,7 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<DropdownField <DocumentSigningDropdownField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient} recipient={recipient}

View File

@ -1,16 +1,15 @@
'use client';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
import { type DocumentData, type Field, FieldType } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -22,14 +21,14 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { BrandingLogo } from '~/components/general/branding-logo';
import { Logo } from '~/components/branding/logo'; import { injectCss } from '~/utils/css-vars';
import { EmbedClientLoading } from '../../client-loading'; import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
import { EmbedDocumentCompleted } from '../../completed'; import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
import { EmbedDocumentFields } from '../../document-fields'; import { EmbedClientLoading } from './embed-client-loading';
import { injectCss } from '../../util'; import { EmbedDocumentCompleted } from './embed-document-completed';
import { ZSignDocumentEmbedDataSchema } from './schema'; import { EmbedDocumentFields } from './embed-document-fields';
export type EmbedSignDocumentClientPageProps = { export type EmbedSignDocumentClientPageProps = {
token: string; token: string;
@ -65,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({
setFullName, setFullName,
setSignature, setSignature,
setSignatureValid, setSignatureValid,
} = useRequiredSigningContext(); } = useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -369,7 +368,7 @@ export const EmbedSignDocumentClientPage = ({
{!hidePoweredBy && ( {!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100"> <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span> <span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" /> <BrandingLogo className="ml-2 inline-block h-[14px]" />
</div> </div>
)} )}
</div> </div>

View File

@ -1,14 +1,12 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@ -42,10 +40,9 @@ export const ZDisable2FAForm = z.object({
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>; export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
export const DisableAuthenticatorAppDialog = () => { export const DisableAuthenticatorAppDialog = () => {
const router = useRouter();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp'); const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
@ -97,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => {
onCloseTwoFactorDisableDialog(); onCloseTwoFactorDisableDialog();
}); });
router.refresh(); await revalidate();
} catch (_err) { } catch (_err) {
toast({ toast({
title: _(msg`Unable to disable two-factor authentication`), title: _(msg`Unable to disable two-factor authentication`),

View File

@ -1,13 +1,11 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { renderSVG } from 'uqr'; import { renderSVG } from 'uqr';
import { z } from 'zod'; import { z } from 'zod';
@ -50,8 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => { export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null); const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
@ -133,7 +130,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) { if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null); setRecoveryCodes(null);
router.refresh(); void revalidate();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -1,4 +1,4 @@
import { msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Copy } from 'lucide-react'; import { Copy } from 'lucide-react';

View File

@ -1,16 +1,13 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -147,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
{match(AppError.parseError(error).message) {match(AppError.parseError(error).message)
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => ( .with('INCORRECT_TWO_FACTOR_CODE', () => (
<Trans>Invalid code. Please try again.</Trans> <Trans>Invalid code. Please try again.</Trans>
)) ))
.otherwise(() => ( .otherwise(() => (

View File

@ -1,22 +1,20 @@
'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ErrorCode, useDropzone } from 'react-dropzone'; import { ErrorCode, useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { base64 } from '@documenso/lib/universal/base64'; import { base64 } from '@documenso/lib/universal/base64';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Team, User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
@ -31,6 +29,8 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export const ZAvatarImageFormSchema = z.object({ export const ZAvatarImageFormSchema = z.object({
bytes: z.string().nullish(), bytes: z.string().nullish(),
}); });
@ -39,15 +39,15 @@ export type TAvatarImageFormSchema = z.infer<typeof ZAvatarImageFormSchema>;
export type AvatarImageFormProps = { export type AvatarImageFormProps = {
className?: string; className?: string;
user: User;
team?: Team;
}; };
export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps) => { export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
const { user } = useSession();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter(); const team = useOptionalCurrentTeam();
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation(); const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
@ -109,7 +109,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
duration: 5000, duration: 5000,
}); });
router.refresh(); void revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -146,11 +146,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<div className="relative"> <div className="relative">
<Avatar className="h-16 w-16 border-2 border-solid"> <Avatar className="h-16 w-16 border-2 border-solid">
{avatarImageId && ( {avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
<AvatarImage
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
/>
)}
<AvatarFallback className="text-sm text-gray-400"> <AvatarFallback className="text-sm text-gray-400">
{initials} {initials}
</AvatarFallback> </AvatarFallback>

View File

@ -1,14 +1,12 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { trpc } from '@documenso/trpc/react'; import { authClient } from '@documenso/auth/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -36,7 +34,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const navigate = useNavigate();
const form = useForm<TForgotPasswordFormSchema>({ const form = useForm<TForgotPasswordFormSchema>({
values: { values: {
@ -47,10 +45,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => { const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
await forgotPassword({ email }).catch(() => null); await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
await navigate('/check-email');
toast({ toast({
title: _(msg`Reset email sent`), title: _(msg`Reset email sent`),
@ -61,8 +59,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
}); });
form.reset(); form.reset();
router.push('/check-email');
}; };
return ( return (

View File

@ -1,15 +1,14 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -39,7 +38,7 @@ export type TPasswordFormSchema = z.infer<typeof ZPasswordFormSchema>;
export type PasswordFormProps = { export type PasswordFormProps = {
className?: string; className?: string;
user: User; user: SessionUser;
}; };
export const PasswordForm = ({ className }: PasswordFormProps) => { export const PasswordForm = ({ className }: PasswordFormProps) => {
@ -57,11 +56,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => { const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
try { try {
await updatePassword({ await authClient.emailPassword.updatePassword({
currentPassword, currentPassword,
password, password,
}); });

View File

@ -1,14 +1,12 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import type { User } from '@documenso/prisma/client'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -39,14 +37,13 @@ export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
export type ProfileFormProps = { export type ProfileFormProps = {
className?: string; className?: string;
user: User;
}; };
export const ProfileForm = ({ className, user }: ProfileFormProps) => { export const ProfileForm = ({ className }: ProfileFormProps) => {
const router = useRouter();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useSession();
const { revalidate } = useRevalidator();
const form = useForm<TProfileFormSchema>({ const form = useForm<TProfileFormSchema>({
values: { values: {
@ -73,7 +70,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
duration: 5000, duration: 5000,
}); });
router.refresh(); await revalidate();
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -1,19 +1,15 @@
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { User } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png'; import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -35,7 +31,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { UserProfileSkeleton } from '../ui/user-profile-skeleton'; import { UserProfileSkeleton } from '../general/user-profile-skeleton';
export const ZClaimPublicProfileFormSchema = z.object({ export const ZClaimPublicProfileFormSchema = z.object({
url: z url: z
@ -92,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) { if (error.code === 'PROFILE_URL_TAKEN') {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: _(msg`This username is already taken`), message: _(msg`This username is already taken`),
}); });
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) { } else if (error.code === 'PREMIUM_PROFILE_URL') {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: error.message, message: error.message,
@ -135,7 +131,7 @@ export const ClaimPublicProfileDialogForm = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Image src={profileClaimTeaserImage} alt="profile claim teaser" /> <img src={profileClaimTeaserImage} alt="profile claim teaser" />
<Form {...form}> <Form {...form}>
<form <form

View File

@ -1,10 +1,10 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import type { TeamProfile, UserProfile } from '@prisma/client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { CheckSquareIcon, CopyIcon } from 'lucide-react'; import { CheckSquareIcon, CopyIcon } from 'lucide-react';
@ -12,9 +12,8 @@ import { useForm } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles'; import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
import type { TeamProfile, UserProfile } from '@documenso/prisma/client';
import { import {
MAX_PROFILE_BIO_LENGTH, MAX_PROFILE_BIO_LENGTH,
ZUpdatePublicProfileMutationSchema, ZUpdatePublicProfileMutationSchema,
@ -90,8 +89,8 @@ export const PublicProfileForm = ({
const error = AppError.parseError(err); const error = AppError.parseError(err);
switch (error.code) { switch (error.code) {
case AppErrorCode.PREMIUM_PROFILE_URL: case 'PREMIUM_PROFILE_URL':
case AppErrorCode.PROFILE_URL_TAKEN: case 'PROFILE_URL_TAKEN':
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: error.message, message: error.message,

View File

@ -1,16 +1,14 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -43,7 +41,7 @@ export type ResetPasswordFormProps = {
}; };
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => { export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
const router = useRouter(); const navigate = useNavigate();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -58,15 +56,15 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => { const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
try { try {
await resetPassword({ await authClient.emailPassword.resetPassword({
password, password,
token, token,
}); });
await navigate('/signin');
form.reset(); form.reset();
toast({ toast({
@ -74,8 +72,6 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
description: _(msg`Your password has been updated successfully.`), description: _(msg`Your password has been updated successfully.`),
duration: 5000, duration: 5000,
}); });
router.push('/signin');
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useLocation, useNavigate } from 'react-router';
import { useSearchParams } from 'react-router';
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
@ -11,10 +12,10 @@ export type SearchParamSelector = {
}; };
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => { export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
const pathname = usePathname(); const { pathname } = useLocation();
const searchParams = useSearchParams(); const [searchParams] = useSearchParams();
const router = useRouter(); const navigate = useNavigate();
const value = useMemo(() => { const value = useMemo(() => {
const p = searchParams?.get(paramKey) ?? 'all'; const p = searchParams?.get(paramKey) ?? 'all';
@ -35,7 +36,7 @@ export const SearchParamSelector = ({ children, paramKey, isValueValid }: Search
params.delete(paramKey); params.delete(paramKey);
} }
router.push(`${pathname}?${params.toString()}`, { scroll: false }); void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
}; };
return ( return (

View File

@ -1,12 +1,11 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { trpc } from '@documenso/trpc/react'; import { authClient } from '@documenso/auth/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -43,11 +42,9 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => { const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
try { try {
await sendConfirmationEmail({ email }); await authClient.emailPassword.resendVerifyEmail({ email });
toast({ toast({
title: _(msg`Confirmation email sent`), title: _(msg`Confirmation email sent`),
@ -60,6 +57,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
form.reset(); form.reset();
} catch (err) { } catch (err) {
toast({ toast({
variant: 'destructive',
title: _(msg`An error occurred while sending your confirmation email`), title: _(msg`An error occurred while sending your confirmation email`),
description: _(msg`Please try again and make sure you enter the correct email address.`), description: _(msg`Please try again and make sure you enter the correct email address.`),
}); });

View File

@ -1,25 +1,22 @@
'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser'; import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
import { KeyRoundIcon } from 'lucide-react'; import { KeyRoundIcon } from 'lucide-react';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6'; import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc'; import { FcGoogle } from 'react-icons/fc';
import { Link, useNavigate } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { authClient } from '@documenso/auth/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -44,19 +41,19 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = { const CommonErrorMessages: Record<string, MessageDescriptor> = {
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect', [AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
[ErrorCode.USER_MISSING_PASSWORD]:
'This account appears to be using a social login method, please sign in using that method',
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
[ErrorCode.UNVERIFIED_EMAIL]:
'This account has not been verified. Please verify your account before signing in.',
[ErrorCode.ACCOUNT_DISABLED]: 'This account has been disabled. Please contact support.',
}; };
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS; const handleFallbackErrorMessages = (code: string) => {
const message = CommonErrorMessages[code];
if (!message) {
return msg`An unknown error occurred`;
}
return message;
};
const LOGIN_REDIRECT_PATH = '/documents'; const LOGIN_REDIRECT_PATH = '/documents';
@ -88,9 +85,8 @@ export const SignInForm = ({
}: SignInFormProps) => { }: SignInFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { getFlag } = useFeatureFlags();
const router = useRouter(); const navigate = useNavigate();
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] = const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false); useState(false);
@ -101,9 +97,7 @@ export const SignInForm = ({
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
const isPasskeyEnabled = getFlag('app_passkey'); const redirectPath = useMemo(() => {
const callbackUrl = useMemo(() => {
// Handle SSR // Handle SSR
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return LOGIN_REDIRECT_PATH; return LOGIN_REDIRECT_PATH;
@ -170,25 +164,20 @@ export const SignInForm = ({
try { try {
setIsPasskeyLoading(true); setIsPasskeyLoading(true);
const options = await createPasskeySigninOptions(); const { options, sessionId } = await createPasskeySigninOptions();
const credential = await startAuthentication(options); const credential = await startAuthentication(options);
const result = await signIn('webauthn', { await authClient.passkey.signIn({
credential: JSON.stringify(credential), credential: JSON.stringify(credential),
callbackUrl, csrfToken: sessionId,
redirect: false, redirectPath,
}); });
if (!result?.url || result.error) {
throw new AppError(result?.error ?? '');
}
window.location.href = result.url;
} catch (err) { } catch (err) {
setIsPasskeyLoading(false); setIsPasskeyLoading(false);
if (err.name === 'NotAllowedError') { // Error from library.
if (err instanceof Error && err.name === 'NotAllowedError') {
return; return;
} }
@ -196,12 +185,15 @@ export const SignInForm = ({
const errorMessage = match(error.code) const errorMessage = match(error.code)
.with( .with(
AppErrorCode.NOT_SETUP, AuthenticationErrorCode.NotSetup,
() => () =>
msg`This passkey is not configured for this application. Please login and add one in the user settings.`, msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
) )
.with(AppErrorCode.EXPIRED_CODE, () => msg`This session has expired. Please try again.`) .with(
.otherwise(() => msg`Please try again later or login using your normal details`); AuthenticationErrorCode.SessionExpired,
() => msg`This session has expired. Please try again.`,
)
.otherwise(() => handleFallbackErrorMessages(error.code));
toast({ toast({
title: 'Something went wrong', title: 'Something went wrong',
@ -214,73 +206,58 @@ export const SignInForm = ({
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => { const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
try { try {
const credentials: Record<string, string> = { await authClient.emailPassword.signIn({
email, email,
password, password,
}; totpCode,
backupCode,
if (totpCode) { redirectPath,
credentials.totpCode = totpCode;
}
if (backupCode) {
credentials.backupCode = backupCode;
}
const result = await signIn('credentials', {
...credentials,
callbackUrl,
redirect: false,
}); });
} catch (err) {
console.log(err);
if (result?.error && isErrorCode(result.error)) { const error = AppError.parseError(err);
if (result.error === TwoFactorEnabledErrorCode) {
setIsTwoFactorAuthenticationDialogOpen(true);
return;
}
const errorMessage = ERROR_MESSAGES[result.error]; if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true);
return;
}
if (result.error === ErrorCode.UNVERIFIED_EMAIL) { if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
router.push(`/unverified-account`); await navigate('/unverified-account');
toast({
title: _(msg`Unable to sign in`),
description: errorMessage ?? _(msg`An unknown error occurred`),
});
return;
}
toast({ toast({
title: _(msg`Unable to sign in`), title: _(msg`Unable to sign in`),
description: errorMessage ?? _(msg`An unknown error occurred`), description: _(
variant: 'destructive', msg`This account has not been verified. Please verify your account before signing in.`,
),
}); });
return; return;
} }
if (!result?.url) { const errorMessage = match(error.code)
throw new Error('An unknown error occurred'); .with(
} AuthenticationErrorCode.InvalidCredentials,
() => msg`The email or password provided is incorrect`,
)
.with(
AuthenticationErrorCode.InvalidTwoFactorCode,
() => msg`The two-factor authentication code provided is incorrect`,
)
.otherwise(() => handleFallbackErrorMessages(error.code));
window.location.href = result.url;
} catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`Unable to sign in`),
description: _( description: _(errorMessage),
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`, variant: 'destructive',
),
}); });
} }
}; };
const onSignInWithGoogleClick = async () => { const onSignInWithGoogleClick = async () => {
try { try {
await signIn('google', { await authClient.google.signIn();
callbackUrl,
});
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
@ -294,9 +271,11 @@ export const SignInForm = ({
const onSignInWithOIDCClick = async () => { const onSignInWithOIDCClick = async () => {
try { try {
await signIn('oidc', { // eslint-disable-next-line no-promise-executor-return
callbackUrl, await new Promise((resolve) => setTimeout(resolve, 2000));
}); // await signIn('oidc', {
// callbackUrl,
// });
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
@ -365,7 +344,7 @@ export const SignInForm = ({
<p className="mt-2 text-right"> <p className="mt-2 text-right">
<Link <Link
href="/forgot-password" to="/forgot-password"
className="text-muted-foreground text-sm duration-200 hover:opacity-70" className="text-muted-foreground text-sm duration-200 hover:opacity-70"
> >
<Trans>Forgot your password?</Trans> <Trans>Forgot your password?</Trans>
@ -384,7 +363,7 @@ export const SignInForm = ({
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>} {isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
</Button> </Button>
{(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && ( {(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase"> <div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" /> <div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent"> <span className="text-muted-foreground bg-transparent">
@ -422,20 +401,18 @@ export const SignInForm = ({
</Button> </Button>
)} )}
{isPasskeyEnabled && ( <Button
<Button type="button"
type="button" size="lg"
size="lg" variant="outline"
variant="outline" disabled={isSubmitting}
disabled={isSubmitting} loading={isPasskeyLoading}
loading={isPasskeyLoading} className="bg-background text-muted-foreground border"
className="bg-background text-muted-foreground border" onClick={onSignInWithPasskey}
onClick={onSignInWithPasskey} >
> {!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />} <Trans>Passkey</Trans>
<Trans>Passkey</Trans> </Button>
</Button>
)}
</fieldset> </fieldset>
</form> </form>

View File

@ -1,27 +1,22 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core'; import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6'; import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc'; import { FcGoogle } from 'react-icons/fc';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import communityCardsImage from '@documenso/assets/images/community-cards.png'; import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -38,14 +33,14 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton'; import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
import { UserProfileTimur } from '~/components/ui/user-profile-timur'; import { UserProfileTimur } from '~/components/general/user-profile-timur';
const SIGN_UP_REDIRECT_PATH = '/documents'; const SIGN_UP_REDIRECT_PATH = '/documents';
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME'; type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
export const ZSignUpFormV2Schema = z export const ZSignUpFormSchema = z
.object({ .object({
name: z name: z
.string() .string()
@ -78,39 +73,39 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`, SIGNUP_DISABLED: msg`Signups are disabled.`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`, [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`, [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`, PROFILE_URL_TAKEN: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`, PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
}; };
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>; export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
export type SignUpFormV2Props = { export type SignUpFormProps = {
className?: string; className?: string;
initialEmail?: string; initialEmail?: string;
isGoogleSSOEnabled?: boolean; isGoogleSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean;
}; };
export const SignUpFormV2 = ({ export const SignUpForm = ({
className, className,
initialEmail, initialEmail,
isGoogleSSOEnabled, isGoogleSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
}: SignUpFormV2Props) => { }: SignUpFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const analytics = useAnalytics(); const analytics = useAnalytics();
const router = useRouter(); const navigate = useNavigate();
const searchParams = useSearchParams(); const [searchParams] = useSearchParams();
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS'); const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
const utmSrc = searchParams?.get('utm_source') ?? null; const utmSrc = searchParams.get('utm_source') ?? null;
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000'); const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
const form = useForm<TSignUpFormV2Schema>({ const form = useForm<TSignUpFormSchema>({
values: { values: {
name: '', name: '',
email: initialEmail ?? '', email: initialEmail ?? '',
@ -119,7 +114,7 @@ export const SignUpFormV2 = ({
url: '', url: '',
}, },
mode: 'onBlur', mode: 'onBlur',
resolver: zodResolver(ZSignUpFormV2Schema), resolver: zodResolver(ZSignUpFormSchema),
}); });
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
@ -127,13 +122,17 @@ export const SignUpFormV2 = ({
const name = form.watch('name'); const name = form.watch('name');
const url = form.watch('url'); const url = form.watch('url');
const { mutateAsync: signup } = trpc.auth.signup.useMutation(); const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
try { try {
await signup({ name, email, password, signature, url }); await authClient.emailPassword.signUp({
name,
email,
password,
signature,
url,
});
router.push(`/unverified-account`); await navigate(`/unverified-account`);
toast({ toast({
title: _(msg`Registration Successful`), title: _(msg`Registration Successful`),
@ -153,10 +152,7 @@ export const SignUpFormV2 = ({
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST; const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
if ( if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
) {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: _(errorMessage), message: _(errorMessage),
@ -181,7 +177,7 @@ export const SignUpFormV2 = ({
const onSignUpWithGoogleClick = async () => { const onSignUpWithGoogleClick = async () => {
try { try {
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH }); await authClient.google.signIn();
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
@ -195,7 +191,9 @@ export const SignUpFormV2 = ({
const onSignUpWithOIDCClick = async () => { const onSignUpWithOIDCClick = async () => {
try { try {
await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH }); // eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 2000));
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
@ -223,11 +221,10 @@ export const SignUpFormV2 = ({
<div className={cn('flex justify-center gap-x-12', className)}> <div className={cn('flex justify-center gap-x-12', className)}>
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex"> <div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
<div className="absolute -inset-8 -z-[2] backdrop-blur"> <div className="absolute -inset-8 -z-[2] backdrop-blur">
<Image <img
src={communityCardsImage} src={communityCardsImage}
fill={true}
alt="community-cards" alt="community-cards"
className="dark:brightness-95 dark:contrast-[70%] dark:invert" className="h-full w-full object-cover dark:brightness-95 dark:contrast-[70%] dark:invert"
/> />
</div> </div>
@ -426,10 +423,7 @@ export const SignUpFormV2 = ({
<p className="text-muted-foreground mt-4 text-sm"> <p className="text-muted-foreground mt-4 text-sm">
<Trans> <Trans>
Already have an account?{' '} Already have an account?{' '}
<Link <Link to="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
href="/signin"
className="text-documenso-700 duration-200 hover:opacity-70"
>
Sign in instead Sign in instead
</Link> </Link>
</Trans> </Trans>

View File

@ -1,17 +1,16 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Team, TeamGlobalSettings } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putFile } from '@documenso/lib/universal/upload/put-file';
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';

View File

@ -1,19 +1,18 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { useSession } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import type { Team, TeamGlobalSettings } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { import {
SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGES,
SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode, isValidLanguageCode,
} from '@documenso/lib/constants/i18n'; } from '@documenso/lib/constants/i18n';
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
import { DocumentVisibility } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert } from '@documenso/ui/primitives/alert'; import { Alert } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -56,9 +55,9 @@ export const TeamDocumentPreferencesForm = ({
}: TeamDocumentPreferencesFormProps) => { }: TeamDocumentPreferencesFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { data } = useSession(); const { user } = useSession();
const placeholderEmail = data?.user.email ?? 'user@example.com'; const placeholderEmail = user.email ?? 'user@example.com';
const { mutateAsync: updateTeamDocumentPreferences } = const { mutateAsync: updateTeamDocumentPreferences } =
trpc.team.updateTeamDocumentSettings.useMutation(); trpc.team.updateTeamDocumentSettings.useMutation();

View File

@ -1,15 +1,13 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import type { z } from 'zod'; import type { z } from 'zod';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema'; import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
@ -31,20 +29,20 @@ export type UpdateTeamDialogProps = {
teamUrl: string; teamUrl: string;
}; };
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({ const ZTeamUpdateFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
name: true, name: true,
url: true, url: true,
}); });
type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>; type TTeamUpdateFormSchema = z.infer<typeof ZTeamUpdateFormSchema>;
export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => { export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
const router = useRouter(); const navigate = useNavigate();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const form = useForm({ const form = useForm({
resolver: zodResolver(ZUpdateTeamFormSchema), resolver: zodResolver(ZTeamUpdateFormSchema),
defaultValues: { defaultValues: {
name: teamName, name: teamName,
url: teamUrl, url: teamUrl,
@ -53,7 +51,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation(); const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => { const onFormSubmit = async ({ name, url }: TTeamUpdateFormSchema) => {
try { try {
await updateTeam({ await updateTeam({
data: { data: {
@ -75,7 +73,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
}); });
if (url !== teamUrl) { if (url !== teamUrl) {
router.push(`${WEBAPP_BASE_URL}/t/${url}/settings`); await navigate(`/t/${url}/settings`);
} }
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -133,7 +131,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
{!form.formState.errors.url && ( {!form.formState.errors.url && (
<span className="text-foreground/50 text-xs font-normal"> <span className="text-foreground/50 text-xs font-normal">
{field.value ? ( {field.value ? (
`${WEBAPP_BASE_URL}/t/${field.value}` `${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
) : ( ) : (
<Trans>A unique URL to identify your team</Trans> <Trans>A unique URL to identify your team</Trans>
)} )}

View File

@ -1,12 +1,10 @@
'use client';
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { ApiToken } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -14,7 +12,6 @@ import { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { ApiToken } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema'; import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema'; import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
@ -41,7 +38,13 @@ import {
import { Switch } from '@documenso/ui/primitives/switch'; import { Switch } from '@documenso/ui/primitives/switch';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { EXPIRATION_DATES } from '../(dashboard)/settings/token/contants'; export const EXPIRATION_DATES = {
ONE_WEEK: msg`7 days`,
ONE_MONTH: msg`1 month`,
THREE_MONTHS: msg`3 months`,
SIX_MONTHS: msg`6 months`,
ONE_YEAR: msg`12 months`,
} as const;
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({ const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
enabled: z.boolean(), enabled: z.boolean(),
@ -61,7 +64,6 @@ export type ApiTokenFormProps = {
}; };
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => { export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
const router = useRouter();
const [isTransitionPending, startTransition] = useTransition(); const [isTransitionPending, startTransition] = useTransition();
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
@ -72,13 +74,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>(); const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
const [noExpirationDate, setNoExpirationDate] = useState(false); const [noExpirationDate, setNoExpirationDate] = useState(false);
// This lets us hide the token from being copied if it has been deleted without
// resorting to a useEffect or any other fanciness. This comes at the cost of it
// taking slighly longer to appear since it will need to wait for the router.refresh()
// to finish updating.
const hasNewlyCreatedToken =
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({ const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
onSuccess(data) { onSuccess(data) {
setNewlyCreatedToken(data); setNewlyCreatedToken(data);
@ -130,8 +125,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
}); });
form.reset(); form.reset();
startTransition(() => router.refresh());
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -263,34 +256,36 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
</form> </form>
</Form> </Form>
<AnimatePresence initial={!hasNewlyCreatedToken}> <AnimatePresence>
{newlyCreatedToken && hasNewlyCreatedToken && ( {newlyCreatedToken &&
<motion.div tokens &&
className="mt-8" tokens.find((token) => token.id === newlyCreatedToken.id) && (
initial={{ opacity: 0, y: -40 }} <motion.div
animate={{ opacity: 1, y: 0 }} className="mt-8"
exit={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: -40 }}
> animate={{ opacity: 1, y: 0 }}
<Card gradient> exit={{ opacity: 0, y: 40 }}
<CardContent className="p-4"> >
<p className="text-muted-foreground mt-2 text-sm"> <Card gradient>
<Trans> <CardContent className="p-4">
Your token was created successfully! Make sure to copy it because you won't be <p className="text-muted-foreground mt-2 text-sm">
able to see it again! <Trans>
</Trans> Your token was created successfully! Make sure to copy it because you won't be
</p> able to see it again!
</Trans>
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm"> <p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken.token} {newlyCreatedToken.token}
</p> </p>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}> <Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
<Trans>Copy token</Trans> <Trans>Copy token</Trans>
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
); );

View File

@ -1,23 +1,21 @@
'use client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion'; import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
export type SignerConversionChartProps = { export type AdminStatsSignerConversionChartProps = {
className?: string; className?: string;
title: string; title: string;
cummulative?: boolean; cummulative?: boolean;
data: GetSignerConversionMonthlyResult; data: GetSignerConversionMonthlyResult;
}; };
export const SignerConversionChart = ({ export const AdminStatsSignerConversionChart = ({
className, className,
data, data,
title, title,
cummulative = false, cummulative = false,
}: SignerConversionChartProps) => { }: AdminStatsSignerConversionChartProps) => {
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => { const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
return { return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'), month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),

View File

@ -1,5 +1,3 @@
'use client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { TooltipProps } from 'recharts'; import type { TooltipProps } from 'recharts';
@ -7,7 +5,7 @@ import type { NameType, ValueType } from 'recharts/types/component/DefaultToolti
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats'; import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
export type UserWithDocumentChartProps = { export type AdminStatsUsersWithDocumentsChartProps = {
className?: string; className?: string;
title: string; title: string;
data: GetUserWithDocumentMonthlyGrowth; data: GetUserWithDocumentMonthlyGrowth;
@ -23,7 +21,7 @@ const CustomTooltip = ({
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => { }: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3"> <div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p> <p className="">{label}</p>
<p className="text-documenso"> <p className="text-documenso">
{`${tooltip} : `} {`${tooltip} : `}
@ -36,13 +34,13 @@ const CustomTooltip = ({
return null; return null;
}; };
export const UserWithDocumentChart = ({ export const AdminStatsUsersWithDocumentsChart = ({
className, className,
data, data,
title, title,
completed = false, completed = false,
tooltip, tooltip,
}: UserWithDocumentChartProps) => { }: AdminStatsUsersWithDocumentsChartProps) => {
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => { const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
return [...data].reverse().map(({ month, count, signed_count }) => { return [...data].reverse().map(({ month, count, signed_count }) => {
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'); const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');

View File

@ -0,0 +1,28 @@
import { type TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
export type AppBannerProps = {
banner: TSiteSettingsBannerSchema;
};
export const AppBanner = ({ banner }: AppBannerProps) => {
if (!banner.enabled) {
return null;
}
return (
<div className="mb-2" style={{ background: banner.data.bgColor }}>
<div
className="mx-auto flex h-auto max-w-screen-xl items-center justify-center px-4 py-3 text-sm font-medium"
style={{ color: banner.data.textColor }}
>
<div className="flex items-center">
<span dangerouslySetInnerHTML={{ __html: banner.data.content }} />
</div>
</div>
</div>
);
};
// Banner
// Custom Text
// Custom Text with Custom Icon

View File

@ -1,15 +1,13 @@
'use client';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core'; import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react'; import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router';
import { Theme, useTheme } from 'remix-themes';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n'; import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { import {
@ -21,7 +19,6 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION, DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META, SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc'; } from '@documenso/lib/constants/trpc';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n'; import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -34,7 +31,6 @@ import {
CommandList, CommandList,
CommandShortcut, CommandShortcut,
} from '@documenso/ui/primitives/command'; } from '@documenso/ui/primitives/command';
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
const DOCUMENTS_PAGES = [ const DOCUMENTS_PAGES = [
@ -70,16 +66,15 @@ const SETTINGS_PAGES = [
{ label: msg`Password`, path: '/settings/password' }, { label: msg`Password`, path: '/settings/password' },
]; ];
export type CommandMenuProps = { export type AppCommandMenuProps = {
open?: boolean; open?: boolean;
onOpenChange?: (_open: boolean) => void; onOpenChange?: (_open: boolean) => void;
}; };
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
const { _ } = useLingui(); const { _ } = useLingui();
const { setTheme } = useTheme();
const router = useRouter(); const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(() => open ?? false); const [isOpen, setIsOpen] = useState(() => open ?? false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -138,10 +133,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const push = useCallback( const push = useCallback(
(path: string) => { (path: string) => {
router.push(path); void navigate(path);
setOpen(false); setOpen(false);
}, },
[router, setOpen], [setOpen],
); );
const addPage = (page: string) => { const addPage = (page: string) => {
@ -227,7 +222,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
</> </>
)} )}
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />} {currentPage === 'theme' && <ThemeCommands />}
{currentPage === 'language' && <LanguageCommands />} {currentPage === 'language' && <LanguageCommands />}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
@ -256,19 +251,18 @@ const Commands = ({
)); ));
}; };
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => { const ThemeCommands = () => {
const { _ } = useLingui(); const { _ } = useLingui();
const THEMES = useMemo( const [, setTheme] = useTheme();
() => [
{ label: msg`Light Mode`, theme: THEMES_TYPE.LIGHT, icon: Sun },
{ label: msg`Dark Mode`, theme: THEMES_TYPE.DARK, icon: Moon },
{ label: msg`System Theme`, theme: THEMES_TYPE.SYSTEM, icon: Monitor },
],
[],
);
return THEMES.map((theme) => ( const themes = [
{ label: msg`Light Mode`, theme: Theme.LIGHT, icon: Sun },
{ label: msg`Dark Mode`, theme: Theme.DARK, icon: Moon },
{ label: msg`System Theme`, theme: null, icon: Monitor },
] as const;
return themes.map((theme) => (
<CommandItem <CommandItem
key={theme.theme} key={theme.theme}
onSelect={() => setTheme(theme.theme)} onSelect={() => setTheme(theme.theme)}
@ -294,9 +288,23 @@ const LanguageCommands = () => {
setIsLoading(true); setIsLoading(true);
try { try {
await dynamicActivate(i18n, lang); await dynamicActivate(lang);
await switchI18NLanguage(lang);
} catch (err) { const formData = new FormData();
formData.append('lang', lang);
const response = await fetch('/api/locale', {
method: 'post',
body: formData,
});
if (!response.ok) {
throw new Error(response.statusText);
}
} catch (e) {
console.error(`Failed to set language: ${e}`);
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
variant: 'destructive', variant: 'destructive',

View File

@ -1,32 +1,28 @@
'use client';
import { type HTMLAttributes, useEffect, useState } from 'react'; import { type HTMLAttributes, useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { usePathname } from 'next/navigation';
import { MenuIcon, SearchIcon } from 'lucide-react'; import { MenuIcon, SearchIcon } from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams'; import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getRootHref } from '@documenso/lib/utils/params'; import { getRootHref } from '@documenso/lib/utils/params';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Logo } from '~/components/branding/logo'; import { BrandingLogo } from '~/components/general/branding-logo';
import { CommandMenu } from '../common/command-menu'; import { AppCommandMenu } from './app-command-menu';
import { DesktopNav } from './desktop-nav'; import { AppNavDesktop } from './app-nav-desktop';
import { AppNavMobile } from './app-nav-mobile';
import { MenuSwitcher } from './menu-switcher'; import { MenuSwitcher } from './menu-switcher';
import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes<HTMLDivElement> & { export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
user: User; user: SessionUser;
teams: TGetTeamsResponse; teams: TGetTeamsResponse;
}; };
export const Header = ({ className, user, teams, ...props }: HeaderProps) => { export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
const params = useParams(); const params = useParams();
const { pathname } = useLocation();
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false); const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false); const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
@ -42,8 +38,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
return () => window.removeEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll);
}, []); }, []);
const pathname = usePathname();
const isPathTeamUrl = (teamUrl: string) => { const isPathTeamUrl = (teamUrl: string) => {
if (!pathname || !pathname.startsWith(`/t/`)) { if (!pathname || !pathname.startsWith(`/t/`)) {
return false; return false;
@ -65,13 +59,13 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
> >
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8"> <div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link <Link
href={`${getRootHref(params, { returnEmptyRootString: true })}/documents`} to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline" className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
> >
<Logo className="h-6 w-auto" /> <BrandingLogo className="h-6 w-auto" />
</Link> </Link>
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} /> <AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
<div <div
className="flex gap-x-4 md:ml-8" className="flex gap-x-4 md:ml-8"
@ -89,9 +83,9 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
<MenuIcon className="text-muted-foreground h-6 w-6" /> <MenuIcon className="text-muted-foreground h-6 w-6" />
</button> </button>
<CommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} /> <AppCommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
<MobileNavigation <AppNavMobile
isMenuOpen={isHamburgerMenuOpen} isMenuOpen={isHamburgerMenuOpen}
onMenuOpenChange={setIsHamburgerMenuOpen} onMenuOpenChange={setIsHamburgerMenuOpen}
/> />

View File

@ -1,12 +1,11 @@
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Link from 'next/link'; import { msg } from '@lingui/core/macro';
import { useParams, usePathname } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router';
import { getRootHref } from '@documenso/lib/utils/params'; import { getRootHref } from '@documenso/lib/utils/params';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -23,14 +22,18 @@ const navigationLinks = [
}, },
]; ];
export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & { export type AppNavDesktopProps = HTMLAttributes<HTMLDivElement> & {
setIsCommandMenuOpen: (value: boolean) => void; setIsCommandMenuOpen: (value: boolean) => void;
}; };
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => { export const AppNavDesktop = ({
className,
setIsCommandMenuOpen,
...props
}: AppNavDesktopProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const pathname = usePathname(); const { pathname } = useLocation();
const params = useParams(); const params = useParams();
const [modifierKey, setModifierKey] = useState(() => 'Ctrl'); const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
@ -56,7 +59,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
{navigationLinks.map(({ href, label }) => ( {navigationLinks.map(({ href, label }) => (
<Link <Link
key={href} key={href}
href={`${rootHref}${href}`} to={`${rootHref}${href}`}
className={cn( className={cn(
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2', 'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
{ {
@ -82,7 +85,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
</div> </div>
<div> <div>
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider"> <div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
{modifierKey}+K {modifierKey}+K
</div> </div>
</div> </div>

View File

@ -1,24 +1,20 @@
'use client'; import { msg } from '@lingui/core/macro';
import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import { Link, useParams } from 'react-router';
import LogoImage from '@documenso/assets/logo.png'; import LogoImage from '@documenso/assets/logo.png';
import { authClient } from '@documenso/auth/client';
import { getRootHref } from '@documenso/lib/utils/params'; import { getRootHref } from '@documenso/lib/utils/params';
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet'; import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher'; import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
export type MobileNavigationProps = { export type AppNavMobileProps = {
isMenuOpen: boolean; isMenuOpen: boolean;
onMenuOpenChange?: (_value: boolean) => void; onMenuOpenChange?: (_value: boolean) => void;
}; };
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => { export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const params = useParams(); const params = useParams();
@ -51,8 +47,8 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
return ( return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}> <Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="flex w-full max-w-[350px] flex-col"> <SheetContent className="flex w-full max-w-[350px] flex-col">
<Link href="/" onClick={handleMenuItemClick}> <Link to="/" onClick={handleMenuItemClick}>
<Image <img
src={LogoImage} src={LogoImage}
alt="Documenso Logo" alt="Documenso Logo"
className="dark:invert" className="dark:invert"
@ -66,7 +62,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link <Link
key={href} key={href}
className="text-foreground hover:text-foreground/80 text-2xl font-semibold" className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
href={href} to={href}
onClick={() => handleMenuItemClick()} onClick={() => handleMenuItemClick()}
> >
{_(text)} {_(text)}
@ -75,11 +71,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<button <button
className="text-foreground hover:text-foreground/80 text-2xl font-semibold" className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
onClick={async () => onClick={async () => authClient.signOut()}
signOut({
callbackUrl: '/',
})
}
> >
<Trans>Sign Out</Trans> <Trans>Sign Out</Trans>
</button> </button>

View File

@ -1,17 +1,13 @@
'use client'; import { msg } from '@lingui/core/macro';
import React from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';

View File

@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
export type LogoProps = SVGAttributes<SVGSVGElement>; export type LogoProps = SVGAttributes<SVGSVGElement>;
export const Logo = ({ ...props }: LogoProps) => { export const BrandingLogo = ({ ...props }: LogoProps) => {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
<path <path

View File

@ -1,16 +1,14 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -25,7 +23,7 @@ import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { signupErrorMessages } from '~/components/forms/v2/signup'; import { signupErrorMessages } from '~/components/forms/signup';
export type ClaimAccountProps = { export type ClaimAccountProps = {
defaultName: string; defaultName: string;
@ -60,9 +58,7 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const { toast } = useToast(); const { toast } = useToast();
const analytics = useAnalytics(); const analytics = useAnalytics();
const router = useRouter(); const navigate = useNavigate();
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
const form = useForm<TClaimAccountFormSchema>({ const form = useForm<TClaimAccountFormSchema>({
values: { values: {
@ -75,9 +71,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => { const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
try { try {
await signup({ name, email, password }); await authClient.emailPassword.signUp({ name, email, password });
router.push(`/unverified-account`); await navigate(`/unverified-account`);
toast({ toast({
title: _(msg`Registration Successful`), title: _(msg`Registration Successful`),

View File

@ -1,15 +1,14 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { useSession } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import type { Field } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { TTemplate } from '@documenso/lib/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
import type { Recipient } from '@documenso/prisma/client';
import type { Field } from '@documenso/prisma/client';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
@ -30,36 +29,37 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useStep } from '@documenso/ui/primitives/stepper'; import { useStep } from '@documenso/ui/primitives/stepper';
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
const ZConfigureDirectTemplateFormSchema = z.object({ const ZDirectTemplateConfigureFormSchema = z.object({
email: z.string().email('Email is invalid'), email: z.string().email('Email is invalid'),
}); });
export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirectTemplateFormSchema>; export type TDirectTemplateConfigureFormSchema = z.infer<typeof ZDirectTemplateConfigureFormSchema>;
export type ConfigureDirectTemplateFormProps = { export type DirectTemplateConfigureFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
isDocumentPdfLoaded: boolean; isDocumentPdfLoaded: boolean;
template: Omit<TTemplate, 'user'>; template: Omit<TTemplate, 'user'>;
directTemplateRecipient: Recipient & { fields: Field[] }; directTemplateRecipient: Recipient & { fields: Field[] };
initialEmail?: string; initialEmail?: string;
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void; onSubmit: (_data: TDirectTemplateConfigureFormSchema) => void;
}; };
export const ConfigureDirectTemplateFormPartial = ({ export const DirectTemplateConfigureForm = ({
flowStep, flowStep,
isDocumentPdfLoaded, isDocumentPdfLoaded,
template, template,
directTemplateRecipient, directTemplateRecipient,
initialEmail, initialEmail,
onSubmit, onSubmit,
}: ConfigureDirectTemplateFormProps) => { }: DirectTemplateConfigureFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { data: session } = useSession();
const { user } = useOptionalSession();
const { recipients } = template; const { recipients } = template;
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext(); const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => { const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
if (recipient.id === directTemplateRecipient.id) { if (recipient.id === directTemplateRecipient.id) {
@ -72,9 +72,9 @@ export const ConfigureDirectTemplateFormPartial = ({
return recipient; return recipient;
}); });
const form = useForm<TConfigureDirectTemplateFormSchema>({ const form = useForm<TDirectTemplateConfigureFormSchema>({
resolver: zodResolver( resolver: zodResolver(
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => { ZDirectTemplateConfigureFormSchema.superRefine((items, ctx) => {
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) { if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@ -125,7 +125,7 @@ export const ConfigureDirectTemplateFormPartial = ({
disabled={ disabled={
field.disabled || field.disabled ||
derivedRecipientAccessAuth !== null || derivedRecipientAccessAuth !== null ||
session?.user.email !== undefined user?.email !== undefined
} }
placeholder="recipient@documenso.com" placeholder="recipient@documenso.com"
/> />

View File

@ -1,16 +1,13 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Field } from '@prisma/client';
import { type Recipient } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TTemplate } from '@documenso/lib/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
import type { Field } from '@documenso/prisma/client';
import { type Recipient } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root'; import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
@ -19,15 +16,19 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper'; import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
import type { TConfigureDirectTemplateFormSchema } from './configure-direct-template'; import {
import { ConfigureDirectTemplateFormPartial } from './configure-direct-template'; DirectTemplateConfigureForm,
import type { DirectTemplateLocalField } from './sign-direct-template'; type TDirectTemplateConfigureFormSchema,
import { SignDirectTemplateForm } from './sign-direct-template'; } from './direct-template-configure-form';
import {
type DirectTemplateLocalField,
DirectTemplateSigningForm,
} from './direct-template-signing-form';
export type TemplatesDirectPageViewProps = { export type DirectTemplatePageViewProps = {
template: Omit<TTemplate, 'user'>; template: Omit<TTemplate, 'user'>;
directTemplateToken: string; directTemplateToken: string;
directTemplateRecipient: Recipient & { fields: Field[] }; directTemplateRecipient: Recipient & { fields: Field[] };
@ -40,15 +41,15 @@ export const DirectTemplatePageView = ({
template, template,
directTemplateRecipient, directTemplateRecipient,
directTemplateToken, directTemplateToken,
}: TemplatesDirectPageViewProps) => { }: DirectTemplatePageViewProps) => {
const router = useRouter(); const navigate = useNavigate();
const searchParams = useSearchParams(); const [searchParams] = useSearchParams();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { email, fullName, setEmail } = useRequiredSigningContext(); const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
const { recipient, setRecipient } = useRequiredDocumentAuthContext(); const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
const [step, setStep] = useState<DirectTemplateStep>('configure'); const [step, setStep] = useState<DirectTemplateStep>('configure');
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false); const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
@ -76,7 +77,7 @@ export const DirectTemplatePageView = ({
/** /**
* Set the email into a temporary recipient so it can be used for reauth and signing email fields. * Set the email into a temporary recipient so it can be used for reauth and signing email fields.
*/ */
const onConfigureDirectTemplateSubmit = ({ email }: TConfigureDirectTemplateFormSchema) => { const onConfigureDirectTemplateSubmit = ({ email }: TDirectTemplateConfigureFormSchema) => {
setEmail(email); setEmail(email);
setRecipient({ setRecipient({
@ -112,7 +113,7 @@ export const DirectTemplatePageView = ({
const redirectUrl = template.templateMeta?.redirectUrl; const redirectUrl = template.templateMeta?.redirectUrl;
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${token}/complete`); await (redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${token}/complete`));
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@ -152,7 +153,7 @@ export const DirectTemplatePageView = ({
currentStep={currentDocumentFlow.stepIndex} currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])} setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
> >
<ConfigureDirectTemplateFormPartial <DirectTemplateConfigureForm
flowStep={directTemplateFlow.configure} flowStep={directTemplateFlow.configure}
template={template} template={template}
directTemplateRecipient={directTemplateRecipient} directTemplateRecipient={directTemplateRecipient}
@ -161,7 +162,7 @@ export const DirectTemplatePageView = ({
initialEmail={email} initialEmail={email}
/> />
<SignDirectTemplateForm <DirectTemplateSigningForm
flowStep={directTemplateFlow.sign} flowStep={directTemplateFlow.sign}
directRecipient={recipient} directRecipient={recipient}
directRecipientFields={directTemplateRecipient.fields} directRecipientFields={directTemplateRecipient.fields}

View File

@ -1,11 +1,10 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import { authClient } from '@documenso/auth/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -19,9 +18,7 @@ export const DirectTemplateAuthPageView = () => {
try { try {
setIsSigningOut(true); setIsSigningOut(true);
await signOut({ await authClient.signOut();
callbackUrl: '/signin',
});
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),

View File

@ -1,6 +1,8 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import type { Field, Recipient, Signature } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -16,8 +18,6 @@ import {
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import type { TTemplate } from '@documenso/lib/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { Field, Recipient, Signature } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import type { import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
@ -38,20 +38,20 @@ import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useStep } from '@documenso/ui/primitives/stepper'; import { useStep } from '@documenso/ui/primitives/stepper';
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field'; import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
import { DateField } from '~/app/(signing)/sign/[token]/date-field'; import { DocumentSigningCompleteDialog } from '~/components/general/document-signing/document-signing-complete-dialog';
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field'; import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
import { EmailField } from '~/app/(signing)/sign/[token]/email-field'; import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field'; import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
import { NameField } from '~/app/(signing)/sign/[token]/name-field'; import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
import { NumberField } from '~/app/(signing)/sign/[token]/number-field'; import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field'; import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog'; import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
export type SignDirectTemplateFormProps = { export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
directRecipient: Recipient; directRecipient: Recipient;
directRecipientFields: Field[]; directRecipientFields: Field[];
@ -64,15 +64,15 @@ export type DirectTemplateLocalField = Field & {
signature?: Signature; signature?: Signature;
}; };
export const SignDirectTemplateForm = ({ export const DirectTemplateSigningForm = ({
flowStep, flowStep,
directRecipient, directRecipient,
directRecipientFields, directRecipientFields,
template, template,
onSubmit, onSubmit,
}: SignDirectTemplateFormProps) => { }: DirectTemplateSigningFormProps) => {
const { fullName, signature, signatureValid, setFullName, setSignature } = const { fullName, signature, signatureValid, setFullName, setSignature } =
useRequiredSigningContext(); useRequiredDocumentSigningContext();
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields); const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -183,7 +183,7 @@ export const SignDirectTemplateForm = ({
{localFields.map((field) => {localFields.map((field) =>
match(field.type) match(field.type)
.with(FieldType.SIGNATURE, () => ( .with(FieldType.SIGNATURE, () => (
<SignatureField <DocumentSigningSignatureField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -192,7 +192,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (
<InitialsField <DocumentSigningInitialsField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -201,7 +201,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.NAME, () => ( .with(FieldType.NAME, () => (
<NameField <DocumentSigningNameField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -210,7 +210,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField <DocumentSigningDateField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -221,7 +221,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField <DocumentSigningEmailField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -235,7 +235,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<TextField <DocumentSigningTextField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -253,7 +253,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<NumberField <DocumentSigningNumberField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -271,7 +271,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<DropdownField <DocumentSigningDropdownField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -289,7 +289,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<RadioField <DocumentSigningRadioField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -307,7 +307,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<CheckboxField <DocumentSigningCheckboxField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -373,7 +373,7 @@ export const SignDirectTemplateForm = ({
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
<SignDialog <DocumentSigningCompleteDialog
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit} onSignatureComplete={handleSubmit}
documentTitle={template.title} documentTitle={template.title}

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { RecipientRole } from '@documenso/prisma/client';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { DialogFooter } from '@documenso/ui/primitives/dialog'; import { DialogFooter } from '@documenso/ui/primitives/dialog';
@ -23,9 +23,9 @@ import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog'; import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type DocumentActionAuth2FAProps = { export type DocumentSigningAuth2FAProps = {
actionTarget?: 'FIELD' | 'DOCUMENT'; actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string; actionVerb?: string;
open: boolean; open: boolean;
@ -42,15 +42,15 @@ const Z2FAAuthFormSchema = z.object({
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>; type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
export const DocumentActionAuth2FA = ({ export const DocumentSigningAuth2FA = ({
actionTarget = 'FIELD', actionTarget = 'FIELD',
actionVerb = 'sign', actionVerb = 'sign',
onReauthFormSubmit, onReauthFormSubmit,
open, open,
onOpenChange, onOpenChange,
}: DocumentActionAuth2FAProps) => { }: DocumentSigningAuth2FAProps) => {
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } = const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
useRequiredDocumentAuthContext(); useRequiredDocumentSigningAuthContext();
const form = useForm<T2FAAuthFormSchema>({ const form = useForm<T2FAAuthFormSchema>({
resolver: zodResolver(Z2FAAuthFormSchema), resolver: zodResolver(Z2FAAuthFormSchema),
@ -109,17 +109,14 @@ export const DocumentActionAuth2FA = ({
)} )}
</p> </p>
{user?.identityProvider === 'DOCUMENSO' && ( <p className="mt-2">
<p className="mt-2"> <Trans>
<Trans> By enabling 2FA, you will be required to enter a code from your authenticator app
By enabling 2FA, you will be required to enter a code from your authenticator app every time you sign in using email password.
every time you sign in. </Trans>
</Trans> </p>
</p>
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<DialogFooter> <DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}> <Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Close</Trans> <Trans>Close</Trans>

View File

@ -1,31 +1,32 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { Trans, useLingui } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { Trans } from '@lingui/macro'; import { authClient } from '@documenso/auth/client';
import { signOut } from 'next-auth/react';
import { RecipientRole } from '@documenso/prisma/client';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { DialogFooter } from '@documenso/ui/primitives/dialog'; import { DialogFooter } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type DocumentActionAuthAccountProps = { export type DocumentSigningAuthAccountProps = {
actionTarget?: 'FIELD' | 'DOCUMENT'; actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string; actionVerb?: string;
onOpenChange: (value: boolean) => void; onOpenChange: (value: boolean) => void;
}; };
export const DocumentActionAuthAccount = ({ export const DocumentSigningAuthAccount = ({
actionTarget = 'FIELD', actionTarget = 'FIELD',
actionVerb = 'sign', actionVerb = 'sign',
onOpenChange, onOpenChange,
}: DocumentActionAuthAccountProps) => { }: DocumentSigningAuthAccountProps) => {
const { recipient } = useRequiredDocumentAuthContext(); const { recipient } = useRequiredDocumentSigningAuthContext();
const router = useRouter(); const { t } = useLingui();
const { toast } = useToast();
const [isSigningOut, setIsSigningOut] = useState(false); const [isSigningOut, setIsSigningOut] = useState(false);
@ -33,15 +34,18 @@ export const DocumentActionAuthAccount = ({
try { try {
setIsSigningOut(true); setIsSigningOut(true);
await signOut({ await authClient.signOut({
redirect: false, redirectPath: `/signin#email=${email}`,
}); });
router.push(`/signin#email=${email}`);
} catch { } catch {
setIsSigningOut(false); setIsSigningOut(false);
// Todo: Alert. toast({
title: t`Something went wrong`,
description: t`We were unable to log you out at this time.`,
duration: 10000,
variant: 'destructive',
});
} }
}; };

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import type { FieldType } from '@prisma/client';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { import {
@ -6,7 +7,6 @@ import {
type TRecipientActionAuth, type TRecipientActionAuth,
type TRecipientActionAuthTypes, type TRecipientActionAuthTypes,
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import type { FieldType } from '@documenso/prisma/client';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -15,12 +15,12 @@ import {
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { DocumentActionAuth2FA } from './document-action-auth-2fa'; import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
import { DocumentActionAuthAccount } from './document-action-auth-account'; import { DocumentSigningAuthAccount } from './document-signing-auth-account';
import { DocumentActionAuthPasskey } from './document-action-auth-passkey'; import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type DocumentActionAuthDialogProps = { export type DocumentSigningAuthDialogProps = {
title?: string; title?: string;
documentAuthType: TRecipientActionAuthTypes; documentAuthType: TRecipientActionAuthTypes;
description?: string; description?: string;
@ -34,15 +34,15 @@ export type DocumentActionAuthDialogProps = {
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void; onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
}; };
export const DocumentActionAuthDialog = ({ export const DocumentSigningAuthDialog = ({
title, title,
description, description,
documentAuthType, documentAuthType,
open, open,
onOpenChange, onOpenChange,
onReauthFormSubmit, onReauthFormSubmit,
}: DocumentActionAuthDialogProps) => { }: DocumentSigningAuthDialogProps) => {
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentAuthContext(); const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
const handleOnOpenChange = (value: boolean) => { const handleOnOpenChange = (value: boolean) => {
if (isCurrentlyAuthenticating) { if (isCurrentlyAuthenticating) {
@ -67,17 +67,17 @@ export const DocumentActionAuthDialog = ({
.with( .with(
{ documentAuthType: DocumentAuth.ACCOUNT }, { documentAuthType: DocumentAuth.ACCOUNT },
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in. { user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
() => <DocumentActionAuthAccount onOpenChange={onOpenChange} />, () => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
) )
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => ( .with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
<DocumentActionAuthPasskey <DocumentSigningAuthPasskey
open={open} open={open}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
onReauthFormSubmit={onReauthFormSubmit} onReauthFormSubmit={onReauthFormSubmit}
/> />
)) ))
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => ( .with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
<DocumentActionAuth2FA <DocumentSigningAuth2FA
open={open} open={open}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
onReauthFormSubmit={onReauthFormSubmit} onReauthFormSubmit={onReauthFormSubmit}

View File

@ -1,38 +1,34 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react'; import { Trans } from '@lingui/react/macro';
import { authClient } from '@documenso/auth/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type SigningAuthPageViewProps = { export type DocumentSigningAuthPageViewProps = {
email: string; email: string;
emailHasAccount?: boolean; emailHasAccount?: boolean;
}; };
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => { export const DocumentSigningAuthPageView = ({
email,
emailHasAccount,
}: DocumentSigningAuthPageViewProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const [isSigningOut, setIsSigningOut] = useState(false); const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email: string) => { const handleChangeAccount = async (email: string) => {
try { try {
setIsSigningOut(true); setIsSigningOut(true);
await signOut({ await authClient.signOut({
redirect: false, redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
}); });
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),

View File

@ -1,8 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser'; import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@ -10,7 +12,6 @@ import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -31,11 +32,11 @@ import {
SelectValue, SelectValue,
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { CreatePasskeyDialog } from '~/app/(dashboard)/settings/security/passkeys/create-passkey-dialog'; import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type DocumentActionAuthPasskeyProps = { export type DocumentSigningAuthPasskeyProps = {
actionTarget?: 'FIELD' | 'DOCUMENT'; actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string; actionVerb?: string;
open: boolean; open: boolean;
@ -49,13 +50,13 @@ const ZPasskeyAuthFormSchema = z.object({
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>; type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
export const DocumentActionAuthPasskey = ({ export const DocumentSigningAuthPasskey = ({
actionTarget = 'FIELD', actionTarget = 'FIELD',
actionVerb = 'sign', actionVerb = 'sign',
onReauthFormSubmit, onReauthFormSubmit,
open, open,
onOpenChange, onOpenChange,
}: DocumentActionAuthPasskeyProps) => { }: DocumentSigningAuthPasskeyProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { const {
@ -66,7 +67,7 @@ export const DocumentActionAuthPasskey = ({
isCurrentlyAuthenticating, isCurrentlyAuthenticating,
setIsCurrentlyAuthenticating, setIsCurrentlyAuthenticating,
refetchPasskeys, refetchPasskeys,
} = useRequiredDocumentAuthContext(); } = useRequiredDocumentSigningAuthContext();
const form = useForm<TPasskeyAuthFormSchema>({ const form = useForm<TPasskeyAuthFormSchema>({
resolver: zodResolver(ZPasskeyAuthFormSchema), resolver: zodResolver(ZPasskeyAuthFormSchema),
@ -189,7 +190,7 @@ export const DocumentActionAuthPasskey = ({
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<CreatePasskeyDialog <PasskeyCreateDialog
onSuccess={async () => refetchPasskeys()} onSuccess={async () => refetchPasskeys()}
trigger={ trigger={
<Button> <Button>

View File

@ -1,9 +1,9 @@
'use client';
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
import type { import type {
TDocumentAuthOptions, TDocumentAuthOptions,
@ -13,17 +13,10 @@ import type {
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { DocumentAuth } from '@documenso/lib/types/document-auth'; import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import {
type Document,
FieldType,
type Passkey,
type Recipient,
type User,
} from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { DocumentActionAuthDialogProps } from './document-action-auth-dialog'; import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog';
import { DocumentActionAuthDialog } from './document-action-auth-dialog'; import { DocumentSigningAuthDialog } from './document-signing-auth-dialog';
type PasskeyData = { type PasskeyData = {
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[]; passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
@ -32,7 +25,7 @@ type PasskeyData = {
isError: boolean; isError: boolean;
}; };
export type DocumentAuthContextValue = { export type DocumentSigningAuthContextValue = {
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>; executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
documentAuthOptions: Document['authOptions']; documentAuthOptions: Document['authOptions'];
documentAuthOption: TDocumentAuthOptions; documentAuthOption: TDocumentAuthOptions;
@ -48,39 +41,39 @@ export type DocumentAuthContextValue = {
passkeyData: PasskeyData; passkeyData: PasskeyData;
preferredPasskeyId: string | null; preferredPasskeyId: string | null;
setPreferredPasskeyId: (_value: string | null) => void; setPreferredPasskeyId: (_value: string | null) => void;
user?: User | null; user?: SessionUser | null;
refetchPasskeys: () => Promise<void>; refetchPasskeys: () => Promise<void>;
}; };
const DocumentAuthContext = createContext<DocumentAuthContextValue | null>(null); const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
export const useDocumentAuthContext = () => { export const useDocumentSigningAuthContext = () => {
return useContext(DocumentAuthContext); return useContext(DocumentSigningAuthContext);
}; };
export const useRequiredDocumentAuthContext = () => { export const useRequiredDocumentSigningAuthContext = () => {
const context = useDocumentAuthContext(); const context = useDocumentSigningAuthContext();
if (!context) { if (!context) {
throw new Error('Document auth context is required'); throw new Error('Document signing auth context is required');
} }
return context; return context;
}; };
export interface DocumentAuthProviderProps { export interface DocumentSigningAuthProviderProps {
documentAuthOptions: Document['authOptions']; documentAuthOptions: Document['authOptions'];
recipient: Recipient; recipient: Recipient;
user?: User | null; user?: SessionUser | null;
children: React.ReactNode; children: React.ReactNode;
} }
export const DocumentAuthProvider = ({ export const DocumentSigningAuthProvider = ({
documentAuthOptions: initialDocumentAuthOptions, documentAuthOptions: initialDocumentAuthOptions,
recipient: initialRecipient, recipient: initialRecipient,
user, user,
children, children,
}: DocumentAuthProviderProps) => { }: DocumentSigningAuthProviderProps) => {
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions); const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
const [recipient, setRecipient] = useState(initialRecipient); const [recipient, setRecipient] = useState(initialRecipient);
@ -186,7 +179,7 @@ export const DocumentAuthProvider = ({
}; };
return ( return (
<DocumentAuthContext.Provider <DocumentSigningAuthContext.Provider
value={{ value={{
user, user,
documentAuthOptions, documentAuthOptions,
@ -210,7 +203,7 @@ export const DocumentAuthProvider = ({
{children} {children}
{documentAuthDialogPayload && derivedRecipientActionAuth && ( {documentAuthDialogPayload && derivedRecipientActionAuth && (
<DocumentActionAuthDialog <DocumentSigningAuthDialog
open={true} open={true}
onOpenChange={() => setDocumentAuthDialogPayload(null)} onOpenChange={() => setDocumentAuthDialogPayload(null)}
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit} onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
@ -218,13 +211,13 @@ export const DocumentAuthProvider = ({
documentAuthType={derivedRecipientActionAuth} documentAuthType={derivedRecipientActionAuth}
/> />
)} )}
</DocumentAuthContext.Provider> </DocumentSigningAuthContext.Provider>
); );
}; };
type ExecuteActionAuthProcedureOptions = Omit< type ExecuteActionAuthProcedureOptions = Omit<
DocumentActionAuthDialogProps, DocumentSigningAuthDialogProps,
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' 'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
>; >;
DocumentAuthProvider.displayName = 'DocumentAuthProvider'; DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';

View File

@ -1,19 +1,17 @@
'use client'; import { useState } from 'react';
import { useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Plural, Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once'; import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { DocumentAuth } from '@documenso/lib/types/document-auth'; import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -27,10 +25,10 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { Form } from '@documenso/ui/primitives/form/form'; import { Form } from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { SigningDisclosure } from '~/components/general/signing-disclosure'; import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { useRequiredSigningContext } from './provider'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [ const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
FieldType.NAME, FieldType.NAME,
@ -55,22 +53,20 @@ const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields. // while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
const AUTO_SIGN_THRESHOLD = 5; const AUTO_SIGN_THRESHOLD = 5;
export type AutoSignProps = { export type DocumentSigningAutoSignProps = {
recipient: Pick<Recipient, 'id' | 'token'>; recipient: Pick<Recipient, 'id' | 'token'>;
fields: Field[]; fields: Field[];
}; };
export const AutoSign = ({ recipient, fields }: AutoSignProps) => { export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAutoSignProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter(); const { email, fullName } = useRequiredDocumentSigningContext();
const { derivedRecipientActionAuth } = useRequiredDocumentSigningAuthContext();
const { email, fullName } = useRequiredSigningContext();
const { derivedRecipientActionAuth } = useRequiredDocumentAuthContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const form = useForm(); const form = useForm();
@ -158,11 +154,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
}); });
} }
startTransition(() => { await revalidate();
router.refresh();
setOpen(false);
});
}; };
unsafe_useEffectOnce(() => { unsafe_useEffectOnce(() => {
@ -205,7 +197,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
</ul> </ul>
</div> </div>
<SigningDisclosure className="mt-4" /> <DocumentSigningDisclosure className="mt-4" />
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}> <form onSubmit={form.handleSubmit(onSubmit)}>
@ -223,7 +215,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
<Button <Button
type="submit" type="submit"
className="min-w-[6rem]" className="min-w-[6rem]"
loading={form.formState.isSubmitting || isPending} loading={form.formState.isSubmitting}
disabled={!autoSignableFields.length} disabled={!autoSignableFields.length}
> >
<Trans>Sign</Trans> <Trans>Sign</Trans>

View File

@ -1,19 +1,16 @@
'use client'; import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta'; import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox'; import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -26,28 +23,27 @@ import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { SigningFieldContainer } from './signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
export type CheckboxFieldProps = { export type DocumentSigningCheckboxFieldProps = {
field: FieldWithSignatureAndFieldMeta; field: FieldWithSignatureAndFieldMeta;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const CheckboxField = ({ export const DocumentSigningCheckboxField = ({
field, field,
recipient, recipient,
onSignField, onSignField,
onUnsignField, onUnsignField,
}: CheckboxFieldProps) => { }: DocumentSigningCheckboxFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const [isPending, startTransition] = useTransition();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta); const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
@ -89,7 +85,7 @@ export const CheckboxField = ({
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField = const shouldAutoSignField =
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) || (!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
(!field.inserted && isReadOnly && isLengthConditionMet); (!field.inserted && isReadOnly && isLengthConditionMet);
@ -110,7 +106,7 @@ export const CheckboxField = ({
await signFieldWithToken(payload); await signFieldWithToken(payload);
} }
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -145,7 +141,7 @@ export const CheckboxField = ({
setCheckedValues([]); setCheckedValues([]);
} }
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -217,7 +213,7 @@ export const CheckboxField = ({
}); });
} finally { } finally {
setCheckedValues(updatedValues); setCheckedValues(updatedValues);
startTransition(() => router.refresh()); await revalidate();
} }
}; };
@ -236,7 +232,12 @@ export const CheckboxField = ({
); );
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox"> <DocumentSigningFieldContainer
field={field}
onSign={onSign}
onRemove={onRemove}
type="Checkbox"
>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md"> <div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
@ -294,6 +295,6 @@ export const CheckboxField = ({
})} })}
</div> </div>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -1,10 +1,10 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import type { Field } from '@prisma/client';
import { RecipientRole } from '@prisma/client';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import type { Field } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@ -14,9 +14,9 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { SigningDisclosure } from '~/components/general/signing-disclosure'; import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
export type SignDialogProps = { export type DocumentSigningCompleteDialogProps = {
isSubmitting: boolean; isSubmitting: boolean;
documentTitle: string; documentTitle: string;
fields: Field[]; fields: Field[];
@ -26,7 +26,7 @@ export type SignDialogProps = {
disabled?: boolean; disabled?: boolean;
}; };
export const SignDialog = ({ export const DocumentSigningCompleteDialog = ({
isSubmitting, isSubmitting,
documentTitle, documentTitle,
fields, fields,
@ -34,7 +34,7 @@ export const SignDialog = ({
onSignatureComplete, onSignatureComplete,
role, role,
disabled = false, disabled = false,
}: SignDialogProps) => { }: DocumentSigningCompleteDialogProps) => {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]); const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
@ -116,7 +116,7 @@ export const SignDialog = ({
)} )}
</div> </div>
<SigningDisclosure className="mt-4" /> <DocumentSigningDisclosure className="mt-4" />
<DialogFooter> <DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">

View File

@ -1,12 +1,9 @@
'use client'; import { msg } from '@lingui/core/macro';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { import {
DEFAULT_DOCUMENT_DATE_FORMAT, DEFAULT_DOCUMENT_DATE_FORMAT,
@ -16,7 +13,6 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -25,9 +21,9 @@ import type {
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { SigningFieldContainer } from './signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
export type DateFieldProps = { export type DocumentSigningDateFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient; recipient: Recipient;
dateFormat?: string | null; dateFormat?: string | null;
@ -36,20 +32,17 @@ export type DateFieldProps = {
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const DateField = ({ export const DocumentSigningDateField = ({
field, field,
recipient, recipient,
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT, dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
timezone = DEFAULT_DOCUMENT_TIME_ZONE, timezone = DEFAULT_DOCUMENT_TIME_ZONE,
onSignField, onSignField,
onUnsignField, onUnsignField,
}: DateFieldProps) => { }: DocumentSigningDateFieldProps) => {
const router = useRouter();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@ -59,14 +52,14 @@ export const DateField = ({
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
const isDifferentTime = field.inserted && localDateString !== field.customText; const isDifferentTime = field.inserted && localDateString !== field.customText;
const tooltipText = _( const tooltipText = _(
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`, msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone || ''}".`,
); );
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
@ -85,7 +78,7 @@ export const DateField = ({
await signFieldWithToken(payload); await signFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -117,7 +110,7 @@ export const DateField = ({
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -130,7 +123,7 @@ export const DateField = ({
}; };
return ( return (
<SigningFieldContainer <DocumentSigningFieldContainer
field={field} field={field}
onSign={onSign} onSign={onSign}
onRemove={onRemove} onRemove={onRemove}
@ -154,6 +147,6 @@ export const DateField = ({
{localDateString} {localDateString}
</p> </p>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -1,14 +1,16 @@
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import Link from 'next/link'; import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
import { Trans } from '@lingui/macro';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
export type SigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>; export type DocumentSigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => { export const DocumentSigningDisclosure = ({
className,
...props
}: DocumentSigningDisclosureProps) => {
return ( return (
<p className={cn('text-muted-foreground text-xs', className)} {...props}> <p className={cn('text-muted-foreground text-xs', className)} {...props}>
<Trans> <Trans>
@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
Read the full{' '} Read the full{' '}
<Link <Link
className="text-documenso-700 underline" className="text-documenso-700 underline"
href="/articles/signature-disclosure" to="/articles/signature-disclosure"
target="_blank" target="_blank"
> >
signature disclosure signature disclosure

View File

@ -1,18 +1,15 @@
'use client'; import { useEffect, useState } from 'react';
import { useEffect, useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta'; import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -29,29 +26,27 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { SigningFieldContainer } from './signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
export type DropdownFieldProps = { export type DocumentSigningDropdownFieldProps = {
field: FieldWithSignatureAndFieldMeta; field: FieldWithSignatureAndFieldMeta;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const DropdownField = ({ export const DocumentSigningDropdownField = ({
field, field,
recipient, recipient,
onSignField, onSignField,
onUnsignField, onUnsignField,
}: DropdownFieldProps) => { }: DocumentSigningDropdownFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const [isPending, startTransition] = useTransition();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta); const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const isReadOnly = parsedFieldMeta?.readOnly; const isReadOnly = parsedFieldMeta?.readOnly;
@ -66,7 +61,7 @@ export const DropdownField = ({
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField = const shouldAutoSignField =
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue); (!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
@ -91,7 +86,8 @@ export const DropdownField = ({
} }
setLocalChoice(''); setLocalChoice('');
startTransition(() => router.refresh());
await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -128,7 +124,8 @@ export const DropdownField = ({
} }
setLocalChoice(''); setLocalChoice('');
startTransition(() => router.refresh());
await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -164,7 +161,7 @@ export const DropdownField = ({
return ( return (
<div className="pointer-events-none"> <div className="pointer-events-none">
<SigningFieldContainer <DocumentSigningFieldContainer
field={field} field={field}
onPreSign={onPreSign} onPreSign={onPreSign}
onSign={onSign} onSign={onSign}
@ -210,7 +207,7 @@ export const DropdownField = ({
{field.customText} {field.customText}
</p> </p>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
</div> </div>
); );
}; };

View File

@ -1,17 +1,13 @@
'use client'; import { msg } from '@lingui/core/macro';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -20,25 +16,27 @@ import type {
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
export type EmailFieldProps = { export type DocumentSigningEmailFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => { export const DocumentSigningEmailField = ({
const router = useRouter(); field,
recipient,
onSignField,
onUnsignField,
}: DocumentSigningEmailFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const { email: providedEmail } = useRequiredSigningContext(); const { email: providedEmail } = useRequiredDocumentSigningContext();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@ -48,7 +46,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
try { try {
@ -69,7 +67,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await signFieldWithToken(payload); await signFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -101,7 +99,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -114,7 +112,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
}; };
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email"> <DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
@ -132,6 +130,6 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
{field.customText} {field.customText}
</p> </p>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -1,21 +1,19 @@
'use client';
import React from 'react'; import React from 'react';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { FieldRootContainer } from '@documenso/ui/components/field/field'; import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type SignatureFieldProps = { export type DocumentSigningFieldContainerProps = {
field: FieldWithSignature; field: FieldWithSignature;
loading?: boolean; loading?: boolean;
children: React.ReactNode; children: React.ReactNode;
@ -53,7 +51,7 @@ export type SignatureFieldProps = {
tooltipText?: string | null; tooltipText?: string | null;
}; };
export const SigningFieldContainer = ({ export const DocumentSigningFieldContainer = ({
field, field,
loading, loading,
onPreSign, onPreSign,
@ -62,8 +60,9 @@ export const SigningFieldContainer = ({
children, children,
type, type,
tooltipText, tooltipText,
}: SignatureFieldProps) => { }: DocumentSigningFieldContainerProps) => {
const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext(); const { executeActionAuthProcedure, isAuthRedirectRequired } =
useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined; const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
const readOnlyField = parsedFieldMeta?.readOnly || false; const readOnlyField = parsedFieldMeta?.readOnly || false;

View File

@ -1,19 +1,16 @@
'use client';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { Trans } from '@lingui/react/macro';
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
import { Trans } from '@lingui/macro';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token'; import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -23,10 +20,10 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useRequiredSigningContext } from './provider'; import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
import { SignDialog } from './sign-dialog'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
export type SigningFormProps = { export type DocumentSigningFormProps = {
document: DocumentAndSender; document: DocumentAndSender;
recipient: Recipient; recipient: Recipient;
fields: Field[]; fields: Field[];
@ -34,19 +31,20 @@ export type SigningFormProps = {
isRecipientsTurn: boolean; isRecipientsTurn: boolean;
}; };
export const SigningForm = ({ export const DocumentSigningForm = ({
document, document,
recipient, recipient,
fields, fields,
redirectUrl, redirectUrl,
isRecipientsTurn, isRecipientsTurn,
}: SigningFormProps) => { }: DocumentSigningFormProps) => {
const router = useRouter(); const navigate = useNavigate();
const analytics = useAnalytics(); const analytics = useAnalytics();
const { data: session } = useSession();
const { user } = useOptionalSession();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } = const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredSigningContext(); useRequiredDocumentSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -109,7 +107,11 @@ export const SigningForm = ({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`); if (redirectUrl) {
window.location.href = redirectUrl;
} else {
await navigate(`/sign/${recipient.token}/complete`);
}
}; };
return ( return (
@ -117,8 +119,8 @@ export const SigningForm = ({
className={cn( className={cn(
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6', 'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{ {
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session, 'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session, 'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
}, },
)} )}
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
@ -159,12 +161,12 @@ export const SigningForm = ({
variant="secondary" variant="secondary"
size="lg" size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1} disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()} onClick={async () => navigate(-1)}
> >
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<SignDialog <DocumentSigningCompleteDialog
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)} onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title} documentTitle={document.title}
@ -241,12 +243,12 @@ export const SigningForm = ({
variant="secondary" variant="secondary"
size="lg" size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1} disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()} onClick={async () => navigate(-1)}
> >
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<SignDialog <DocumentSigningCompleteDialog
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)} onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title} documentTitle={document.title}

View File

@ -1,18 +1,14 @@
'use client'; import { msg } from '@lingui/core/macro';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -21,31 +17,29 @@ import type {
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
export type InitialsFieldProps = { export type DocumentSigningInitialsFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const InitialsField = ({ export const DocumentSigningInitialsField = ({
field, field,
recipient, recipient,
onSignField, onSignField,
onUnsignField, onUnsignField,
}: InitialsFieldProps) => { }: DocumentSigningInitialsFieldProps) => {
const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { revalidate } = useRevalidator();
const { fullName } = useRequiredSigningContext(); const { fullName } = useRequiredDocumentSigningContext();
const initials = extractInitials(fullName); const initials = extractInitials(fullName);
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@ -54,7 +48,7 @@ export const InitialsField = ({
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
try { try {
@ -75,7 +69,7 @@ export const InitialsField = ({
await signFieldWithToken(payload); await signFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -107,7 +101,7 @@ export const InitialsField = ({
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -120,7 +114,12 @@ export const InitialsField = ({
}; };
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials"> <DocumentSigningFieldContainer
field={field}
onSign={onSign}
onRemove={onRemove}
type="Initials"
>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
@ -138,6 +137,6 @@ export const InitialsField = ({
{field.customText} {field.customText}
</p> </p>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -1,17 +1,15 @@
'use client'; import { useState } from 'react';
import { useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Recipient } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -24,29 +22,31 @@ import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { useRequiredSigningContext } from './provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
export type NameFieldProps = { export type DocumentSigningNameFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => { export const DocumentSigningNameField = ({
const router = useRouter(); field,
recipient,
onSignField,
onUnsignField,
}: DocumentSigningNameFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const { fullName: providedFullName, setFullName: setProvidedFullName } = const { fullName: providedFullName, setFullName: setProvidedFullName } =
useRequiredSigningContext(); useRequiredDocumentSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@ -56,7 +56,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const [showFullNameModal, setShowFullNameModal] = useState(false); const [showFullNameModal, setShowFullNameModal] = useState(false);
const [localFullName, setLocalFullName] = useState(''); const [localFullName, setLocalFullName] = useState('');
@ -107,7 +107,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await signFieldWithToken(payload); await signFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -139,7 +139,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -152,7 +152,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
}; };
return ( return (
<SigningFieldContainer <DocumentSigningFieldContainer
field={field} field={field}
onPreSign={onPreSign} onPreSign={onPreSign}
onSign={onSign} onSign={onSign}
@ -227,6 +227,6 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -1,19 +1,17 @@
'use client'; import { useEffect, useState } from 'react';
import { useEffect, useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { Hash, Loader } from 'lucide-react'; import { Hash, Loader } from 'lucide-react';
import { useRevalidator } from 'react-router';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number'; import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta'; import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -26,8 +24,8 @@ import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { SigningFieldContainer } from './signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
type ValidationErrors = { type ValidationErrors = {
isNumber: string[]; isNumber: string[];
@ -37,19 +35,23 @@ type ValidationErrors = {
numberFormat: string[]; numberFormat: string[];
}; };
export type NumberFieldProps = { export type DocumentSigningNumberFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient; recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
}; };
export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => { export const DocumentSigningNumberField = ({
field,
recipient,
onSignField,
onUnsignField,
}: DocumentSigningNumberFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [showRadioModal, setShowRadioModal] = useState(false); const [showRadioModal, setShowRadioModal] = useState(false);
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null; const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
@ -69,7 +71,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const [errors, setErrors] = useState(initialErrors); const [errors, setErrors] = useState(initialErrors);
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@ -79,7 +81,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
isPending: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value; const text = e.target.value;
@ -135,7 +137,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber(''); setLocalNumber('');
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -186,7 +188,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : ''); setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -229,7 +231,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0); const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
return ( return (
<SigningFieldContainer <DocumentSigningFieldContainer
field={field} field={field}
onPreSign={onPreSign} onPreSign={onPreSign}
onSign={onSign} onSign={onSign}
@ -340,6 +342,6 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

Some files were not shown because too many files have changed in this diff Show More