Compare commits

..

36 Commits

Author SHA1 Message Date
12f3b7629e fix: wip 2025-02-12 23:17:43 +11:00
1d7f3723bc fix: meta 2025-02-12 19:10:41 +11:00
4c57095ee1 fix: wip 2025-02-12 18:39:00 +11:00
15922d447b fix: wip 2025-02-12 16:41:35 +11:00
548d92c2fc fix: wip 2025-02-11 02:04:00 +11:00
d24f67d922 fix: wip 2025-02-10 03:33:22 +11:00
5b395fc9ad fix: wip 2025-02-09 21:57:26 +11:00
e128e9369e fix: auth 2025-02-09 00:46:25 +11:00
f5bfec1990 fix: dev 2025-02-08 20:38:47 +11:00
82b5795636 fix: build 2025-02-08 20:35:20 +11:00
4aec21a37f fix: migrate lingui 2025-02-07 19:40:21 +11:00
19dc43dca1 fix: migrate lingui 2025-02-07 19:33:58 +11:00
d3392dada7 fix: minimal vite config 2025-02-07 16:33:30 +11:00
8373af3f41 fix: wip 2025-02-07 00:58:50 +11:00
e5cc6455dd fix: wip 2025-02-06 15:36:25 +11:00
b127fae0e0 fix: wip 2025-02-06 15:14:16 +11:00
6fa3751a72 fix: wip 2025-02-06 14:09:44 +11:00
d164b90aa3 fix: wip 2025-02-06 11:54:54 +11:00
738201eb55 fix: errors 2025-02-06 01:57:23 +11:00
7effe66387 fix: wip 2025-02-05 23:37:21 +11:00
9c7910a070 fix: chao nextjs 2025-02-05 15:55:20 +11:00
f55ccb21dd fix: wip 2025-02-05 14:59:08 +11:00
6b4c33a1bf fix: wip 2025-02-05 01:29:26 +11:00
f4b2f8614e fix: wip 2025-02-05 00:57:10 +11:00
1057ae6d2a fix: wip 2025-02-05 00:57:00 +11:00
540cc5bfc1 fix: wip 2025-02-04 22:25:11 +11:00
381a9d3fb8 fix: wip 2025-02-04 16:24:26 +11:00
e5a9d9ddf0 fix: add embed 2025-02-03 23:56:27 +11:00
d1913dbf9c fix: wip 2025-02-03 20:09:35 +11:00
8bffa7c3ed fix: wip 2025-02-03 19:52:23 +11:00
b2af10173a fix: wip 2025-02-03 14:10:28 +11:00
28fb35327d fix: wip 2025-01-31 23:29:42 +11:00
e20cb7e179 fix: wip 2025-01-31 23:17:50 +11:00
aec44b78d0 fix: wip 2025-01-31 18:57:45 +11:00
d7d0fca501 fix: wip 2025-01-31 14:09:02 +11:00
f7a98180d7 wip 2025-01-30 14:54:15 +11:00
933 changed files with 33789 additions and 37515 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: {

View File

@ -111,83 +111,6 @@ The colors will be automatically converted to the appropriate format internally.
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system. 4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
## CSS Class Targets
In addition to CSS variables, specific components in the embedded experience can be targeted using CSS classes for more granular styling:
### Component Classes
| Class Name | Description |
| --------------------------------- | ----------------------------------------------------------------------- |
| `.embed--Root` | Main container for the embedded signing experience |
| `.embed--DocumentContainer` | Container for the document and signing widget |
| `.embed--DocumentViewer` | Container for the document viewer |
| `.embed--DocumentWidget` | The signing widget container |
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget, handles positioning |
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
| `.embed--WaitingForTurn` | Container for the waiting screen when it's not the user's turn to sign |
| `.embed--DocumentCompleted` | Container for the completion screen after signing |
| `.field--FieldRootContainer` | Base container for document fields (signatures, text, checkboxes, etc.) |
Field components also expose several data attributes that can be used for styling different states:
| Data Attribute | Values | Description |
| ------------------- | ---------------------------------------------- | ------------------------------------ |
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
### Field Styling Example
```css
/* Style all field containers */
.field--FieldRootContainer {
transition: all 200ms ease;
}
/* Style specific field types */
.field--FieldRootContainer[data-field-type='SIGNATURE'] {
background-color: rgba(0, 0, 0, 0.02);
}
/* Style inserted fields */
.field--FieldRootContainer[data-inserted='true'] {
background-color: var(--primary);
opacity: 0.2;
}
/* Style fields being validated */
.field--FieldRootContainer[data-validate='true'] {
border-color: orange;
}
```
### Example Usage
```css
/* Custom styles for the document widget */
.embed--DocumentWidget {
background-color: #ffffff;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
/* Custom styles for the waiting screen */
.embed--WaitingForTurn {
background-color: #f9fafb;
padding: 2rem;
}
/* Responsive adjustments for the document container */
@media (min-width: 768px) {
.embed--DocumentContainer {
gap: 2rem;
}
}
```
## Related ## Related
- [React Integration](/developers/embedding/react) - [React Integration](/developers/embedding/react)

View File

@ -3,8 +3,6 @@ title: Public API
description: Learn how to interact with your documents programmatically using the Documenso public API. description: Learn how to interact with your documents programmatically using the Documenso public API.
--- ---
import { Callout, Steps } from 'nextra/components';
# Public API # Public API
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as: Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
@ -15,24 +13,10 @@ Documenso provides a public REST API enabling you to interact with your document
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API. The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
## API V1 - Stable ## Swagger Documentation
Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods. The [Swagger documentation](https://app.documenso.com/api/v1/openapi) also provides information about the API endpoints, request parameters, response formats, and authentication methods.
## API V2 - Beta
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
<Callout type="warning">
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
</Callout>
🚀 [V2 Announcement](https://documen.so/sdk-blog)
💬 [Leave Feedback](https://documen.so/sdk-feedback)
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
## Availability ## Availability
The API is available to individual users, teams and higher plans. [Fair Use](https://documen.so/fair) applies. The API is available to individual users and teams.

View File

@ -21,7 +21,6 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
- `document.signed` - `document.signed`
- `document.completed` - `document.completed`
- `document.rejected` - `document.rejected`
- `document.cancelled`
## Create a webhook subscription ## Create a webhook subscription
@ -38,7 +37,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
To create a new webhook subscription, you need to provide the following information: To create a new webhook subscription, you need to provide the following information:
- Enter the webhook URL that will receive the event payload. - Enter the webhook URL that will receive the event payload.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`. - Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`.
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request. - Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp) ![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp)
@ -529,96 +528,6 @@ Example payload for the `document.rejected` event:
} }
``` ```
Example payload for the `document.rejected` event:
```json
{
"event": "DOCUMENT_CANCELLED",
"payload": {
"id": 7,
"externalId": null,
"userId": 3,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "cm6exvn93006hi02ru90a265a",
"createdAt": "2025-01-27T11:02:14.393Z",
"updatedAt": "2025-01-27T11:03:16.387Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "cm6exvn96006ji02rqvzjvwoy",
"subject": "",
"message": "",
"timezone": "Etc/UTC",
"password": null,
"dateFormat": "yyyy-MM-dd hh:mm a",
"redirectUrl": "",
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": {
"documentDeleted": true,
"documentPending": true,
"recipientSigned": true,
"recipientRemoved": true,
"documentCompleted": true,
"ownerDocumentCompleted": true,
"recipientSigningRequest": true
}
},
"recipients": [
{
"id": 7,
"documentId": 7,
"templateId": null,
"email": "mybirihix@mailinator.com",
"name": "Zorita Baird",
"token": "XkKx1HCs6Znm2UBJA2j6o",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": { "accessAuth": null, "actionAuth": null },
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
],
"Recipient": [
{
"id": 7,
"documentId": 7,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "XkKx1HCs6Znm2UBJA2j6o",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": { "accessAuth": null, "actionAuth": null },
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2025-01-27T11:03:27.730Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
## Availability ## Availability
Webhooks are available to individual users and teams. Webhooks are available to individual users and teams.

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();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
{
documentId: id, 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, isPending: 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(
(role) => (
<SelectItem key={role} value={role}> <SelectItem key={role} value={role}>
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role} {_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
</SelectItem> </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();
@ -67,7 +65,7 @@ export const TransferTeamDialog = ({
const { const {
data, data,
refetch: refetchTeamMembers, refetch: refetchTeamMembers,
isPending: loadingTeamMembers, isLoading: loadingTeamMembers,
isLoadingError: loadingTeamMembersError, isLoadingError: loadingTeamMembersError,
} = trpc.team.getTeamMembers.useQuery({ } = trpc.team.getTeamMembers.useQuery({
teamId, teamId,
@ -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,12 @@ 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}> {/* Todo: Wouldn't this break for google? */}
<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);
@ -77,11 +76,7 @@ export const TemplateDirectLinkDialog = ({
); );
const validDirectTemplateRecipients = useMemo( const validDirectTemplateRecipients = useMemo(
() => () => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
template.recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
),
[template.recipients], [template.recipients],
); );
@ -90,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);
@ -110,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`;
@ -133,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);
@ -143,7 +142,6 @@ export const TemplateDirectLinkDialog = ({
duration: 5000, duration: 5000,
}); });
router.refresh();
setToken(null); setToken(null);
}, },
onError: () => { onError: () => {
@ -235,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>
@ -436,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, isPending: 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);
@ -485,6 +483,7 @@ export const EmbedDirectTemplateClientPage = ({
{/* Fields */} {/* Fields */}
<EmbedDocumentFields <EmbedDocumentFields
recipient={recipient}
fields={localFields} fields={localFields}
metadata={metadata} metadata={metadata}
onSignField={onSignField} onSignField={onSignField}
@ -495,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 = {
@ -10,8 +10,9 @@ export type EmbedDocumentCompletedPageProps = {
}; };
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => { export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
console.log({ signature });
return ( return (
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6"> <div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<h3 className="text-foreground text-2xl font-semibold"> <h3 className="text-foreground text-2xl font-semibold">
<Trans>Document Completed!</Trans> <Trans>Document Completed!</Trans>
</h3> </h3>

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, 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,18 +19,19 @@ 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;
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | TemplateMeta | null;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
@ -40,6 +39,7 @@ export type EmbedDocumentFieldsProps = {
}; };
export const EmbedDocumentFields = ({ export const EmbedDocumentFields = ({
recipient,
fields, fields,
metadata, metadata,
onSignField, onSignField,
@ -50,34 +50,38 @@ 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}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
typedSignatureEnabled={metadata?.typedSignatureEnabled} typedSignatureEnabled={metadata?.typedSignatureEnabled}
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (
<InitialsField <DocumentSigningInitialsField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
)) ))
.with(FieldType.NAME, () => ( .with(FieldType.NAME, () => (
<NameField <DocumentSigningNameField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
)) ))
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField <DocumentSigningDateField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT} dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
@ -85,9 +89,10 @@ export const EmbedDocumentFields = ({
/> />
)) ))
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField <DocumentSigningEmailField
key={field.id} key={field.id}
field={field} field={field}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -99,9 +104,10 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<TextField <DocumentSigningTextField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -114,9 +120,10 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<NumberField <DocumentSigningNumberField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -129,9 +136,10 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<RadioField <DocumentSigningRadioField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -144,9 +152,10 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<CheckboxField <DocumentSigningCheckboxField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -159,9 +168,10 @@ export const EmbedDocumentFields = ({
}; };
return ( return (
<DropdownField <DocumentSigningDropdownField
key={field.id} key={field.id}
field={fieldWithMeta} field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />

View File

@ -0,0 +1,376 @@
import { useEffect, useLayoutEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
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 { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { BrandingLogo } from '~/components/general/branding-logo';
import { injectCss } from '~/utils/css-vars';
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
import { EmbedClientLoading } from './embed-client-loading';
import { EmbedDocumentCompleted } from './embed-document-completed';
import { EmbedDocumentFields } from './embed-document-fields';
export type EmbedSignDocumentClientPageProps = {
token: string;
documentId: number;
documentData: DocumentData;
recipient: Recipient;
fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null;
isCompleted?: boolean;
hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean;
};
export const EmbedSignDocumentClientPage = ({
token,
documentId,
documentData,
recipient,
fields,
metadata,
isCompleted,
hidePoweredBy = false,
isPlatformOrEnterprise = false,
}: EmbedSignDocumentClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setSignature,
setSignatureValid,
} = useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [isExpanded, setIsExpanded] = useState(false);
const [isNameLocked, setIsNameLocked] = useState(false);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
const [pendingFields, _completedFields] = [
fields.filter((field) => !field.inserted),
fields.filter((field) => field.inserted),
];
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const onNextFieldClick = () => {
validateFieldsInserted(fields);
setShowPendingFieldTooltip(true);
setIsExpanded(false);
};
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(fields);
if (!valid) {
setShowPendingFieldTooltip(true);
return;
}
await completeDocumentWithToken({
documentId,
token,
});
if (window.parent) {
window.parent.postMessage(
{
action: 'document-completed',
data: {
token,
documentId,
recipientId: recipient.id,
},
},
'*',
);
}
setHasCompletedDocument(true);
} catch (err) {
if (window.parent) {
window.parent.postMessage(
{
action: 'document-error',
data: null,
},
'*',
);
}
toast({
title: _(msg`Something went wrong`),
description: _(
msg`We were unable to submit this document at this time. Please try again later.`,
),
variant: 'destructive',
});
}
};
useLayoutEffect(() => {
const hash = window.location.hash.slice(1);
try {
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
if (!isCompleted && data.name) {
setFullName(data.name);
}
// Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName);
if (data.darkModeDisabled) {
document.documentElement.classList.add('dark-mode-disabled');
}
if (isPlatformOrEnterprise) {
injectCss({
css: data.css,
cssVars: data.cssVars,
});
}
} catch (err) {
console.error(err);
}
setHasFinishedInit(true);
// !: While the two setters are stable we still want to ensure we're avoiding
// !: re-renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (hasFinishedInit && hasDocumentLoaded && window.parent) {
window.parent.postMessage(
{
action: 'document-ready',
data: null,
},
'*',
);
}
}, [hasFinishedInit, hasDocumentLoaded]);
if (hasCompletedDocument) {
return (
<EmbedDocumentCompleted
name={fullName}
signature={{
id: 1,
fieldId: 1,
recipientId: 1,
created: new Date(),
signatureImageAsBase64: signature?.startsWith('data:') ? signature : null,
typedSignature: signature?.startsWith('data:') ? null : signature,
}}
/>
);
}
return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
</Button>
</div>
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Sign the document to complete the process.</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
</div>
{/* Form */}
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
/>
</div>
<div>
<Label htmlFor="email">
<Trans>Email</Trans>
</Label>
<Input
type="email"
id="email"
className="bg-background mt-2"
value={email}
disabled
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
</div>
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>
</Button>
) : (
<Button
className="col-start-2"
disabled={isThrottled || (hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
<Trans>Complete</Trans>
</Button>
)}
</div>
</div>
</div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
</ElementVisible>
{/* Fields */}
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
</div>
{!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">
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</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) {
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true); setIsTwoFactorAuthenticationDialogOpen(true);
return; return;
} }
const errorMessage = ERROR_MESSAGES[result.error]; if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
await navigate('/unverified-account');
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
router.push(`/unverified-account`);
toast({ toast({
title: _(msg`Unable to sign in`), title: _(msg`Unable to sign in`),
description: errorMessage ?? _(msg`An unknown error occurred`),
});
return;
}
toast({
title: _(msg`Unable to sign in`),
description: errorMessage ?? _(msg`An unknown error occurred`),
variant: 'destructive',
});
return;
}
if (!result?.url) {
throw new Error('An unknown error occurred');
}
window.location.href = result.url;
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),
description: _( description: _(
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`, msg`This account has not been verified. Please verify your account before signing in.`,
), ),
}); });
return;
}
const errorMessage = match(error.code)
.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));
toast({
title: _(msg`Unable to sign in`),
description: _(errorMessage),
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,7 +401,6 @@ export const SignInForm = ({
</Button> </Button>
)} )}
{isPasskeyEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
@ -435,7 +413,6 @@ export const SignInForm = ({
{!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,8 +256,10 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
</form> </form>
</Form> </Form>
<AnimatePresence initial={!hasNewlyCreatedToken}> <AnimatePresence>
{newlyCreatedToken && hasNewlyCreatedToken && ( {newlyCreatedToken &&
tokens &&
tokens.find((token) => token.id === newlyCreatedToken.id) && (
<motion.div <motion.div
className="mt-8" className="mt-8"
initial={{ opacity: 0, y: -40 }} initial={{ opacity: 0, y: -40 }}

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;
@ -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,22 +66,21 @@ 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('');
const [pages, setPages] = useState<string[]>([]); const [pages, setPages] = useState<string[]>([]);
const { data: searchDocumentsData, isPending: isSearchingDocuments } = const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
trpcReact.document.searchDocuments.useQuery( trpcReact.document.searchDocuments.useQuery(
{ {
query: search, query: search,
@ -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',
{ {

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,21 +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 { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context'; import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
export type SignDirectTemplateFormProps = { export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
directRecipient: Recipient; directRecipient: Recipient;
directRecipientFields: Field[]; directRecipientFields: Field[];
@ -65,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);
@ -170,7 +169,7 @@ export const SignDirectTemplateForm = ({
}; };
return ( return (
<RecipientProvider recipient={directRecipient}> <>
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} /> <DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
@ -184,34 +183,37 @@ 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}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (
<InitialsField <DocumentSigningInitialsField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
)) ))
.with(FieldType.NAME, () => ( .with(FieldType.NAME, () => (
<NameField <DocumentSigningNameField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
)) ))
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField <DocumentSigningDateField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient}
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT} dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE} timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
onSignField={onSignField} onSignField={onSignField}
@ -219,9 +221,10 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField <DocumentSigningEmailField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -232,12 +235,13 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<TextField <DocumentSigningTextField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
fieldMeta: parsedFieldMeta, fieldMeta: parsedFieldMeta,
}} }}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -249,12 +253,13 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<NumberField <DocumentSigningNumberField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
fieldMeta: parsedFieldMeta, fieldMeta: parsedFieldMeta,
}} }}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -266,12 +271,13 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<DropdownField <DocumentSigningDropdownField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
fieldMeta: parsedFieldMeta, fieldMeta: parsedFieldMeta,
}} }}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -283,12 +289,13 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<RadioField <DocumentSigningRadioField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
fieldMeta: parsedFieldMeta, fieldMeta: parsedFieldMeta,
}} }}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -300,12 +307,13 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<CheckboxField <DocumentSigningCheckboxField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
fieldMeta: parsedFieldMeta, fieldMeta: parsedFieldMeta,
}} }}
recipient={directRecipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
/> />
@ -343,7 +351,6 @@ export const SignDirectTemplateForm = ({
onChange={(value) => { onChange={(value) => {
setSignature(value); setSignature(value);
}} }}
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
/> />
</CardContent> </CardContent>
</Card> </Card>
@ -366,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}
@ -376,6 +383,6 @@ export const SignDirectTemplateForm = ({
/> />
</div> </div>
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>
</RecipientProvider> </>
); );
}; };

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. every time you sign in using email password.
</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,12 +1,10 @@
'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';
@ -25,24 +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 { useRecipientContext } from './recipient-context'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container';
export type CheckboxFieldProps = { export type DocumentSigningCheckboxFieldProps = {
field: FieldWithSignatureAndFieldMeta; field: FieldWithSignatureAndFieldMeta;
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 = ({ field, onSignField, onUnsignField }: CheckboxFieldProps) => { export const DocumentSigningCheckboxField = ({
field,
recipient,
onSignField,
onUnsignField,
}: DocumentSigningCheckboxFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext(); 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);
@ -84,7 +85,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
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);
@ -105,7 +106,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
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,9 +118,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: isAssistantMode description: _(msg`An error occurred while signing the document.`),
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -142,13 +141,13 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
setCheckedValues([]); setCheckedValues([]);
} }
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`), description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -180,27 +179,30 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
...checkedValues, ...checkedValues,
item.value.length > 0 ? item.value : `empty-value-${item.id}`, item.value.length > 0 ? item.value : `empty-value-${item.id}`,
]; ];
} else {
updatedValues = checkedValues.filter(
(v) => v !== item.value && v !== `empty-value-${item.id}`,
);
}
setCheckedValues(updatedValues);
await removeSignedFieldWithToken({ await removeSignedFieldWithToken({
token: recipient.token, token: recipient.token,
fieldId: field.id, fieldId: field.id,
}); });
if (updatedValues.length > 0) { if (isLengthConditionMet) {
await signFieldWithToken({ await signFieldWithToken({
token: recipient.token, token: recipient.token,
fieldId: field.id, fieldId: field.id,
value: toCheckboxValue(updatedValues), value: toCheckboxValue(checkedValues),
isBase64: true, isBase64: true,
}); });
} }
} else {
updatedValues = checkedValues.filter(
(v) => v !== item.value && v !== `empty-value-${item.id}`,
);
await removeSignedFieldWithToken({
token: recipient.token,
fieldId: field.id,
});
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -210,7 +212,8 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
variant: 'destructive', variant: 'destructive',
}); });
} finally { } finally {
startTransition(() => router.refresh()); setCheckedValues(updatedValues);
await revalidate();
} }
}; };
@ -229,7 +232,12 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
); );
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" />
@ -287,6 +295,6 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
})} })}
</div> </div>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
); );
}; };

