Compare commits

..

56 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
9183f668d3 chore: bump node version for docker 2025-01-27 12:20:04 +11:00
54ea96391a fix: correct redirect after document duplication (#1595) 2025-01-23 16:34:22 +11:00
42d24fd1a1 feat: copy, paste, duplicate template fields (#1594) 2025-01-23 14:28:26 +11:00
dc36a8182c v1.9.0-rc.11 2025-01-21 09:49:22 +11:00
0ef85b47b1 fix: handle empty object as fieldMeta 2025-01-21 09:46:54 +11:00
058d9dd0ba v1.9.0-rc.10 2025-01-20 19:54:39 +11:00
74bb230247 fix: add empty success responses (#1600) 2025-01-20 19:47:39 +11:00
7c1e0f34e8 v1.9.0-rc.9 2025-01-20 16:08:15 +11:00
7e31323faa fix: add team context to more vanilla client usages 2025-01-20 15:53:28 +11:00
a28cdf437b feat: add get field endpoints (#1599) 2025-01-20 15:53:12 +11:00
80dfbeb16f feat: add angular embedding docs (#1592) 2025-01-20 09:35:47 +11:00
9de3a32ceb fix: pass team id to vanilla trpc client 2025-01-20 09:30:36 +11:00
0d3864548c fix: bump trpc and openapi packages (#1591) 2025-01-19 22:07:02 +11:00
9e03747e43 feat: add create document beta endpoint (#1584) 2025-01-16 13:36:00 +11:00
5750f2b477 feat: add prisma json types (#1583) 2025-01-15 13:46:45 +11:00
901be70f97 feat: add consistent response schemas (#1582) 2025-01-14 00:43:35 +11:00
7d0a9c6439 fix: refactor prisma relations (#1581) 2025-01-13 13:41:53 +11:00
48b55758e3 feat: ignore unrecognized fields from authorization response (#1479)
When authenticating using OIDC some IDPs send additional fields in their
authorization response. 

This leads to an error because these fields can't be persisted to the DB 
through the auth.js prisma adapter. 

This PR solves this by deleting all unrecognized fields from the 
authorization response before persisting. 

This behaviour is also compliant to 
[RFC6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)
2025-01-13 11:05:37 +11:00
dcaccb65f2 v1.9.0-rc.8 2025-01-13 10:21:05 +11:00
723e1b4ea2 fix: include all template meta in findTemplates 2025-01-13 09:34:23 +11:00
961 changed files with 38272 additions and 25803 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

@ -10,7 +10,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-22.04
permissions: permissions:
actions: read actions: read
contents: read contents: read

View File

@ -8,7 +8,7 @@ jobs:
e2e_tests: e2e_tests:
name: 'E2E Tests' name: 'E2E Tests'
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-22.04 runs-on: warp-ubuntu-2204-x64-16x
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -16,7 +16,7 @@
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",
"@documenso/trpc": "*", "@documenso/trpc": "*",
"@documenso/ui": "*", "@documenso/ui": "*",
"next": "14.2.23", "next": "14.2.6",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"nextra": "^2.13.4", "nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4", "nextra-theme-docs": "^2.13.4",
@ -27,6 +27,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"typescript": "5.2.2" "typescript": "5.6.2"
} }
} }

View File

@ -5,5 +5,6 @@
"svelte": "Svelte Integration", "svelte": "Svelte Integration",
"solid": "Solid Integration", "solid": "Solid Integration",
"preact": "Preact Integration", "preact": "Preact Integration",
"angular": "Angular Integration",
"css-variables": "CSS Variables" "css-variables": "CSS Variables"
} }

View File

@ -0,0 +1,90 @@
---
title: Angular Integration
description: Learn how to use our embedding SDK within your Angular application.
---
# Angular Integration
Our Angular SDK provides a simple way to embed a signing experience within your Angular application. It supports both direct link templates and signing tokens.
## Installation
To install the SDK, run the following command:
```bash
npm install @documenso/embed-angular
```
## Usage
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
### Direct Link Template
If you have a direct link template, you can simply provide the token for the template to the `EmbedDirectTemplate` component.
```typescript
import { Component } from '@angular/core';
import { EmbedDirectTemplate } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-direct-template [token]="token" />
`,
standalone: true,
imports: [EmbedDirectTemplate],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| email | string (optional) | The email the signer that will be used by default for signing |
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
### Signing Token
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
```typescript
import { Component } from '@angular/core';
import { EmbedSignDocument } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-sign-document [token]="token" />
`,
standalone: true,
imports: [EmbedSignDocument],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |

View File

@ -5,7 +5,7 @@ description: Learn how to use embedding to bring signing to your own website or
# Embedding # Embedding
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, or using generalized web components, this guide will help you get started with embedding Documenso. Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, Angular, or using generalized web components, this guide will help you get started with embedding Documenso.
## Availability ## Availability
@ -73,13 +73,14 @@ These customization options are available for both Direct Templates and Signing
We support embedding across a range of popular JavaScript frameworks, including: We support embedding across a range of popular JavaScript frameworks, including:
| Framework | Package | | Framework | Package |
| --------- | -------------------------------------------------------------------------------- | | --------- | ---------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) | | React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) | | Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) | | Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) | | Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) | | Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested. Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
@ -127,7 +128,7 @@ This will show a dialog which will ask you to configure which recipient should b
## Embedding with Signing Tokens ## Embedding with Signing Tokens
To embed the signing process for an ordinary document, youll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely. To embed the signing process for an ordinary document, you'll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
#### Instructions #### Instructions
@ -164,6 +165,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
- [Vue](/developers/embedding/vue) - [Vue](/developers/embedding/vue)
- [Svelte](/developers/embedding/svelte) - [Svelte](/developers/embedding/svelte)
- [Solid](/developers/embedding/solid) - [Solid](/developers/embedding/solid)
- [Angular](/developers/embedding/angular)
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases. If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
@ -174,4 +176,5 @@ If you're using **web components**, the integration process is slightly differen
- [Svelte Integration](/developers/embedding/svelte) - [Svelte Integration](/developers/embedding/svelte)
- [Solid Integration](/developers/embedding/solid) - [Solid Integration](/developers/embedding/solid)
- [Preact Integration](/developers/embedding/preact) - [Preact Integration](/developers/embedding/preact)
- [Angular Integration](/developers/embedding/angular)
- [CSS Variables](/developers/embedding/css-variables) - [CSS Variables](/developers/embedding/css-variables)