View File

@ -0,0 +1,150 @@
import { useMemo, useState } from 'react';
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 { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
export type DocumentSigningCompleteDialogProps = {
isSubmitting: boolean;
documentTitle: string;
fields: Field[];
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
role: RecipientRole;
disabled?: boolean;
};
export const DocumentSigningCompleteDialog = ({
isSubmitting,
documentTitle,
fields,
fieldsValidated,
onSignatureComplete,
role,
disabled = false,
}: DocumentSigningCompleteDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
const handleOpenChange = (open: boolean) => {
if (isSubmitting || !isComplete) {
return;
}
setShowDialog(open);
};
return (
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button
className="w-full"
type="button"
size="lg"
onClick={fieldsValidated}
loading={isSubmitting}
disabled={disabled}
>
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
<div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
</div>
</DialogTitle>
<div className="text-muted-foreground max-w-[50ch]">
{role === RecipientRole.VIEWER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.SIGNER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete signing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.APPROVER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete approving{' '}
<span className="inline-block max-w-[11rem] truncate align-baseline">
"{documentTitle}"
</span>
.
</span>
<br /> Are you sure?
</Trans>
</span>
)}
</div>
<DocumentSigningDisclosure className="mt-4" />
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowDialog(false);
}}
>
<Trans>Cancel</Trans>
</Button>
<Button
type="button"
className="flex-1"
disabled={!isComplete}
loading={isSubmitting}
onClick={onSignatureComplete}
>
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

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,41 +13,36 @@ 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 { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
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 {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRecipientContext } from './recipient-context'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container';
export type DateFieldProps = { export type DocumentSigningDateFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
recipient: Recipient;
dateFormat?: string | null; dateFormat?: string | null;
timezone?: string | null; timezone?: string | null;
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 DateField = ({ export const DocumentSigningDateField = ({
field, field,
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 { recipient, targetSigner, isAssistantMode } = useRecipientContext();
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);
@ -60,15 +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 safeFieldMeta = ZDateFieldMeta.safeParse(field.fieldMeta); const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
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) => {
@ -87,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);
@ -99,9 +90,7 @@ export const DateField = ({
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: isAssistantMode description: _(msg`An error occurred while signing the document.`),
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -121,20 +110,20 @@ export const DateField = ({
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`), description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
}; };
return ( return (
<SigningFieldContainer <DocumentSigningFieldContainer
field={field} field={field}
onSign={onSign} onSign={onSign}
onRemove={onRemove} onRemove={onRemove}
@ -154,22 +143,10 @@ export const DateField = ({
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{localDateString} {localDateString}
</p> </p>
</div>
)} )}
</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,12 +1,10 @@
'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';
@ -28,25 +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 { useRecipientContext } from './recipient-context'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { SigningFieldContainer } from './signing-field-container';
export type DropdownFieldProps = { export type DocumentSigningDropdownFieldProps = {
field: FieldWithSignatureAndFieldMeta; field: FieldWithSignatureAndFieldMeta;
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 = ({ field, onSignField, onUnsignField }: DropdownFieldProps) => { export const DocumentSigningDropdownField = ({
field,
recipient,
onSignField,
onUnsignField,
}: DocumentSigningDropdownFieldProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext(); 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;
@ -61,7 +61,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
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);
@ -86,7 +86,8 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
} }
setLocalChoice(''); setLocalChoice('');
startTransition(() => router.refresh());
await revalidate();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -98,9 +99,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: isAssistantMode description: _(msg`An error occurred while signing the document.`),
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -125,13 +124,14 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
} }
setLocalChoice(''); setLocalChoice('');
startTransition(() => router.refresh());
await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`), description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -161,7 +161,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
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}
@ -207,7 +207,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
{field.customText} {field.customText}
</p> </p>
)} )}
</SigningFieldContainer> </DocumentSigningFieldContainer>
</div> </div>
); );
}; };