View File

@ -13,11 +13,11 @@
"dependencies": { "dependencies": {
"@documenso/prisma": "*", "@documenso/prisma": "*",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next": "14.2.23" "next": "14.2.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "18.3.5",
"typescript": "5.2.2" "typescript": "5.6.2"
} }
} }

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();
@ -36,7 +36,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
const [enteredEmail, setEnteredEmail] = useState<string>(''); const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } = const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation(); trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {
@ -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,14 +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 { TRPCClientError } from '@documenso/trpc/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';
@ -24,19 +21,19 @@ 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('');
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } = const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
trpc.admin.deleteDocument.useMutation(); trpc.admin.deleteDocument.useMutation();
const handleDeleteDocument = async () => { const handleDeleteDocument = async () => {
@ -53,23 +50,14 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
duration: 5000, duration: 5000,
}); });
router.push('/admin/documents'); await navigate('/admin/documents');
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({
toast({ title: _(msg`An unknown error occurred`),
title: _(msg`An error occurred`), variant: 'destructive',
description: err.message, description:
variant: 'destructive', 'We encountered an unknown error while attempting to delete your document. Please try again later.',
}); });
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
'We encountered an unknown error while attempting to delete your document. Please try again later.',
});
}
} }
}; };
@ -77,7 +65,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
<div> <div>
<div> <div>
<Alert <Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row " className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral" variant="neutral"
> >
<div> <div>

View File