View File

@ -1,46 +1,42 @@
'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 { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
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 {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
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 { useRecipientContext } from './recipient-context'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { SigningFieldContainer } from './signing-field-container';
export type EmailFieldProps = { export type DocumentSigningEmailFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
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, 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 { recipient, targetSigner, isAssistantMode } = useRecipientContext();
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);
@ -50,10 +46,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
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 safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta); const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
try { try {
@ -74,7 +67,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
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);
@ -86,9 +79,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: isAssistantMode description: _(msg`An error occurred while signing the document.`),
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -108,20 +99,20 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
await removeSignedFieldWithToken(payload); await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh()); await revalidate();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`), description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
}; };
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" />
@ -135,22 +126,10 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText} {field.customText}
</p> </p>
</div>
)} )}
</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;
@ -46,7 +44,6 @@ export type SignatureFieldProps = {
| 'Email' | 'Email'
| 'Name' | 'Name'
| 'Signature' | 'Signature'
| 'Text'
| 'Radio' | 'Radio'
| 'Dropdown' | 'Dropdown'
| 'Number' | 'Number'
@ -54,7 +51,7 @@ export type SignatureFieldProps = {
tooltipText?: string | null; tooltipText?: string | null;
}; };
export const SigningFieldContainer = ({ export const DocumentSigningFieldContainer = ({
field, field,
loading, loading,
onPreSign, onPreSign,
@ -63,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

@ -0,0 +1,268 @@
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
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 { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
import { useRequiredDocumentSigningContext } from './document-signing-provider';
export type DocumentSigningFormProps = {
document: DocumentAndSender;
recipient: Recipient;
fields: Field[];
redirectUrl?: string | null;
isRecipientsTurn: boolean;
};
export const DocumentSigningForm = ({
document,
recipient,
fields,
redirectUrl,
isRecipientsTurn,
}: DocumentSigningFormProps) => {
const navigate = useNavigate();
const analytics = useAnalytics();
const { user } = useOptionalSession();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredDocumentSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const { mutateAsync: completeDocumentWithToken } =
trpc.recipient.completeDocumentWithToken.useMutation();
const { handleSubmit, formState } = useForm();
// Keep the loading state going if successful since the redirect may take some time.
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
}, [fields]);
const fieldsValidated = () => {
setValidateUninsertedFields(true);
validateFieldsInserted(fieldsRequiringValidation);
};
const onFormSubmit = async () => {
setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
if (hasSignatureField && !signatureValid) {
return;
}
if (!isFieldsValid) {
return;
}
await completeDocument();
// Reauth is currently not required for completing the document.
// await executeActionAuthProcedure({
// onReauthFormSubmit: completeDocument,
// actionTarget: 'DOCUMENT',
// });
};
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
await completeDocumentWithToken({
token: recipient.token,
documentId: document.id,
authOptions,
});
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: document.id,
timestamp: new Date().toISOString(),
});
if (redirectUrl) {
window.location.href = redirectUrl;
} else {
await navigate(`/sign/${recipient.token}/complete`);
}
};
return (
<form
className={cn(
'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))]': user,
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
},
)}
onSubmit={handleSubmit(onFormSubmit)}
>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
<fieldset
disabled={isSubmitting}
className={cn(
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
)}
>
<div className={cn('flex flex-1 flex-col')}>
<h3 className="text-foreground text-2xl font-semibold">
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
</h3>
{recipient.role === RecipientRole.VIEWER ? (
<>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Please mark as viewed to complete</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4" />
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>
</>
) : (
<>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Please review the document before signing.</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>
</>
)}
</div>
</fieldset>
</form>
);
};

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