@ -1,14 +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 type { User } from '@documenso/prisma/client'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { TRPCClientError } from '@documenso/trpc/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';
@ -24,20 +23,18 @@ 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, isLoading: isDeletingUser } = const { mutateAsync: deleteUser, isPending: isDeletingUser } =
trpc.admin.deleteUser.useMutation(); trpc.admin.deleteUser.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {
@ -46,31 +43,27 @@ 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) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = match(error.code)
description: err.message, .with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
variant: 'destructive', .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
}); .otherwise(() => msg`An error occurred while deleting the user.`);
} else {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`Error`),
variant: 'destructive', description: _(errorMessage),
description: variant: 'destructive',
err.message ?? duration: 7500,
_( });
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
} }
}; };

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,18 +22,21 @@ 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();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isLoading: isDisablingUser } = const { mutateAsync: disableUser, isPending: isDisablingUser } =
trpc.admin.disableUser.useMutation(); trpc.admin.disableUser.useMutation();
const onDisableAccount = async () => { const onDisableAccount = async () => {

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,18 +22,18 @@ 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();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isLoading: isEnablingUser } = const { mutateAsync: enableUser, isPending: isEnablingUser } =
trpc.admin.enableUser.useMutation(); trpc.admin.enableUser.useMutation();
const onEnableAccount = async () => { const onEnableAccount = async () => {

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();
@ -51,9 +50,8 @@ export const DeleteDocumentDialog = ({
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
const { mutateAsync: deleteDocument, isLoading } = 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,26 +81,13 @@ 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));
}; };
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -193,8 +188,8 @@ export const DeleteDocumentDialog = ({
<Button <Button
type="button" type="button"
loading={isLoading} loading={isPending}
onClick={onDelete} onClick={() => void deleteDocument({ documentId: id })}
disabled={!isDeleteEnabled && canManageDocument} disabled={!isDeleteEnabled && canManageDocument}
variant="destructive" variant="destructive"
> >

View File

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

View File

@ -1,11 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -26,30 +25,28 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type MoveDocumentDialogProps = { type DocumentMoveDialogProps = {
documentId: number; documentId: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
}; };
export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => { export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentMoveDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveDocument, isLoading } = 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">
@ -119,8 +114,8 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}> <Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>} {isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

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);
@ -65,7 +64,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
}, },
}); });
const { mutateAsync: createPasskeyRegistrationOptions, isLoading } = const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
trpc.auth.createPasskeyRegistrationOptions.useMutation(); trpc.auth.createPasskeyRegistrationOptions.useMutation();
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation(); const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
@ -141,7 +140,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
> >
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button variant="secondary" loading={isLoading}> <Button variant="secondary" loading={isPending}>
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" /> <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
<Trans>Add passkey</Trans> <Trans>Add passkey</Trans>
</Button> </Button>

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,
@ -116,7 +115,7 @@ export const ManagePublicTemplateDialog = ({
}, },
}); });
const { mutateAsync: updateTemplateSettings, isLoading: isUpdatingTemplateSettings } = const { mutateAsync: updateTemplateSettings, isPending: isUpdatingTemplateSettings } =
trpc.template.updateTemplate.useMutation(); trpc.template.updateTemplate.useMutation();
const setTemplateToPrivate = async (templateId: number) => { const setTemplateToPrivate = async (templateId: number) => {

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();
@ -39,7 +40,7 @@ export const CreateTeamCheckoutDialog = ({
const { data, isLoading } = trpc.team.getTeamPrices.useQuery(); const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
const { mutateAsync: createCheckout, isLoading: isCreatingCheckout } = const { mutateAsync: createCheckout, isPending: isCreatingCheckout } =
trpc.team.createTeamPendingCheckout.useMutation({ trpc.team.createTeamPendingCheckout.useMutation({
onSuccess: (checkoutUrl) => { onSuccess: (checkoutUrl) => {
window.open(checkoutUrl, '_blank'); window.open(checkoutUrl, '_blank');

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),
@ -64,7 +61,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
}, },
}); });
const { mutateAsync: createTeamEmailVerification, isLoading } = const { mutateAsync: createTeamEmailVerification, isPending } =
trpc.team.createTeamEmailVerification.useMutation(); trpc.team.createTeamEmailVerification.useMutation();
const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => { const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
@ -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) {
@ -120,7 +117,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
> >
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button variant="outline" loading={isLoading} className="bg-background"> <Button variant="outline" loading={isPending} className="bg-background">
<Plus className="-ml-1 mr-1 h-5 w-5" /> <Plus className="-ml-1 mr-1 h-5 w-5" />
<Trans>Add email</Trans> <Trans>Add email</Trans>
</Button> </Button>

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,15 +40,14 @@ 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, isLoading: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({ trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({
@ -69,7 +66,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
}, },
}); });
const { mutateAsync: deleteTeamEmailVerification, isLoading: isDeletingTeamEmailVerification } = const { mutateAsync: deleteTeamEmailVerification, isPending: isDeletingTeamEmailVerification } =
trpc.team.deleteTeamEmailVerification.useMutation({ trpc.team.deleteTeamEmailVerification.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({
@ -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,19 +30,19 @@ 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();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: leaveTeam, isLoading: isLeavingTeam } = trpc.team.leaveTeam.useMutation({ const { mutateAsync: leaveTeam, isPending: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: _(msg`Success`), title: _(msg`Success`),
@ -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,20 +28,20 @@ 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();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: deleteTeamMembers, isLoading: isDeletingTeamMember } = const { mutateAsync: deleteTeamMembers, isPending: isDeletingTeamMember } =
trpc.team.deleteTeamMembers.useMutation({ trpc.team.deleteTeamMembers.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({

View File

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

View File

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

View File

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

View File

@ -1,15 +1,12 @@
'use client'; import { useState } from 'react';
import React, { useState } from 'react'; import { msg } from '@lingui/core/macro';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { FilePlus, Loader } from 'lucide-react'; import { FilePlus, Loader } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { useNavigate } from 'react-router';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -26,21 +23,21 @@ import {
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type NewTemplateDialogProps = { type TemplateCreateDialogProps = {
teamId?: number; teamId?: number;
templateRootPath: string; templateRootPath: string;
}; };
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => { export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => {
const router = useRouter(); const navigate = useNavigate();
const { data: session } = useSession(); const { user } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation(); const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false); const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false); const [isUploadingFile, setIsUploadingFile] = useState(false);
const onFileDrop = async (file: File) => { const onFileDrop = async (file: File) => {
@ -51,15 +48,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
setIsUploadingFile(true); setIsUploadingFile(true);
try { try {
const { type, data } = await putPdfFile(file); const response = await putPdfFile(file);
const { id: templateDocumentDataId } = await createDocumentData({
type,
data,
});
const { id } = await createTemplate({ const { id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId, templateDocumentDataId: response.id,
}); });
toast({ toast({
@ -70,9 +63,9 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
duration: 5000, duration: 5000,
}); });
setShowNewTemplateDialog(false); setShowTemplateCreateDialog(false);
router.push(`${templateRootPath}/${id}/edit`); await navigate(`${templateRootPath}/${id}/edit`);
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@ -86,11 +79,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, isLoading } = 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`),
@ -51,7 +53,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -70,7 +72,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
disabled={isLoading} disabled={isPending}
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
@ -79,7 +81,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
loading={isLoading} loading={isPending}
onClick={async () => deleteTemplate({ templateId: id })} onClick={async () => deleteTemplate({ templateId: id })}
> >
<Trans>Delete</Trans> <Trans>Delete</Trans>

View File

@ -1,17 +1,15 @@
'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; Recipient: Recipient[] }; template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
}; };
export const TemplateDirectLinkDialogWrapper = ({ export const TemplateDirectLinkDialogWrapper = ({

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';
@ -46,12 +45,10 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type TemplateDirectLinkDialogProps = { type TemplateDirectLinkDialogProps = {
template: Template & { template: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null; directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[]; recipients: Recipient[];
}; };
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
@ -67,11 +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 team = useOptionalCurrentTeam();
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);
@ -81,21 +76,21 @@ export const TemplateDirectLinkDialog = ({
); );
const validDirectTemplateRecipients = useMemo( const validDirectTemplateRecipients = useMemo(
() => template.Recipient.filter((recipient) => recipient.role !== RecipientRole.CC), () => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
[template.Recipient], [template.recipients],
); );
const { const {
mutateAsync: createTemplateDirectLink, mutateAsync: createTemplateDirectLink,
isLoading: 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);
@ -108,9 +103,11 @@ export const TemplateDirectLinkDialog = ({
}, },
}); });
const { mutateAsync: toggleTemplateDirectLink, isLoading: 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`;
@ -131,9 +128,11 @@ export const TemplateDirectLinkDialog = ({
}, },
}); });
const { mutateAsync: deleteTemplateDirectLink, isLoading: 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>
@ -326,7 +324,7 @@ export const TemplateDirectLinkDialog = ({
</div> </div>
{/* Prevent creating placeholder direct template recipient if the email already exists. */} {/* Prevent creating placeholder direct template recipient if the email already exists. */}
{!template.Recipient.some( {!template.recipients.some(
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL, (recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
) && ( ) && (
<DialogFooter className="mx-auto"> <DialogFooter className="mx-auto">
@ -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, isLoading } = 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.`),
@ -55,7 +49,7 @@ export const DuplicateTemplateDialog = ({
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -70,7 +64,7 @@ export const DuplicateTemplateDialog = ({
<DialogFooter> <DialogFooter>
<Button <Button
type="button" type="button"
disabled={isLoading} disabled={isPending}
variant="secondary" variant="secondary"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
@ -79,7 +73,7 @@ export const DuplicateTemplateDialog = ({
<Button <Button
type="button" type="button"
loading={isLoading} loading={isPending}
onClick={async () => onClick={async () =>
duplicateTemplate({ duplicateTemplate({
templateId: id, templateId: id,

View File

@ -1,13 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { msg } from '@lingui/core/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -28,29 +27,46 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type MoveTemplateDialogProps = { type TemplateMoveDialogProps = {
templateId: number; templateId: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
onMove?: ({
templateId,
teamUrl,
}: {
templateId: number;
teamUrl: string;
}) => Promise<void> | void;
}; };
export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => { export const TemplateMoveDialog = ({
const router = useRouter(); templateId,
open,
onOpenChange,
onMove,
}: TemplateMoveDialogProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => { const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
router.refresh(); onSuccess: async () => {
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,8 +144,12 @@ 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={isLoading} disabled={!selectedTeamId || isLoading}> <Button
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>} onClick={handleOnMove}
loading={isPending}
disabled={!selectedTeamId || isPending}
>
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

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);
@ -100,7 +98,7 @@ export const EmbedDirectTemplateClientPage = ({
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE); const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } = const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
trpc.template.createDocumentFromDirectTemplate.useMutation(); trpc.template.createDocumentFromDirectTemplate.useMutation();
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => { const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
@ -118,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({
}); });
if (field.type === FieldType.SIGNATURE) { if (field.type === FieldType.SIGNATURE) {
newField.Signature = { newField.signature = {
id: 1, id: 1,
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
@ -163,7 +161,7 @@ export const EmbedDirectTemplateClientPage = ({
customText: '', customText: '',
inserted: false, inserted: false,
signedValue: undefined, signedValue: undefined,
Signature: undefined, signature: undefined,
}); });
}), }),
); );
@ -496,7 +494,7 @@ export const EmbedDirectTemplateClientPage = ({
{!hidePoweredBy && ( {!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100"> <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span> <span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" /> <BrandingLogo className="ml-2 inline-block h-[14px]" />
</div> </div>
)} )}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { renderSVG } from 'uqr'; import { renderSVG } from 'uqr';
import { z } from 'zod'; import { z } from 'zod';
@ -50,8 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => { export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null); const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
@ -61,7 +58,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
const { const {
mutateAsync: setup2FA, mutateAsync: setup2FA,
data: setup2FAData, data: setup2FAData,
isLoading: isSettingUp2FA, isPending: isSettingUp2FA,
} = trpc.twoFactorAuthentication.setup.useMutation({ } = trpc.twoFactorAuthentication.setup.useMutation({
onError: () => { onError: () => {
toast({ toast({
@ -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';
@ -47,7 +44,7 @@ export const ViewRecoveryCodesDialog = () => {
const { const {
data: recoveryCodes, data: recoveryCodes,
mutate, mutate,
isLoading, isPending,
error, error,
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); } = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
@ -121,7 +118,7 @@ export const ViewRecoveryCodesDialog = () => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<fieldset className="flex flex-col space-y-4" disabled={isLoading}> <fieldset className="flex flex-col space-y-4" disabled={isPending}>
<FormField <FormField
name="token" name="token"
control={viewRecoveryCodesForm.control} control={viewRecoveryCodesForm.control}
@ -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(() => (
@ -164,7 +161,7 @@ export const ViewRecoveryCodesDialog = () => {
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" loading={isLoading}> <Button type="submit" loading={isPending}>
<Trans>View</Trans> <Trans>View</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>

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,15 +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 { TRPCClientError } from '@documenso/trpc/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';
@ -40,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: {
@ -74,23 +70,15 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
duration: 5000, duration: 5000,
}); });
router.refresh(); await revalidate();
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({
toast({ title: _(msg`An unknown error occurred`),
title: _(msg`An error occurred`), description: _(
description: err.message, msg`We encountered an unknown error while attempting update your profile. Please try again later.`,
variant: 'destructive', ),
}); variant: 'destructive',
} else { });
toast({
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
),
variant: 'destructive',
});
}
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,17 @@
'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 { z } from 'zod'; 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 type { ApiToken } from '@documenso/prisma/client'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { TRPCClientError } from '@documenso/trpc/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';
@ -40,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(),
@ -60,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();
@ -71,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);
@ -129,25 +125,22 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
}); });
form.reset(); form.reset();
} catch (err) {
const error = AppError.parseError(err);
startTransition(() => router.refresh()); const errorMessage = match(error.code)
} catch (error) { .with(
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') { AppErrorCode.UNAUTHORIZED,
toast({ () => msg`You do not have permission to create a token for this team`,
title: _(msg`An error occurred`), )
description: error.message, .otherwise(() => msg`Something went wrong. Please try again later.`);
variant: 'destructive',
}); toast({
} else { title: _(msg`An error occurred`),
toast({ description: _(errorMessage),
title: _(msg`An unknown error occurred`), variant: 'destructive',
description: _( duration: 5000,
msg`We encountered an unknown error while attempting create the new token. Please try again later.`, });
),
variant: 'destructive',
duration: 5000,
});
}
} }
}; };
@ -263,34 +256,36 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
</form> </form>
</Form> </Form>
<AnimatePresence initial={!hasNewlyCreatedToken}> <AnimatePresence>
{newlyCreatedToken && hasNewlyCreatedToken && ( {newlyCreatedToken &&
<motion.div tokens &&
className="mt-8" tokens.find((token) => token.id === newlyCreatedToken.id) && (
initial={{ opacity: 0, y: -40 }} <motion.div
animate={{ opacity: 1, y: 0 }} className="mt-8"
exit={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: -40 }}
> animate={{ opacity: 1, y: 0 }}
<Card gradient> exit={{ opacity: 0, y: 40 }}
<CardContent className="p-4"> >
<p className="text-muted-foreground mt-2 text-sm"> <Card gradient>
<Trans> <CardContent className="p-4">
Your token was created successfully! Make sure to copy it because you won't be <p className="text-muted-foreground mt-2 text-sm">
able to see it again! <Trans>
</Trans> Your token was created successfully! Make sure to copy it because you won't be
</p> able to see it again!
</Trans>
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm"> <p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken.token} {newlyCreatedToken.token}
</p> </p>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}> <Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
<Trans>Copy token</Trans> <Trans>Copy token</Trans>
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +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 type { Field, Recipient } from '@documenso/prisma/client'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { TemplateWithDetails } from '@documenso/prisma/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
@ -29,38 +29,39 @@ 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<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
directTemplateRecipient: Recipient & { Field: 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 { Recipient } = template; const { user } = useOptionalSession();
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
const recipientsWithBlankDirectRecipientEmail = Recipient.map((recipient) => { const { recipients } = template;
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
if (recipient.id === directTemplateRecipient.id) { if (recipient.id === directTemplateRecipient.id) {
return { return {
...recipient, ...recipient,
@ -71,10 +72,10 @@ 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.Recipient.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,
message: _(msg`Email cannot already exist in the template`), message: _(msg`Email cannot already exist in the template`),
@ -96,7 +97,7 @@ export const ConfigureDirectTemplateFormPartial = ({
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded &&
directTemplateRecipient.Field.map((field, index) => ( directTemplateRecipient.fields.map((field, index) => (
<ShowFieldItem <ShowFieldItem
key={index} key={index}
field={field} field={field}
@ -124,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 { Field } from '@documenso/prisma/client'; import type { TTemplate } from '@documenso/lib/types/template';
import { type Recipient } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
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,18 +16,22 @@ 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<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
directTemplateToken: string; directTemplateToken: string;
directTemplateRecipient: Recipient & { Field: Field[] }; directTemplateRecipient: Recipient & { fields: Field[] };
}; };
type DirectTemplateStep = 'configure' | 'sign'; type DirectTemplateStep = 'configure' | 'sign';
@ -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,10 +162,10 @@ export const DirectTemplatePageView = ({
initialEmail={email} initialEmail={email}
/> />
<SignDirectTemplateForm <DirectTemplateSigningForm
flowStep={directTemplateFlow.sign} flowStep={directTemplateFlow.sign}
directRecipient={recipient} directRecipient={recipient}
directRecipientFields={directTemplateRecipient.Field} directRecipientFields={directTemplateRecipient.fields}
template={template} template={template}
onSubmit={onSignDirectTemplateSubmit} onSubmit={onSignDirectTemplateSubmit}
/> />

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';
@ -14,10 +16,8 @@ import {
ZRadioFieldMeta, ZRadioFieldMeta,
ZTextFieldMeta, ZTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
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 { TemplateWithDetails } from '@documenso/prisma/types/template';
import type { import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
@ -38,41 +38,41 @@ import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useStep } from '@documenso/ui/primitives/stepper'; import { useStep } from '@documenso/ui/primitives/stepper';
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field'; import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
import { DateField } from '~/app/(signing)/sign/[token]/date-field'; import { DocumentSigningCompleteDialog } from '~/components/general/document-signing/document-signing-complete-dialog';
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field'; import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
import { EmailField } from '~/app/(signing)/sign/[token]/email-field'; import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field'; import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
import { NameField } from '~/app/(signing)/sign/[token]/name-field'; import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
import { NumberField } from '~/app/(signing)/sign/[token]/number-field'; import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field'; import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog'; import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
export type SignDirectTemplateFormProps = { export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
directRecipient: Recipient; directRecipient: Recipient;
directRecipientFields: Field[]; directRecipientFields: Field[];
template: Omit<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>; onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
}; };
export type DirectTemplateLocalField = Field & { export type DirectTemplateLocalField = Field & {
signedValue?: TSignFieldWithTokenMutationSchema; signedValue?: TSignFieldWithTokenMutationSchema;
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);
@ -95,7 +95,7 @@ export const SignDirectTemplateForm = ({
}; };
if (field.type === FieldType.SIGNATURE) { if (field.type === FieldType.SIGNATURE) {
tempField.Signature = { tempField.signature = {
id: 1, id: 1,
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
@ -127,7 +127,7 @@ export const SignDirectTemplateForm = ({
customText: '', customText: '',
inserted: false, inserted: false,
signedValue: undefined, signedValue: undefined,
Signature: undefined, signature: undefined,
}; };
}), }),
); );
@ -183,7 +183,7 @@ export const SignDirectTemplateForm = ({
{localFields.map((field) => {localFields.map((field) =>
match(field.type) match(field.type)
.with(FieldType.SIGNATURE, () => ( .with(FieldType.SIGNATURE, () => (
<SignatureField <DocumentSigningSignatureField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -192,7 +192,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (
<InitialsField <DocumentSigningInitialsField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -201,7 +201,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.NAME, () => ( .with(FieldType.NAME, () => (
<NameField <DocumentSigningNameField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -210,7 +210,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField <DocumentSigningDateField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -221,7 +221,7 @@ export const SignDirectTemplateForm = ({
/> />
)) ))
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField <DocumentSigningEmailField
key={field.id} key={field.id}
field={field} field={field}
recipient={directRecipient} recipient={directRecipient}
@ -235,7 +235,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<TextField <DocumentSigningTextField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -253,7 +253,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<NumberField <DocumentSigningNumberField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -271,7 +271,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<DropdownField <DocumentSigningDropdownField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -289,7 +289,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<RadioField <DocumentSigningRadioField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -307,7 +307,7 @@ export const SignDirectTemplateForm = ({
: null; : null;
return ( return (
<CheckboxField <DocumentSigningCheckboxField
key={field.id} key={field.id}
field={{ field={{
...field, ...field,
@ -373,7 +373,7 @@ export const SignDirectTemplateForm = ({
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
<SignDialog <DocumentSigningCompleteDialog
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit} onSignatureComplete={handleSubmit}
documentTitle={template.title} documentTitle={template.title}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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