mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
5 Commits
v1.9.0-rc.
...
feat/signa
| Author | SHA1 | Date | |
|---|---|---|---|
| a730acc76a | |||
| 51afe95780 | |||
| 80d2d0703a | |||
| a301dd345f | |||
| 69bf404b32 |
4
.github/actions/cache-build/action.yml
vendored
4
.github/actions/cache-build/action.yml
vendored
@ -3,7 +3,7 @@ description: 'Cache or restore if necessary'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v20.x
|
||||
default: v18.x
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@ -17,7 +17,7 @@ runs:
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
|
||||
key: prod-build-${{ github.run_id }}-${{ hashFiles('package-lock.json') }}
|
||||
key: prod-build-${{ github.run_id }}
|
||||
restore-keys: prod-build-
|
||||
|
||||
- run: npm run build
|
||||
|
||||
2
.github/actions/node-install/action.yml
vendored
2
.github/actions/node-install/action.yml
vendored
@ -2,7 +2,7 @@ name: 'Setup node and cache node_modules'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v20.x
|
||||
default: v18.x
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
|
||||
@ -303,10 +303,6 @@ WantedBy=multi-user.target
|
||||
|
||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||
|
||||
## Elestio
|
||||
|
||||
[](https://elest.io/open-source/documenso)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I'm not receiving any emails when using the developer quickstart.
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"next-plausible": "^3.12.0",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
@ -32,4 +32,4 @@
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,5 @@
|
||||
"title": "API & Integration Guides"
|
||||
},
|
||||
"public-api": "Public API",
|
||||
"embedding": "Embedding",
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
---
|
||||
title: Get Started
|
||||
description: Learn how to use embedding to bring signing to your own website or application
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
## Availability
|
||||
|
||||
Embedding is currently available for all users on a **Teams Plan** and above, as well as **Early Adopter's** within a team (Early Adopters can create a team for free).
|
||||
|
||||
In the future, we will roll out a **Platform Plan** that will offer additional enhancements for embedding, including the option to remove Documenso branding for a more customized experience.
|
||||
|
||||
## How Embedding Works
|
||||
|
||||
Embedding with Documenso allows you to handle document signing in two main ways:
|
||||
|
||||
1. **Using Direct Templates**: Using direct templates you can have an evergreen template that upon completion will create a new document within Documenso.
|
||||
2. **Using a Signing Token**: A more advanced option for those running rich integrations with Documenso already. Given a recipients signing token you can embed the signing experience in your application rather than direct the recipient to Documenso.
|
||||
|
||||
_For most use-cases we recommend using direct templates, however if you have a need for a more advanced integration, we are happy to help you get started._
|
||||
|
||||
## Supported Frameworks
|
||||
|
||||
We support embedding across a range of popular JavaScript frameworks, including:
|
||||
|
||||
| Framework | Package |
|
||||
| --------- | -------------------------------------------------------------------------------- |
|
||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||
|
||||
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.
|
||||
|
||||
## Embedding with Direct Templates
|
||||
|
||||
#### Instructions
|
||||
|
||||
To get started with embedding using a Direct Template we will need the URL segment which is also referred to as the token for the template.
|
||||
|
||||
You can find your URL/Token by performing the following steps:
|
||||
|
||||
1. **Navigate to your team's templates within Documenso**
|
||||
|
||||

|
||||
|
||||
2. **Click on the direct link template you want to embed**
|
||||
|
||||
This will copy the URL to your clipboard, e.g. `https://stg-app.documenso.com/d/-WoSwWVT-fYOERS2MI37k`
|
||||
|
||||
**For the above url the token is `-WoSwWVT-fYOERS2MI37k`**
|
||||
|
||||
3. Provide the token to the `EmbedDirectTemplate` component in your frameworks SDK
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedDirectTemplate token={token} />;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Converting a regular template to a direct link template**
|
||||
|
||||
If you don't currently have any direct link templates you can easily create one by selecting the "Direct Link" option within the actions dropdown on the templates table.
|
||||
|
||||
This will show a dialog which will ask you to configure which recipient should be used as the direct link signer.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Embedding with Signing Tokens
|
||||
|
||||
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
|
||||
|
||||
1. Retrieve the signing token for the recipient document you want to embed
|
||||
|
||||
This will typically be done using an API integration where signing tokens are provided as part of the response when creating a document. Alternatively you can manually get a signing link by clicking hovering over a recipients avatar and clicking their email on a document that you own.
|
||||
|
||||

|
||||
|
||||
With the signing url on our clipboard we can extract the token the same way we did for the direct link template.
|
||||
|
||||
So `https://stg-app.documenso.com/sign/lm7Tp2_yhvFfzdeJQzYQF` will become `lm7Tp2_yhvFfzdeJQzYQF`
|
||||
|
||||
2. Provide the token to the `EmbedSignDocument` component in your frameworks SDK
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-react';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={token} />;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Embedding in Your Application
|
||||
|
||||
Once you've obtained the appropriate tokens, you can integrate the signing experience into your application. For framework-specific instructions, please refer to the guides provided in our documentation for:
|
||||
|
||||
- [React](/developers/embedding/react)
|
||||
- [Preact](/developers/embedding/preact)
|
||||
- [Vue](/developers/embedding/vue)
|
||||
- [Svelte](/developers/embedding/svelte)
|
||||
- [Solid](/developers/embedding/solid)
|
||||
|
||||
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.
|
||||
|
||||
## Stay Tuned for the Platform Plan
|
||||
|
||||
While embedding is already a powerful tool, we're working on a **Platform Plan** that will introduce even more functionality. This plan will offer:
|
||||
|
||||
- Additional customization options
|
||||
- The ability to remove Documenso branding
|
||||
- Additional controls for the signing experience
|
||||
|
||||
More details will be shared as we approach the release.
|
||||
@ -1,77 +0,0 @@
|
||||
---
|
||||
title: Preact Integration
|
||||
description: Learn how to use our embedding SDK within your Preact application.
|
||||
---
|
||||
|
||||
# Preact Integration
|
||||
|
||||
Our Preact SDK provides a simple way to embed a signing experience within your Preact application. It supports both direct link templates and signing tokens.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the SDK, run the following command:
|
||||
|
||||
```bash
|
||||
npm install @documenso/embed-preact
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-preact';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedDirectTemplate token={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 |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| 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 has been signed |
|
||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field has been unsigned |
|
||||
|
||||
### Signing Token
|
||||
|
||||
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-preact';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={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 |
|
||||
@ -1,77 +0,0 @@
|
||||
---
|
||||
title: React Integration
|
||||
description: Learn how to use our embedding SDK within your React application.
|
||||
---
|
||||
|
||||
# React Integration
|
||||
|
||||
Our React SDK provides a simple way to embed a signing experience within your React application. It supports both direct link templates and signing tokens.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the SDK, run the following command:
|
||||
|
||||
```bash
|
||||
npm install @documenso/embed-react
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedDirectTemplate token={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 |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| 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 has been signed |
|
||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field has been unsigned |
|
||||
|
||||
### Signing Token
|
||||
|
||||
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-react';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={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 |
|
||||
@ -1,77 +0,0 @@
|
||||
---
|
||||
title: Solid.js Integration
|
||||
description: Learn how to use our embedding SDK within your Solid.js application.
|
||||
---
|
||||
|
||||
# Solid.js Integration
|
||||
|
||||
Our Solid.js SDK provides a simple way to embed a signing experience within your Solid.js application. It supports both direct link templates and signing tokens.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the SDK, run the following command:
|
||||
|
||||
```bash
|
||||
npm install @documenso/embed-solid
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-solid';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedDirectTemplate token={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 |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| 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 has been signed |
|
||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field has been unsigned |
|
||||
|
||||
### Signing Token
|
||||
|
||||
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-solid';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={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 |
|
||||
@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Svelte Integration
|
||||
description: Learn how to use our embedding SDK within your Svelte application.
|
||||
---
|
||||
|
||||
# Svelte Integration
|
||||
|
||||
Our Svelte SDK provides a simple way to embed a signing experience within your Svelte application. It supports both direct link templates and signing tokens.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the SDK, run the following command:
|
||||
|
||||
```bash
|
||||
npm install @documenso/embed-svelte
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```html
|
||||
<script lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-svelte';
|
||||
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedDirectTemplate {token} />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 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 |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| 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 has been signed |
|
||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field has been unsigned |
|
||||
|
||||
### Signing Token
|
||||
|
||||
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-svelte';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={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 |
|
||||
@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Vue Integration
|
||||
description: Learn how to use our embedding SDK within your Vue application.
|
||||
---
|
||||
|
||||
# Vue Integration
|
||||
|
||||
Our Vue SDK provides a simple way to embed a signing experience within your Vue application. It supports both direct link templates and signing tokens.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the SDK, run the following command:
|
||||
|
||||
```bash
|
||||
npm install @documenso/embed-vue
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-vue';
|
||||
|
||||
const token = ref('YOUR_TOKEN_HERE'); // Replace with the actual token
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedDirectTemplate :token="token" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 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 |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| 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 has been signed |
|
||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field has been unsigned |
|
||||
|
||||
### Signing Token
|
||||
|
||||
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-vue';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||
|
||||
return <EmbedSignDocument token={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 |
|
||||
@ -3,6 +3,5 @@
|
||||
"quickstart": "Developer Quickstart",
|
||||
"manual": "Manual Setup",
|
||||
"gitpod": "Gitpod",
|
||||
"signing-certificate": "Signing Certificate",
|
||||
"translations": "Translations"
|
||||
}
|
||||
"signing-certificate": "Signing Certificate"
|
||||
}
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
---
|
||||
title: Translations
|
||||
description: Handling translations in code.
|
||||
---
|
||||
|
||||
# About
|
||||
|
||||
Documenso uses the following stack to handle translations:
|
||||
|
||||
- [Lingui](https://lingui.dev/) - React i10n library
|
||||
- [Crowdin](https://crowdin.com/) - Handles syncing translations
|
||||
- [OpenAI](https://openai.com/) - Provides AI translations
|
||||
|
||||
Additional reading can be found in the [Lingui documentation](https://lingui.dev/introduction).
|
||||
|
||||
## Requirements
|
||||
|
||||
You **must** insert **`setupI18nSSR()`** when creating any of the following files:
|
||||
|
||||
- Server layout.tsx
|
||||
- Server page.tsx
|
||||
- Server loading.tsx
|
||||
|
||||
Server meaning it does not have `'use client'` in it.
|
||||
|
||||
```tsx
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export default function SomePage() {
|
||||
setupI18nSSR(); // Required if there are translations within the page, or nested in components.
|
||||
|
||||
// Rest of code...
|
||||
}
|
||||
```
|
||||
|
||||
Additional information can be found [here.](https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui)
|
||||
|
||||
## Quick guide
|
||||
|
||||
If you require more in-depth information, please see the [Lingui documentation](https://lingui.dev/introduction).
|
||||
|
||||
### HTML
|
||||
|
||||
Wrap all text to translate in **`<Trans></Trans>`** tags exported from **@lingui/macro** (not @lingui/react).
|
||||
|
||||
```html
|
||||
<h1>
|
||||
<Trans>Title</Trans>
|
||||
</h1>
|
||||
```
|
||||
|
||||
For text that is broken into elements, but represent a whole sentence, you must wrap it in a Trans tag so ensure the full message is extracted correctly.
|
||||
|
||||
```html
|
||||
<h1>
|
||||
<Trans>
|
||||
This is one
|
||||
<span className="text-foreground/60">full</span>
|
||||
<a href="https://documenso.com">sentence</a>
|
||||
</Trans>
|
||||
</h1>
|
||||
```
|
||||
|
||||
### Constants outside of react components
|
||||
|
||||
```tsx
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
// Wrap text in msg`text to translate` when it's in a constant here, or another file/package.
|
||||
export const CONSTANT_WITH_MSG = {
|
||||
foo: msg`Hello`,
|
||||
bar: msg`World`,
|
||||
};
|
||||
|
||||
export const SomeComponent = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* This will render the correct translated text. */}
|
||||
<p>{_(CONSTANT_WITH_MSG.foo)}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Plurals
|
||||
|
||||
Lingui provides a Plural component to make it easy. See full documentation [here.](https://lingui.dev/ref/macro#plural-1)
|
||||
|
||||
```tsx
|
||||
// Basic usage.
|
||||
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
|
||||
```
|
||||
|
||||
### Dates
|
||||
|
||||
Lingui provides a [DateTime instance](https://lingui.dev/ref/core#i18n.date) with the configured locale.
|
||||
|
||||
#### Server components
|
||||
|
||||
Note that the i18n instance is coming from **setupI18nSSR**.
|
||||
|
||||
```tsx
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
export const SomeComponent = () => {
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
|
||||
};
|
||||
```
|
||||
|
||||
#### Client components
|
||||
|
||||
Note that the i18n instance is coming from the **import**.
|
||||
|
||||
```tsx
|
||||
import { i18n } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
export const SomeComponent = () => {
|
||||
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
|
||||
};
|
||||
```
|
||||
@ -15,7 +15,7 @@ The signature field collects the signer's signature. It's required for each reci
|
||||
|
||||
The field doesn't have any additional settings. You just need to place it on the document where you want the signer to sign.
|
||||
|
||||

|
||||

|
||||
|
||||
### Document Signing View
|
||||
|
||||
@ -23,11 +23,11 @@ The recipient will see the signature field when they open the document to sign.
|
||||
|
||||
The recipient must click on the signature field to open the signing view, where they can sign using their mouse, touchpad, or touchscreen.
|
||||
|
||||

|
||||

|
||||
|
||||
The image below shows the signature field signed by the recipient.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -39,7 +39,7 @@ The email field is used to collect the signer's email address.
|
||||
|
||||
The field doesn't have any additional settings. You just need to place it on the document where you want the signer to sign.
|
||||
|
||||

|
||||

|
||||
|
||||
### Document Signing View
|
||||
|
||||
@ -47,11 +47,11 @@ When the recipient opens the document to sign, they will see the email field.
|
||||
|
||||
The recipient must click on the email field to automatically sign the field with the email associated with their account.
|
||||
|
||||

|
||||

|
||||
|
||||
The image below shows the email field signed by the recipient.
|
||||
|
||||

|
||||

|
||||
|
||||
After entering their email address, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -63,7 +63,7 @@ The name field is used to collect the signer's name.
|
||||
|
||||
The field doesn't have any additional settings. You just need to place it on the document where you want the signer to sign.
|
||||
|
||||

|
||||

|
||||
|
||||
### Document Signing View
|
||||
|
||||
@ -71,11 +71,11 @@ When the recipient opens the document to sign, they will see the name field.
|
||||
|
||||
The recipient must click on the name field, which will automatically sign the field with the name associated with their account.
|
||||
|
||||

|
||||

|
||||
|
||||
The image below shows the name field signed by the recipient.
|
||||
|
||||

|
||||

|
||||
|
||||
After entering their name, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -87,7 +87,7 @@ The date field is used to collect the date of the signature.
|
||||
|
||||
The field doesn't have any additional settings. You just need to place it on the document where you want the signer to sign.
|
||||
|
||||

|
||||

|
||||
|
||||
### Document Signing View
|
||||
|
||||
@ -95,11 +95,11 @@ When the recipient opens the document to sign, they will see the date field.
|
||||
|
||||
The recipient must click on the date field to automatically sign the field with the current date and time.
|
||||
|
||||

|
||||

|
||||
|
||||
The image below shows the date field signed by the recipient.
|
||||
|
||||

|
||||

|
||||
|
||||
After entering the date, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -111,11 +111,11 @@ The text field is used to collect text input from the signer.
|
||||
|
||||
Place the text field on the document where you want the signer to enter text. The text field comes with additional settings that can be configured.
|
||||
|
||||

|
||||

|
||||
|
||||
To open the settings, click on the text field and then on the "Sliders" icon. That opens the settings panel on the right side of the screen.
|
||||
|
||||

|
||||

|
||||
|
||||
The text field settings include:
|
||||
|
||||
@ -137,7 +137,7 @@ It also comes with a couple of rules:
|
||||
|
||||
Let's look at the following example.
|
||||
|
||||

|
||||

|
||||
|
||||
The field is configured as follows:
|
||||
|
||||
@ -156,23 +156,23 @@ What the recipient sees when they open the document to sign depends on the setti
|
||||
|
||||
In this case, the recipient sees the text field signed with the default value.
|
||||
|
||||

|
||||

|
||||
|
||||
The recipient can modify the text field value since the field is not read-only. To change the value, the recipient must click the field to un-sign it.
|
||||
|
||||
Once it's unsigned, the field uses the label set by the sender.
|
||||
|
||||

|
||||

|
||||
|
||||
To sign the field with a different value, the recipient needs to click on the field and enter the new value.
|
||||
|
||||

|
||||

|
||||
|
||||
Since the text field has a character limit, the recipient must enter a value that doesn't exceed the limit. Otherwise, an error message will appear, and the field will not be signed.
|
||||
|
||||
The image below illustrates the text field signed with a new value.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing the field, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -184,11 +184,11 @@ The number field is used for collecting a number input from the signer.
|
||||
|
||||
Place the number field on the document where you want the signer to enter a number. The number field comes with additional settings that can be configured.
|
||||
|
||||

|
||||

|
||||
|
||||
To open the settings, click on the number field and then on the "Sliders" icon. That opens the settings panel on the right side of the screen.
|
||||
|
||||

|
||||

|
||||
|
||||
The number field settings include:
|
||||
|
||||
@ -221,7 +221,7 @@ In this example, the number field is configured as follows:
|
||||
- Validation:
|
||||
- Min value: 5, Max value: 50
|
||||
|
||||

|
||||

|
||||
|
||||
Since the field has a label set, the label is displayed instead of the default number field value - "Add number".
|
||||
|
||||
@ -231,23 +231,23 @@ What the recipient sees when they open the document to sign depends on the setti
|
||||
|
||||
The recipient sees the number field signed with the default value in this case.
|
||||
|
||||

|
||||

|
||||
|
||||
Since the number field is not read-only, the recipient can modify its value. To change the value, the recipient must click the field to un-sign it.
|
||||
|
||||
Once it's unsigned, the field uses the label set by the sender.
|
||||
|
||||

|
||||

|
||||
|
||||
To sign the field with a different value, the recipient needs to click on the field and enter the new value.
|
||||
|
||||

|
||||

|
||||
|
||||
Since the number field has a validation rule set, the recipient must enter a value that meets the rules. In this example, the value needs to be between 5 and 50. Otherwise, an error message will appear, and the field will not be signed.
|
||||
|
||||
The image below illustrates the text field signed with a new value.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing the field, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -259,11 +259,11 @@ The radio field is used to collect a single choice from the signer.
|
||||
|
||||
Place the radio field on the document where you want the signer to select a choice. The radio field comes with additional settings that can be configured.
|
||||
|
||||

|
||||

|
||||
|
||||
To open the settings, click on the radio field and then on the "Sliders" icon. That opens the settings panel on the right side of the screen.
|
||||
|
||||

|
||||

|
||||
|
||||
The radio field settings include:
|
||||
|
||||
@ -293,7 +293,7 @@ In this example, the radio field is configured as follows:
|
||||
- Empty value
|
||||
- Option 3
|
||||
|
||||

|
||||

|
||||
|
||||
Since the field contains radio options, it displays them instead of the default radio field value, "Radio".
|
||||
|
||||
@ -303,11 +303,11 @@ What the recipient sees when they open the document to sign depends on the setti
|
||||
|
||||
In this case, the recipient sees the radio field unsigned because the sender didn't select a value.
|
||||
|
||||

|
||||

|
||||
|
||||
The recipient can select one of the options by clicking on the radio button next to the option.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing the field, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -319,11 +319,11 @@ The checkbox field is used to collect multiple choices from the signer.
|
||||
|
||||
Place the checkbox field on the document where you want the signer to select choices. The checkbox field comes with additional settings that can be configured.
|
||||
|
||||

|
||||

|
||||
|
||||
To open the settings, click on the checkbox field and then on the "Sliders" icon. That opens the settings panel on the right side of the screen.
|
||||
|
||||

|
||||

|
||||
|
||||
The checkbox field settings include the following:
|
||||
|
||||
@ -356,7 +356,7 @@ In this example, the checkbox field is configured as follows:
|
||||
- Option 3 (checked)
|
||||
- Empty value
|
||||
|
||||

|
||||

|
||||
|
||||
Since the field contains checkbox options, it displays them instead of the default checkbox field value, "Checkbox".
|
||||
|
||||
@ -366,7 +366,7 @@ What the recipient sees when they open the document to sign depends on the setti
|
||||
|
||||
In this case, the recipient sees the checkbox field signed with the values selected by the sender.
|
||||
|
||||

|
||||

|
||||
|
||||
Since the field is required, the recipient can either sign with the values selected by the sender or modify the values.
|
||||
|
||||
@ -377,11 +377,11 @@ The values can be modified in 2 ways:
|
||||
|
||||
The image below illustrates the checkbox field with the values cleared by the recipient. Since the field is required, it has a red border instead of the yellow one (non-required fields).
|
||||
|
||||

|
||||

|
||||
|
||||
Then, the recipient can select values other than the ones chosen by the sender.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing the field, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -393,11 +393,11 @@ The dropdown/select field collects a single choice from a list of options.
|
||||
|
||||
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
||||
|
||||

|
||||

|
||||
|
||||
To open the settings, click on the dropdown/select field and then on the "Sliders" icon. That opens the settings panel on the right side of the screen.
|
||||
|
||||

|
||||

|
||||
|
||||
The dropdown/select field settings include:
|
||||
|
||||
@ -433,14 +433,14 @@ What the recipient sees when they open the document to sign depends on the setti
|
||||
|
||||
In this case, the recipient sees the dropdown/select field with the default label, "-- Select ---" since the sender has not set a default value.
|
||||
|
||||

|
||||

|
||||
|
||||
The recipient can modify the dropdown/select field value since the field is not read-only. To change the value, the recipient must click on the field for the dropdown list to appear.
|
||||
|
||||

|
||||

|
||||
|
||||
The recipient can select one of the options from the list. The image below illustrates the dropdown/select field signed with a new value.
|
||||
|
||||

|
||||

|
||||
|
||||
After signing the field, the recipient can click the "Complete" button to complete the signing process.
|
||||
|
||||
@ -18,17 +18,17 @@ The guide assumes you have a Documenso account. If you don't, you can create a f
|
||||
|
||||
Navigate to the [Documenso dashboard](https://app.documenso.com/documents) and click on the "Add a document" button. Select the document you want to upload and wait for the upload to complete.
|
||||
|
||||

|
||||

|
||||
|
||||
After the upload is complete, you will be redirected to the document's page. You can configure the document's settings and add recipients and fields here.
|
||||
|
||||

|
||||

|
||||
|
||||
### (Optional) Advanced Options
|
||||
|
||||
Click on the "Advanced options" button to access additional settings for the document. You can set an external ID, date format, time zone, and the redirect URL.
|
||||
|
||||

|
||||

|
||||
|
||||
The external ID allows you to set a custom ID for the document that can be used to identify the document in your external system(s).
|
||||
|
||||
@ -45,7 +45,7 @@ The available options are:
|
||||
- **Require account** - The recipient must be signed in to view the document.
|
||||
- **None** - The document can be accessed directly by the URL sent to the recipient.
|
||||
|
||||

|
||||

|
||||
|
||||
<Callout type="info">
|
||||
The "Document Access" feature is only available for Enterprise accounts.
|
||||
@ -61,7 +61,7 @@ The available options are:
|
||||
- **Require 2FA** - The recipient must have an account and 2FA enabled via their settings.
|
||||
- **None** - No authentication required.
|
||||
|
||||

|
||||

|
||||
|
||||
This can be overridden by setting the authentication requirements directly for each recipient in the next step.
|
||||
|
||||
@ -75,11 +75,11 @@ Click the "+ Add Signer" button to add a new recipient. You can configure the re
|
||||
|
||||
You can choose any option from the ["Recipient Authentication"](#optional-recipient-authentication) section, or you can set it to "Inherit authentication method" to use the global action signing authentication method configured in the "General Settings" step.
|
||||
|
||||

|
||||

|
||||
|
||||
You can also set the recipient's role, which determines their actions and permissions in the document.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Roles
|
||||
|
||||
@ -96,7 +96,7 @@ Documenso has 4 roles for recipients with different permissions and actions.
|
||||
|
||||
Documenso supports 9 different field types that can be added to the document. Each field type collects various information from the recipients when they sign the document.
|
||||
|
||||

|
||||

|
||||
|
||||
The available field types are:
|
||||
|
||||
@ -121,13 +121,13 @@ All fields can be placed anywhere on the document and resized as needed.
|
||||
|
||||
Signer Roles require at least 1 signature field. You will get an error message if you try to send a document without a signature field.
|
||||
|
||||

|
||||

|
||||
|
||||
### Email Settings
|
||||
|
||||
Before sending the document, you can configure the email settings and customize the subject line, message, and sender name.
|
||||
|
||||

|
||||

|
||||
|
||||
If you leave the email subject and message empty, Documenso will use the default email template.
|
||||
|
||||
@ -135,13 +135,13 @@ If you leave the email subject and message empty, Documenso will use the default
|
||||
|
||||
After configuring the document, click the "Send" button to send the document to the recipients. The recipients will receive an email with a link to sign the document.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Signing Link
|
||||
|
||||
If you need to copy the signing link for each recipient, you can do so by clicking on the recipient whose link you want to copy. The signing link is copied automatically to your clipboard.
|
||||
|
||||

|
||||

|
||||
|
||||
The signing link has the following format:
|
||||
|
||||
|
||||
@ -10,15 +10,15 @@ Documenso allows you to create templates, which are reusable documents. Template
|
||||
|
||||
To create a new template, navigate to the ["Templates" page](https://app.documenso.com/templates) and click on the "New Template" button.
|
||||
|
||||

|
||||

|
||||
|
||||
Clicking on the "New Template" button opens a new modal to upload the document you want to use as a template. Select the document and wait for Documenso to upload it to your account.
|
||||
|
||||

|
||||

|
||||
|
||||
Once the upload is complete, Documenso opens the template configuration page.
|
||||
|
||||

|
||||

|
||||
|
||||
You can then configure the template by adding recipients, fields, and other options.
|
||||
|
||||
@ -28,7 +28,7 @@ When you send a document for signing, Documenso emails the recipients with a lin
|
||||
|
||||
Documenso uses a generic subject and message but also allows you to customize them for each document and template.
|
||||
|
||||

|
||||

|
||||
|
||||
To configure the email options, click the "Email Options" tab and fill in the subject and message fields. Every time you use this template for signing, Documenso will use the custom subject and message you provided. They can also be overridden before sending the document.
|
||||
|
||||
@ -36,7 +36,7 @@ To configure the email options, click the "Email Options" tab and fill in the su
|
||||
|
||||
The template also has advanced options that you can configure. These options include settings such as the external ID, date format, time zone and the redirect URL.
|
||||
|
||||

|
||||

|
||||
|
||||
The external ID allows you to set a custom ID for the document that can be used to identify the document in your external system(s).
|
||||
|
||||
@ -50,7 +50,7 @@ You can add placeholders for the template recipients. Placeholders specify where
|
||||
|
||||
You can also add recipients directly to the template. Recipients are the people who will receive the document for signing.
|
||||
|
||||

|
||||

|
||||
|
||||
If you add placeholders to the template, you must replace them with actual recipients when creating a document from it. See the modal from the ["Use a Template"](#use-a-template) section.
|
||||
|
||||
@ -70,7 +70,7 @@ Documenso provides the following field types:
|
||||
- **Checkbox** - Collects multiple choices from the signer
|
||||
- **Dropdown/Select** - Collects a single choice from a list of choices
|
||||
|
||||

|
||||

|
||||
|
||||
After adding the fields, press the "Save Template" button to save the template.
|
||||
|
||||
@ -85,7 +85,7 @@ Click on the "Use Template" button to create a new document from the template. B
|
||||
|
||||
After filling in the recipients, click the "Create Document" button to create the document in your account.
|
||||
|
||||

|
||||

|
||||
|
||||
You can also send the document straight to the recipients for signing by checking the "Send document" checkbox.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -20,8 +20,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/macro": "^4.11.3",
|
||||
"@lingui/react": "^4.11.3",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@openstatus/react": "^0.0.3",
|
||||
"cmdk": "^0.2.1",
|
||||
"contentlayer": "^0.3.4",
|
||||
@ -32,16 +31,16 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-plausible": "^3.10.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.77.3",
|
||||
"react": "^18",
|
||||
"react": "18.2.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.11.0",
|
||||
"recharts": "^2.7.2",
|
||||
@ -50,10 +49,18 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@lingui/loader": "^4.11.1",
|
||||
"@lingui/swc-plugin": "4.0.6",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7"
|
||||
},
|
||||
"overrides": {
|
||||
"next-auth": {
|
||||
"next": "$next"
|
||||
},
|
||||
"next-contentlayer": {
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ import { allDocuments } from 'contentlayer/generated';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
||||
@ -31,8 +29,6 @@ const mdxComponents: MDXComponents = {
|
||||
* Will render the document if it exists, otherwise will return a 404.
|
||||
*/
|
||||
export default function ContentPage({ params }: { params: { content: string } }) {
|
||||
setupI18nSSR();
|
||||
|
||||
const post = allDocuments.find((post) => post._raw.flattenedPath === params.content);
|
||||
|
||||
if (!post) {
|
||||
|
||||
@ -7,8 +7,6 @@ import { ChevronLeft } from 'lucide-react';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
import { CallToAction } from '~/components/(marketing)/call-to-action';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@ -49,8 +47,6 @@ const mdxComponents: MDXComponents = {
|
||||
};
|
||||
|
||||
export default function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
setupI18nSSR();
|
||||
|
||||
const post = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
|
||||
|
||||
if (!post) {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
import { SinglePlayerClient } from './client';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -15,7 +13,5 @@ export const dynamic = 'force-dynamic';
|
||||
// !: the Single Player Mode page. This regression was introduced during
|
||||
// !: the upgrade of Next.js to v13.5.x.
|
||||
export default function SingleplayerPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
return <SinglePlayerClient />;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -23,8 +23,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/macro": "^4.11.3",
|
||||
"@lingui/react": "^4.11.3",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"@tanstack/react-query": "^4.29.5",
|
||||
@ -35,7 +34,7 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-plausible": "^3.10.1",
|
||||
@ -44,9 +43,8 @@
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.75.3",
|
||||
"posthog-node": "^3.1.1",
|
||||
"react": "^18",
|
||||
"react-call": "^1.3.0",
|
||||
"react-dom": "^18",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
@ -62,16 +60,24 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@lingui/loader": "^4.11.1",
|
||||
"@lingui/swc-plugin": "4.0.6",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"next-auth": {
|
||||
"next": "$next"
|
||||
},
|
||||
"next-contentlayer": {
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
const { token } = await createDocumentFromDirectTemplate({
|
||||
const token = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
|
||||
@ -63,7 +63,6 @@ export const SigningPageView = ({
|
||||
{document.User.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
|
||||
export type EmbedAuthenticateViewProps = {
|
||||
email?: string;
|
||||
returnTo: string;
|
||||
};
|
||||
|
||||
export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => {
|
||||
return (
|
||||
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
||||
<div className="flex w-full max-w-md flex-col">
|
||||
<Logo className="h-8" />
|
||||
|
||||
<Alert className="mt-8" variant="warning">
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
To view this document you need to be signed into your account, please sign in to
|
||||
continue.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<SignInForm className="mt-4" initialEmail={email} returnTo={returnTo} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZBaseEmbedDataSchema = z.object({
|
||||
css: z.string().optional().transform(value => value || undefined),
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
export const EmbedClientLoading = () => {
|
||||
return (
|
||||
<div className="bg-background fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,33 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
import type { Signature } from '@documenso/prisma/client';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
|
||||
export type EmbedDocumentCompletedPageProps = {
|
||||
name?: string;
|
||||
signature?: Signature;
|
||||
};
|
||||
|
||||
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
<h3 className="text-foreground text-2xl font-semibold">
|
||||
<Trans>Document Completed!</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="mt-8 w-full max-w-md">
|
||||
<SigningCard3D
|
||||
className='w-full mx-auto'
|
||||
name={name || 'Documenso'}
|
||||
signature={signature}
|
||||
signingCelebrationImage={signingCelebration}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
|
||||
<Trans>The document is now completed, please follow any instructions provided within the parent application.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,456 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { FieldType, type DocumentData, type Field } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
import { EmbedDocumentFields } from '../../document-fields';
|
||||
import { ZDirectTemplateEmbedDataSchema } from './schema';
|
||||
|
||||
export type EmbedDirectTemplateClientPageProps = {
|
||||
token: string;
|
||||
updatedAt: Date;
|
||||
documentData: DocumentData;
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
};
|
||||
|
||||
export const EmbedDirectTemplateClientPage = ({
|
||||
token,
|
||||
updatedAt,
|
||||
documentData,
|
||||
recipient,
|
||||
fields,
|
||||
metadata,
|
||||
}: EmbedDirectTemplateClientPageProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { fullName, email, signature, setFullName, setEmail, setSignature } =
|
||||
useRequiredSigningContext();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(false);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const [isEmailLocked, setIsEmailLocked] = useState(false);
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||
|
||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
||||
|
||||
const [pendingFields, _completedFields] = [
|
||||
localFields.filter((field) => !field.inserted),
|
||||
localFields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
|
||||
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
||||
|
||||
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
|
||||
setLocalFields((fields) =>
|
||||
fields.map((field) => {
|
||||
if (field.id !== payload.fieldId) {
|
||||
return field;
|
||||
}
|
||||
|
||||
const newField: DirectTemplateLocalField = structuredClone({
|
||||
...field,
|
||||
customText: payload.value,
|
||||
inserted: true,
|
||||
signedValue: payload,
|
||||
});
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
newField.Signature = {
|
||||
id: 1,
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
fieldId: 1,
|
||||
signatureImageAsBase64: payload.value,
|
||||
typedSignature: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DATE) {
|
||||
newField.customText = DateTime.now()
|
||||
.setZone(metadata?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
|
||||
.toFormat(metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||
}
|
||||
|
||||
return newField;
|
||||
}),
|
||||
);
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'field-signed',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setShowPendingFieldTooltip(false);
|
||||
};
|
||||
|
||||
const onUnsignField = (payload: TRemovedSignedFieldWithTokenMutationSchema) => {
|
||||
setLocalFields((fields) =>
|
||||
fields.map((field) => {
|
||||
if (field.id !== payload.fieldId) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return structuredClone({
|
||||
...field,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
signedValue: undefined,
|
||||
Signature: undefined,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'field-unsigned',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setShowPendingFieldTooltip(false);
|
||||
};
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
validateFieldsInserted(localFields);
|
||||
|
||||
setShowPendingFieldTooltip(true);
|
||||
setIsExpanded(false);
|
||||
};
|
||||
|
||||
const onCompleteClick = async () => {
|
||||
try {
|
||||
const valid = validateFieldsInserted(localFields);
|
||||
|
||||
if (!valid) {
|
||||
setShowPendingFieldTooltip(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
|
||||
|
||||
if (directTemplateExternalId) {
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
localFields.forEach((field) => {
|
||||
if (!field.signedValue) {
|
||||
throw new Error('Invalid configuration');
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
documentId,
|
||||
token: documentToken,
|
||||
recipientId,
|
||||
} = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken: token,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
directRecipientEmail: email,
|
||||
templateUpdatedAt: updatedAt,
|
||||
signedFieldValues: localFields.map((field) => {
|
||||
if (!field.signedValue) {
|
||||
throw new Error('Invalid configuration');
|
||||
}
|
||||
|
||||
return field.signedValue;
|
||||
}),
|
||||
});
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-completed',
|
||||
data: {
|
||||
token: documentToken,
|
||||
documentId,
|
||||
recipientId,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setHasCompletedDocument(true);
|
||||
} catch (err) {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-error',
|
||||
data: String(err),
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We were unable to submit this document at this time. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
try {
|
||||
const data = ZDirectTemplateEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||
|
||||
if (data.email) {
|
||||
setEmail(data.email);
|
||||
setIsEmailLocked(!!data.lockEmail);
|
||||
}
|
||||
|
||||
if (data.name) {
|
||||
setFullName(data.name);
|
||||
setIsNameLocked(!!data.lockName);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
|
||||
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||
// !: re-renders.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFinishedInit && hasDocumentLoaded && window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-ready',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||
|
||||
if (hasCompletedDocument) {
|
||||
return (
|
||||
<EmbedDocumentCompleted
|
||||
name={fullName}
|
||||
signature={{
|
||||
id: 1,
|
||||
fieldId: 1,
|
||||
recipientId: 1,
|
||||
created: new Date(),
|
||||
typedSignature: null,
|
||||
signatureImageAsBase64: signature,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||
|
||||
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
|
||||
{/* Viewer */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widget */}
|
||||
<div
|
||||
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="w-full border-border bg-widget flex md:min-h-[min(calc(100dvh-2rem),48rem)] h-fit flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
|
||||
<Trans>Sign document</Trans>
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Sign the document to complete the process.</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
disabled={isNameLocked}
|
||||
value={fullName}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">
|
||||
<Trans>Email</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-background mt-2"
|
||||
disabled={isEmailLocked}
|
||||
value={email}
|
||||
onChange={(e) => !isEmailLocked && setEmail(e.target.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
key={signature}
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
|
||||
|
||||
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
|
||||
{pendingFields.length > 0 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="col-start-2"
|
||||
disabled={isThrottled}
|
||||
loading={isSubmitting}
|
||||
onClick={() => throttledOnCompleteClick()}
|
||||
>
|
||||
<Trans>Complete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</ElementVisible>
|
||||
|
||||
{/* Fields */}
|
||||
<EmbedDocumentFields
|
||||
recipient={recipient}
|
||||
fields={localFields}
|
||||
metadata={metadata}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export default function EmbedDirectTemplateNotFound() {
|
||||
return <div>Not Found</div>
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||
|
||||
import { EmbedAuthenticateView } from '../../authenticate';
|
||||
import { EmbedPaywall } from '../../paywall';
|
||||
import { EmbedDirectTemplateClientPage } from './client';
|
||||
|
||||
export type EmbedDirectTemplatePageProps = {
|
||||
params: {
|
||||
url?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTemplatePageProps) {
|
||||
if (params.url?.length !== 1) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [token] = params.url;
|
||||
|
||||
const template = await getTemplateByDirectLinkToken({
|
||||
token,
|
||||
}).catch(() => null);
|
||||
|
||||
// `template.directLink` is always available but we're doing this to
|
||||
// satisfy the type checker.
|
||||
if (!template || !template.directLink) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// TODO: Make this more robust, we need to ensure the owner is either
|
||||
// TODO: the member of a team that has an active subscription, is an early
|
||||
// TODO: adopter or is an enterprise user.
|
||||
if (IS_BILLING_ENABLED() && !template.teamId) {
|
||||
return <EmbedPaywall />;
|
||||
}
|
||||
|
||||
const { user } = await getServerComponentSession();
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
});
|
||||
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
||||
.with(null, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
return <EmbedAuthenticateView email={user?.email} returnTo={`/embed/direct/${token}`} />;
|
||||
}
|
||||
|
||||
const { directTemplateRecipientId } = template.directLink;
|
||||
|
||||
const recipient = template.Recipient.find(
|
||||
(recipient) => recipient.id === directTemplateRecipientId,
|
||||
);
|
||||
|
||||
if (!recipient) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
|
||||
|
||||
return (
|
||||
<SigningProvider
|
||||
email={user?.email}
|
||||
fullName={user?.name}
|
||||
signature={user?.signature}
|
||||
>
|
||||
<DocumentAuthProvider
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
>
|
||||
<EmbedDirectTemplateClientPage
|
||||
token={token}
|
||||
updatedAt={template.updatedAt}
|
||||
documentData={template.templateDocumentData}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
);
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZBaseEmbedDataSchema } from '../../base-schema';
|
||||
|
||||
export const ZDirectTemplateEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
||||
email: z
|
||||
.union([z.literal(''), z.string().email()])
|
||||
.optional()
|
||||
.transform((value) => value || undefined),
|
||||
lockEmail: z.boolean().optional().default(false),
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value || undefined),
|
||||
lockName: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type TDirectTemplateEmbedDataSchema = z.infer<typeof ZDirectTemplateEmbedDataSchema>;
|
||||
|
||||
export type TDirectTemplateEmbedDataInputSchema = z.input<typeof ZDirectTemplateEmbedDataSchema>;
|
||||
@ -1,185 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { FieldType, type Field } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
|
||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
||||
|
||||
export type EmbedDocumentFieldsProps = {
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const EmbedDocumentFields = ({
|
||||
recipient,
|
||||
fields,
|
||||
metadata,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: EmbedDocumentFieldsProps) => {
|
||||
return (
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.DATE, () => (
|
||||
<DateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||
timezone={metadata?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => (
|
||||
<EmailField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.TEXT, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with(FieldType.NUMBER, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<NumberField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with(FieldType.RADIO, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<RadioField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with(FieldType.CHECKBOX, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<CheckboxField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with(FieldType.DROPDOWN, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.otherwise(() => null),
|
||||
)}
|
||||
</ElementVisible>
|
||||
);
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export const EmbedPaywall = () => {
|
||||
return <div>
|
||||
<h1>Paywall</h1>
|
||||
</div>
|
||||
}
|
||||
@ -1,327 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { type DocumentData, type Field } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
import { EmbedDocumentFields } from '../../document-fields';
|
||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||
|
||||
export type EmbedSignDocumentClientPageProps = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
documentData: DocumentData;
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
export const EmbedSignDocumentClientPage = ({
|
||||
token,
|
||||
documentId,
|
||||
documentData,
|
||||
recipient,
|
||||
fields,
|
||||
metadata,
|
||||
isCompleted,
|
||||
}: EmbedSignDocumentClientPageProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { fullName, email, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||
|
||||
const [pendingFields, _completedFields] = [
|
||||
fields.filter((field) => !field.inserted),
|
||||
fields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
validateFieldsInserted(fields);
|
||||
|
||||
setShowPendingFieldTooltip(true);
|
||||
setIsExpanded(false);
|
||||
};
|
||||
|
||||
const onCompleteClick = async () => {
|
||||
try {
|
||||
const valid = validateFieldsInserted(fields);
|
||||
|
||||
if (!valid) {
|
||||
setShowPendingFieldTooltip(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await completeDocumentWithToken({
|
||||
documentId,
|
||||
token,
|
||||
});
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-completed',
|
||||
data: {
|
||||
token,
|
||||
documentId,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setHasCompletedDocument(true);
|
||||
} catch (err) {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-error',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We were unable to submit this document at this time. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
try {
|
||||
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||
|
||||
if (!isCompleted && data.name) {
|
||||
setFullName(data.name);
|
||||
}
|
||||
|
||||
// Since a recipient can be provided a name we can lock it without requiring
|
||||
// a to be provided by the parent application, unlike direct templates.
|
||||
setIsNameLocked(!!data.lockName);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
|
||||
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||
// !: re-renders.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFinishedInit && hasDocumentLoaded && window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-ready',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||
|
||||
if (hasCompletedDocument) {
|
||||
return (
|
||||
<EmbedDocumentCompleted
|
||||
name={fullName}
|
||||
signature={{
|
||||
id: 1,
|
||||
fieldId: 1,
|
||||
recipientId: 1,
|
||||
created: new Date(),
|
||||
typedSignature: null,
|
||||
signatureImageAsBase64: signature,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||
|
||||
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
|
||||
{/* Viewer */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widget */}
|
||||
<div
|
||||
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="w-full border-border bg-widget flex flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
|
||||
<Trans>Sign document</Trans>
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Sign the document to complete the process.</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
disabled={isNameLocked}
|
||||
value={fullName}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">
|
||||
<Trans>Email</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-background mt-2"
|
||||
value={email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
key={signature}
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
|
||||
|
||||
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
|
||||
{pendingFields.length > 0 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="col-start-2"
|
||||
disabled={isThrottled}
|
||||
loading={isSubmitting}
|
||||
onClick={() => throttledOnCompleteClick()}
|
||||
>
|
||||
<Trans>Complete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</ElementVisible>
|
||||
|
||||
{/* Fields */}
|
||||
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export default function EmbedDirectTemplateNotFound() {
|
||||
return <div>Not Found</div>
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||
|
||||
import { EmbedAuthenticateView } from '../../authenticate';
|
||||
import { EmbedPaywall } from '../../paywall';
|
||||
import { EmbedSignDocumentClientPage } from './client';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type EmbedSignDocumentPageProps = {
|
||||
params: {
|
||||
url?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumentPageProps) {
|
||||
if (params.url?.length !== 1) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [token] = params.url;
|
||||
|
||||
const { user } = await getServerComponentSession();
|
||||
|
||||
const [document, fields, recipient] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
userId: user?.id,
|
||||
requireAccessAuth: false,
|
||||
}).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
]);
|
||||
|
||||
// `document.directLink` is always available but we're doing this to
|
||||
// satisfy the type checker.
|
||||
if (!document || !recipient) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// TODO: Make this more robust, we need to ensure the owner is either
|
||||
// TODO: the member of a team that has an active subscription, is an early
|
||||
// TODO: adopter or is an enterprise user.
|
||||
if (IS_BILLING_ENABLED() && !document.teamId) {
|
||||
return <EmbedPaywall />;
|
||||
}
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
||||
.with(null, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
return <EmbedAuthenticateView email={user?.email || recipient.email} returnTo={`/embed/direct/${token}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SigningProvider
|
||||
email={recipient.email}
|
||||
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
||||
signature={user?.email === recipient.email ? user?.signature : undefined}
|
||||
>
|
||||
<DocumentAuthProvider
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
>
|
||||
<EmbedSignDocumentClientPage
|
||||
token={token}
|
||||
documentId={document.id}
|
||||
documentData={document.documentData}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={document.documentMeta}
|
||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZBaseEmbedDataSchema } from '../../base-schema';
|
||||
|
||||
export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
||||
email: z
|
||||
.union([z.literal(''), z.string().email()])
|
||||
.optional()
|
||||
.transform((value) => value || undefined),
|
||||
lockEmail: z.boolean().optional().default(false),
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value || undefined),
|
||||
lockName: z.boolean().optional().default(false),
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@ -74,7 +74,6 @@ export type SignInFormProps = {
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
oidcProviderLabel?: string;
|
||||
returnTo?: string;
|
||||
};
|
||||
|
||||
export const SignInForm = ({
|
||||
@ -83,7 +82,6 @@ export const SignInForm = ({
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
oidcProviderLabel,
|
||||
returnTo,
|
||||
}: SignInFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -102,22 +100,6 @@ export const SignInForm = ({
|
||||
|
||||
const isPasskeyEnabled = getFlag('app_passkey');
|
||||
|
||||
const callbackUrl = useMemo(() => {
|
||||
// Handle SSR
|
||||
if (typeof window === 'undefined') {
|
||||
return LOGIN_REDIRECT_PATH;
|
||||
}
|
||||
|
||||
let url = new URL(returnTo || LOGIN_REDIRECT_PATH, window.location.origin);
|
||||
|
||||
// Don't allow different origins
|
||||
if (url.origin !== window.location.origin) {
|
||||
url = new URL(LOGIN_REDIRECT_PATH, window.location.origin);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}, [returnTo]);
|
||||
|
||||
const { mutateAsync: createPasskeySigninOptions } =
|
||||
trpc.auth.createPasskeySigninOptions.useMutation();
|
||||
|
||||
@ -175,7 +157,7 @@ export const SignInForm = ({
|
||||
|
||||
const result = await signIn('webauthn', {
|
||||
credential: JSON.stringify(credential),
|
||||
callbackUrl,
|
||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
@ -228,7 +210,7 @@ export const SignInForm = ({
|
||||
|
||||
const result = await signIn('credentials', {
|
||||
...credentials,
|
||||
callbackUrl,
|
||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
@ -277,9 +259,7 @@ export const SignInForm = ({
|
||||
|
||||
const onSignInWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', {
|
||||
callbackUrl,
|
||||
});
|
||||
await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -293,9 +273,7 @@ export const SignInForm = ({
|
||||
|
||||
const onSignInWithOIDCClick = async () => {
|
||||
try {
|
||||
await signIn('oidc', {
|
||||
callbackUrl,
|
||||
});
|
||||
await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
|
||||
@ -76,20 +76,6 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (req.nextUrl.pathname.startsWith('/embed')) {
|
||||
const res = NextResponse.next();
|
||||
|
||||
// Allow third parties to iframe the document.
|
||||
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.headers.set('Access-Control-Allow-Origin', '*');
|
||||
res.headers.set('Content-Security-Policy', "frame-ancestors *");
|
||||
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
res.headers.set('X-Frame-Options', 'ALLOW-ALL');
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
765
package-lock.json
generated
765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.0-rc.2",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
@ -43,7 +43,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.7.1",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@lingui/cli": "^4.11.3",
|
||||
"@lingui/cli": "^4.11.1",
|
||||
"@trigger.dev/cli": "^2.3.18",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
@ -63,13 +63,20 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@documenso/pdf-sign": "^0.1.0",
|
||||
"@lingui/core": "^4.11.3",
|
||||
"@lingui/core": "^4.11.1",
|
||||
"@lingui/macro": "^4.11.2",
|
||||
"inngest-cli": "^0.29.1",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"react": "^18"
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"next": "14.2.6"
|
||||
"next-auth": {
|
||||
"next": "14.0.3"
|
||||
},
|
||||
"next-contentlayer": {
|
||||
"next": "14.0.3"
|
||||
},
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
|
||||
@ -21,7 +21,6 @@ import {
|
||||
ZSendDocumentForSigningMutationSchema,
|
||||
ZSuccessfulDeleteTemplateResponseSchema,
|
||||
ZSuccessfulDocumentResponseSchema,
|
||||
ZSuccessfulFieldCreationResponseSchema,
|
||||
ZSuccessfulFieldResponseSchema,
|
||||
ZSuccessfulGetDocumentResponseSchema,
|
||||
ZSuccessfulGetTemplateResponseSchema,
|
||||
@ -237,7 +236,7 @@ export const ApiContractV1 = c.router(
|
||||
path: '/api/v1/documents/:id/fields',
|
||||
body: ZCreateFieldMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulFieldCreationResponseSchema,
|
||||
200: ZSuccessfulFieldResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { createNextRoute } from '@ts-rest/next';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
@ -16,6 +15,7 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
|
||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { createField } from '@documenso/lib/server-only/field/create-field';
|
||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||
import { updateField } from '@documenso/lib/server-only/field/update-field';
|
||||
@ -32,13 +32,6 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
@ -46,8 +39,6 @@ import {
|
||||
getPresignGetUrl,
|
||||
getPresignPostUrl,
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
@ -879,167 +870,100 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
createField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
const fields = Array.isArray(args.body) ? args.body : [args.body];
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
|
||||
args.body;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
select: { id: true, status: true },
|
||||
where: {
|
||||
id: Number(documentId),
|
||||
...(team?.id
|
||||
? {
|
||||
team: {
|
||||
id: team.id,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
if (pageNumber <= 0) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid page number',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: { message: 'Document not found' },
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { message: 'Document is already completed' },
|
||||
body: {
|
||||
message: 'Document is already completed',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const recipient = await getRecipientById({
|
||||
id: Number(recipientId),
|
||||
documentId: Number(documentId),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!recipient) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Recipient not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Recipient has already signed the document',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return Promise.all(
|
||||
fields.map(async (fieldData) => {
|
||||
const {
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
pageX,
|
||||
pageY,
|
||||
fieldMeta,
|
||||
} = fieldData;
|
||||
|
||||
if (pageNumber <= 0) {
|
||||
throw new Error('Invalid page number');
|
||||
}
|
||||
|
||||
const recipient = await getRecipientById({
|
||||
id: Number(recipientId),
|
||||
documentId: Number(documentId),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error('Recipient has already signed the document');
|
||||
}
|
||||
|
||||
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
|
||||
type,
|
||||
);
|
||||
|
||||
if (advancedField && !fieldMeta) {
|
||||
throw new Error(
|
||||
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
|
||||
throw new Error('Field meta type does not match the field type');
|
||||
}
|
||||
|
||||
const result = match(type)
|
||||
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
|
||||
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
|
||||
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
|
||||
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
|
||||
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
||||
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
||||
success: true,
|
||||
data: {},
|
||||
}))
|
||||
.with('FREE_SIGNATURE', () => ({
|
||||
success: false,
|
||||
error: 'FREE_SIGNATURE is not supported',
|
||||
data: {},
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Field meta parsing failed');
|
||||
}
|
||||
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: result.data,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_CREATED',
|
||||
documentId: Number(documentId),
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
id: field.id,
|
||||
documentId: Number(field.documentId),
|
||||
recipientId: field.recipientId ?? -1,
|
||||
type: field.type,
|
||||
pageNumber: field.page,
|
||||
pageX: Number(field.positionX),
|
||||
pageY: Number(field.positionY),
|
||||
pageWidth: Number(field.width),
|
||||
pageHeight: Number(field.height),
|
||||
customText: field.customText,
|
||||
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
|
||||
inserted: field.inserted,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const field = await createField({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
fieldMeta,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
const remappedField = {
|
||||
id: field.id,
|
||||
documentId: field.documentId,
|
||||
recipientId: field.recipientId ?? -1,
|
||||
type: field.type,
|
||||
pageNumber: field.page,
|
||||
pageX: Number(field.positionX),
|
||||
pageY: Number(field.positionY),
|
||||
pageWidth: Number(field.width),
|
||||
pageHeight: Number(field.height),
|
||||
customText: field.customText,
|
||||
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
|
||||
inserted: field.inserted,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
fields: createdFields,
|
||||
...remappedField,
|
||||
documentId: Number(documentId),
|
||||
},
|
||||
};
|
||||
@ -1050,8 +974,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
updateField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, fieldId } = args.params;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
|
||||
args.body;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@ -1113,7 +1036,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
fieldMeta: fieldMeta ? ZFieldMetaSchema.parse(fieldMeta) : undefined,
|
||||
});
|
||||
|
||||
const remappedField = {
|
||||
|
||||
@ -293,7 +293,7 @@ export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecip
|
||||
/**
|
||||
* Fields
|
||||
*/
|
||||
const ZCreateFieldSchema = z.object({
|
||||
export const ZCreateFieldMutationSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
pageNumber: z.number(),
|
||||
@ -301,17 +301,12 @@ const ZCreateFieldSchema = z.object({
|
||||
pageY: z.number(),
|
||||
pageWidth: z.number(),
|
||||
pageHeight: z.number(),
|
||||
fieldMeta: ZFieldMetaSchema.openapi({}),
|
||||
fieldMeta: ZFieldMetaSchema,
|
||||
});
|
||||
|
||||
export const ZCreateFieldMutationSchema = z.union([
|
||||
ZCreateFieldSchema,
|
||||
z.array(ZCreateFieldSchema).min(1),
|
||||
]);
|
||||
|
||||
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
|
||||
|
||||
export const ZUpdateFieldMutationSchema = ZCreateFieldSchema.partial();
|
||||
export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial();
|
||||
|
||||
export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>;
|
||||
|
||||
@ -319,26 +314,6 @@ export const ZDeleteFieldMutationSchema = null;
|
||||
|
||||
export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
|
||||
|
||||
const ZSuccessfulFieldSchema = z.object({
|
||||
id: z.number(),
|
||||
documentId: z.number(),
|
||||
recipientId: z.number(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
pageNumber: z.number(),
|
||||
pageX: z.number(),
|
||||
pageY: z.number(),
|
||||
pageWidth: z.number(),
|
||||
pageHeight: z.number(),
|
||||
customText: z.string(),
|
||||
fieldMeta: ZFieldMetaSchema,
|
||||
inserted: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZSuccessfulFieldCreationResponseSchema = z.object({
|
||||
fields: z.union([ZSuccessfulFieldSchema, z.array(ZSuccessfulFieldSchema)]),
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export const ZSuccessfulFieldResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
documentId: z.number(),
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.24.5",
|
||||
"react": "^18",
|
||||
"react": "18.2.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
type ThrottleOptions = {
|
||||
leading?: boolean;
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
ms = 500,
|
||||
options: ThrottleOptions = {}
|
||||
): [(...args: Parameters<T>) => void, boolean, () => void] {
|
||||
const [isThrottling, setIsThrottling] = useState(false);
|
||||
const $isThrottling = useRef(false);
|
||||
|
||||
const $timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const $lastArgs = useRef<Parameters<T> | null>(null);
|
||||
|
||||
const { leading = true, trailing = true } = options;
|
||||
|
||||
const $setIsThrottling = useCallback((value: boolean) => {
|
||||
$isThrottling.current = value;
|
||||
setIsThrottling(value);
|
||||
}, []);
|
||||
|
||||
const throttledFn = useCallback(
|
||||
(...args: Parameters<T>) => {
|
||||
if (!$isThrottling.current) {
|
||||
$setIsThrottling(true);
|
||||
if (leading) {
|
||||
fn(...args);
|
||||
} else {
|
||||
$lastArgs.current = args;
|
||||
}
|
||||
|
||||
$timeout.current = setTimeout(() => {
|
||||
if (trailing && $lastArgs.current) {
|
||||
fn(...$lastArgs.current);
|
||||
$lastArgs.current = null;
|
||||
}
|
||||
$setIsThrottling(false);
|
||||
}, ms);
|
||||
} else {
|
||||
$lastArgs.current = args;
|
||||
}
|
||||
},
|
||||
[fn, ms, leading, trailing, $setIsThrottling]
|
||||
);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
if ($timeout.current) {
|
||||
clearTimeout($timeout.current);
|
||||
$timeout.current = null;
|
||||
$setIsThrottling(false);
|
||||
$lastArgs.current = null;
|
||||
}
|
||||
}, [$setIsThrottling]);
|
||||
|
||||
return [throttledFn, isThrottling, cancel];
|
||||
}
|
||||
@ -41,13 +41,13 @@
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.24.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.11.3",
|
||||
"playwright": "1.43.0",
|
||||
"react": "^18",
|
||||
"react": "18.2.0",
|
||||
"remeda": "^1.27.1",
|
||||
"sharp": "0.32.6",
|
||||
"stripe": "^12.7.0",
|
||||
|
||||
@ -147,7 +147,7 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const [recipient] = result.Recipient;
|
||||
const recipient = result.Recipient[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
|
||||
@ -110,21 +110,24 @@ export const createField = async ({
|
||||
}
|
||||
|
||||
const result = match(type)
|
||||
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
|
||||
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
|
||||
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
|
||||
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
|
||||
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
||||
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
||||
success: true,
|
||||
data: {},
|
||||
}))
|
||||
.with('FREE_SIGNATURE', () => ({
|
||||
success: false,
|
||||
error: 'FREE_SIGNATURE is not supported',
|
||||
data: {},
|
||||
}))
|
||||
.exhaustive();
|
||||
.with('RADIO', () => {
|
||||
return ZRadioFieldMeta.safeParse(fieldMeta);
|
||||
})
|
||||
.with('CHECKBOX', () => {
|
||||
return ZCheckboxFieldMeta.safeParse(fieldMeta);
|
||||
})
|
||||
.with('DROPDOWN', () => {
|
||||
return ZDropdownFieldMeta.safeParse(fieldMeta);
|
||||
})
|
||||
.with('NUMBER', () => {
|
||||
return ZNumberFieldMeta.safeParse(fieldMeta);
|
||||
})
|
||||
.with('TEXT', () => {
|
||||
return ZTextFieldMeta.safeParse(fieldMeta);
|
||||
})
|
||||
.otherwise(() => {
|
||||
return { success: false, data: {} };
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Field meta parsing failed');
|
||||
@ -142,7 +145,7 @@ export const createField = async ({
|
||||
height: pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: result.data,
|
||||
fieldMeta: advancedField ? result.data : undefined,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
|
||||
@ -37,10 +37,6 @@ export const updateField = async ({
|
||||
requestMetadata,
|
||||
fieldMeta,
|
||||
}: UpdateFieldOptions) => {
|
||||
if (type === 'FREE_SIGNATURE') {
|
||||
throw new Error('Cannot update a FREE_SIGNATURE field');
|
||||
}
|
||||
|
||||
const oldField = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
@ -65,11 +61,6 @@ export const updateField = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const newFieldMeta = {
|
||||
...(oldField.fieldMeta as FieldMeta),
|
||||
...fieldMeta,
|
||||
};
|
||||
|
||||
const field = prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
@ -83,39 +74,13 @@ export const updateField = async ({
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta: newFieldMeta,
|
||||
fieldMeta,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
@ -139,5 +104,31 @@ export const updateField = async ({
|
||||
return updatedField;
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type GetActiveSubscriptionsByUserIdOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const getActiveSubscriptionsByUserId = async ({
|
||||
userId,
|
||||
}: GetActiveSubscriptionsByUserIdOptions) => {
|
||||
return await prisma.subscription.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: {
|
||||
not: SubscriptionStatus.INACTIVE,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -210,7 +210,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const initialRequestTime = new Date();
|
||||
|
||||
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
const { documentId, directRecipientToken } = await prisma.$transaction(async (tx) => {
|
||||
const documentData = await tx.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
@ -539,9 +539,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
token: createdDirectRecipient.token,
|
||||
documentId: document.id,
|
||||
recipientId: createdDirectRecipient.id,
|
||||
directRecipientToken: createdDirectRecipient.token,
|
||||
};
|
||||
});
|
||||
|
||||
@ -560,9 +559,5 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
// Log and reseal as required until we configure middleware.
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
documentId,
|
||||
recipientId,
|
||||
};
|
||||
return directRecipientToken;
|
||||
};
|
||||
|
||||
@ -116,7 +116,7 @@ msgid "Advanced Options"
|
||||
msgstr "Erweiterte Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:369
|
||||
msgid "Advanced settings"
|
||||
msgstr "Erweiterte Einstellungen"
|
||||
|
||||
@ -140,6 +140,14 @@ msgstr "Genehmiger"
|
||||
msgid "Approving"
|
||||
msgstr "Genehmigung"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:271
|
||||
msgid "Black"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:298
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
|
||||
msgid "Cancel"
|
||||
@ -167,7 +175,7 @@ msgid "Character Limit"
|
||||
msgstr "Zeichenbeschränkung"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:756
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:755
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
|
||||
@ -179,7 +187,7 @@ msgstr "Checkbox-Werte"
|
||||
msgid "Clear filters"
|
||||
msgstr "Filter löschen"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:321
|
||||
msgid "Clear Signature"
|
||||
msgstr "Unterschrift löschen"
|
||||
|
||||
@ -196,7 +204,7 @@ msgid "Configure Direct Recipient"
|
||||
msgstr "Direkten Empfänger konfigurieren"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:371
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Konfigurieren Sie das Feld {0}"
|
||||
|
||||
@ -213,7 +221,7 @@ msgid "Custom Text"
|
||||
msgstr "Benutzerdefinierter Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:652
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:651
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
|
||||
@ -245,7 +253,7 @@ msgid "Drag & drop your PDF here."
|
||||
msgstr "Ziehen Sie Ihr PDF hierher."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:782
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:781
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
|
||||
@ -257,7 +265,7 @@ msgstr "Dropdown-Optionen"
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:232
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:239
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:600
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:599
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
|
||||
msgid "Email"
|
||||
@ -312,6 +320,10 @@ msgstr "Globale Empfängerauthentifizierung"
|
||||
msgid "Go Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:307
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:72
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Ich bin ein Unterzeichner dieses Dokuments"
|
||||
@ -367,7 +379,7 @@ msgstr "Min"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:626
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:625
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
|
||||
msgid "Name"
|
||||
@ -386,12 +398,12 @@ msgid "Needs to view"
|
||||
msgstr "Muss sehen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:465
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:464
|
||||
msgid "No recipient matching this description was found."
|
||||
msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:629
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:481
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:480
|
||||
msgid "No recipients with this role"
|
||||
msgstr "Keine Empfänger mit dieser Rolle"
|
||||
|
||||
@ -416,7 +428,7 @@ msgid "No value found."
|
||||
msgstr "Kein Wert gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:704
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:703
|
||||
msgid "Number"
|
||||
msgstr "Nummer"
|
||||
|
||||
@ -451,7 +463,7 @@ msgid "Placeholder"
|
||||
msgstr "Platzhalter"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:730
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:729
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
|
||||
@ -477,6 +489,10 @@ msgstr "Erhält Kopie"
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Empfängeraktion Authentifizierung"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:289
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
@ -502,7 +518,7 @@ msgstr "Zeilen pro Seite"
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:798
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:797
|
||||
msgid "Save Template"
|
||||
msgstr "Vorlage speichern"
|
||||
|
||||
@ -552,7 +568,7 @@ msgstr "Unterschreiben"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:548
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:547
|
||||
msgid "Signature"
|
||||
msgstr "Unterschrift"
|
||||
|
||||
@ -598,7 +614,7 @@ msgid "Template title"
|
||||
msgstr "Vorlagentitel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:678
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:677
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@ -733,6 +749,10 @@ msgstr "Viewer"
|
||||
msgid "Viewing"
|
||||
msgstr "Viewing"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
|
||||
msgid "White"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
|
||||
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
|
||||
msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -26,15 +26,15 @@ msgstr ""
|
||||
msgid "\"{documentTitle}\" has been successfully deleted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:76
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:75
|
||||
msgid "({0}) has invited you to approve this document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:73
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:72
|
||||
msgid "({0}) has invited you to sign this document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:70
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:69
|
||||
msgid "({0}) has invited you to view this document"
|
||||
msgstr ""
|
||||
|
||||
@ -500,11 +500,11 @@ msgstr ""
|
||||
#: apps/web/src/components/forms/public-profile-claim-dialog.tsx:113
|
||||
#: apps/web/src/components/forms/public-profile-form.tsx:104
|
||||
#: apps/web/src/components/forms/reset-password.tsx:87
|
||||
#: apps/web/src/components/forms/signin.tsx:248
|
||||
#: apps/web/src/components/forms/signin.tsx:256
|
||||
#: apps/web/src/components/forms/signin.tsx:270
|
||||
#: apps/web/src/components/forms/signin.tsx:285
|
||||
#: apps/web/src/components/forms/signin.tsx:301
|
||||
#: apps/web/src/components/forms/signin.tsx:230
|
||||
#: apps/web/src/components/forms/signin.tsx:238
|
||||
#: apps/web/src/components/forms/signin.tsx:252
|
||||
#: apps/web/src/components/forms/signin.tsx:265
|
||||
#: apps/web/src/components/forms/signin.tsx:279
|
||||
#: apps/web/src/components/forms/signup.tsx:113
|
||||
#: apps/web/src/components/forms/signup.tsx:128
|
||||
#: apps/web/src/components/forms/signup.tsx:142
|
||||
@ -608,7 +608,7 @@ msgid "Background Color"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:167
|
||||
#: apps/web/src/components/forms/signin.tsx:473
|
||||
#: apps/web/src/components/forms/signin.tsx:451
|
||||
msgid "Backup Code"
|
||||
msgstr ""
|
||||
|
||||
@ -633,6 +633,14 @@ msgstr ""
|
||||
msgid "Billing"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:207
|
||||
#~ msgid "Black"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:223
|
||||
#~ msgid "Blue"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx:101
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -752,8 +760,6 @@ msgstr ""
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:175
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:107
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:435
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:312
|
||||
msgid "Click to insert field"
|
||||
msgstr ""
|
||||
|
||||
@ -770,8 +776,6 @@ msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx:58
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:425
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:302
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:522
|
||||
msgid "Complete"
|
||||
msgstr ""
|
||||
@ -1202,10 +1206,6 @@ msgstr ""
|
||||
msgid "Document completed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/completed.tsx:16
|
||||
msgid "Document Completed!"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:142
|
||||
msgid "Document created"
|
||||
msgstr ""
|
||||
@ -1397,13 +1397,11 @@ msgstr ""
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:220
|
||||
#: apps/web/src/app/(recipient)/d/[token]/configure-direct-template.tsx:118
|
||||
#: apps/web/src/app/(signing)/sign/[token]/email-field.tsx:126
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:376
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:254
|
||||
#: apps/web/src/components/(teams)/dialogs/add-team-email-dialog.tsx:169
|
||||
#: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:153
|
||||
#: apps/web/src/components/forms/forgot-password.tsx:81
|
||||
#: apps/web/src/components/forms/profile.tsx:122
|
||||
#: apps/web/src/components/forms/signin.tsx:326
|
||||
#: apps/web/src/components/forms/signin.tsx:304
|
||||
#: apps/web/src/components/forms/signup.tsx:180
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
@ -1567,14 +1565,12 @@ msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/forgot-password/page.tsx:21
|
||||
#: apps/web/src/components/forms/signin.tsx:358
|
||||
#: apps/web/src/components/forms/signin.tsx:336
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:326
|
||||
#: apps/web/src/app/(signing)/sign/[token]/name-field.tsx:193
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:361
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:239
|
||||
#: apps/web/src/components/forms/profile.tsx:110
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:300
|
||||
msgid "Full Name"
|
||||
@ -1616,6 +1612,10 @@ msgstr ""
|
||||
msgid "Go to your <0>public profile settings</0> to add documents."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:231
|
||||
#~ msgid "Green"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/profile/page.tsx:29
|
||||
msgid "Here you can edit your personal details."
|
||||
msgstr ""
|
||||
@ -2012,8 +2012,6 @@ msgstr ""
|
||||
msgid "New Template"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:416
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:293
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:509
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
@ -2067,7 +2065,7 @@ msgstr ""
|
||||
msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:160
|
||||
#: apps/web/src/components/forms/signin.tsx:142
|
||||
msgid "Not supported"
|
||||
msgstr ""
|
||||
|
||||
@ -2125,7 +2123,7 @@ msgstr ""
|
||||
msgid "Or"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:378
|
||||
#: apps/web/src/components/forms/signin.tsx:356
|
||||
msgid "Or continue with"
|
||||
msgstr ""
|
||||
|
||||
@ -2142,7 +2140,7 @@ msgstr ""
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:423
|
||||
#: apps/web/src/components/forms/signin.tsx:401
|
||||
msgid "Passkey"
|
||||
msgstr ""
|
||||
|
||||
@ -2175,14 +2173,14 @@ msgstr ""
|
||||
msgid "Passkeys allow you to sign in and authenticate using biometrics, password managers, etc."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:161
|
||||
#: apps/web/src/components/forms/signin.tsx:143
|
||||
msgid "Passkeys are not supported on this browser"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:65
|
||||
#: apps/web/src/components/forms/password.tsx:123
|
||||
#: apps/web/src/components/forms/reset-password.tsx:110
|
||||
#: apps/web/src/components/forms/signin.tsx:344
|
||||
#: apps/web/src/components/forms/signin.tsx:322
|
||||
#: apps/web/src/components/forms/signup.tsx:196
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:332
|
||||
msgid "Password"
|
||||
@ -2305,7 +2303,7 @@ msgstr ""
|
||||
msgid "Please try again and make sure you enter the correct email address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:203
|
||||
#: apps/web/src/components/forms/signin.tsx:185
|
||||
msgid "Please try again later or login using your normal details"
|
||||
msgstr ""
|
||||
|
||||
@ -2426,6 +2424,10 @@ msgstr ""
|
||||
msgid "Recovery codes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:215
|
||||
#~ msgid "Red"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx:78
|
||||
#: apps/web/src/components/forms/signup.tsx:93
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:127
|
||||
@ -2715,11 +2717,6 @@ msgstr ""
|
||||
msgid "Sign as<0>{0} <1>({1})</1></0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:329
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:207
|
||||
msgid "Sign document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/document-action-auth-dialog.tsx:59
|
||||
msgid "Sign field"
|
||||
msgstr ""
|
||||
@ -2730,8 +2727,8 @@ msgid "Sign Here"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/not-found.tsx:29
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
msgid "Sign In"
|
||||
msgstr ""
|
||||
|
||||
@ -2744,11 +2741,6 @@ msgstr ""
|
||||
msgid "Sign Out"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:350
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:228
|
||||
msgid "Sign the document to complete the process."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx:72
|
||||
msgid "Sign up"
|
||||
msgstr ""
|
||||
@ -2771,8 +2763,6 @@ msgstr ""
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:338
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signature-field.tsx:197
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signature-field.tsx:227
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:391
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:268
|
||||
#: apps/web/src/components/forms/profile.tsx:132
|
||||
msgid "Signature"
|
||||
msgstr ""
|
||||
@ -2789,8 +2779,8 @@ msgstr ""
|
||||
msgid "Signed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
msgid "Signing in..."
|
||||
msgstr ""
|
||||
|
||||
@ -2839,8 +2829,6 @@ msgstr ""
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/layout-billing-banner.tsx:53
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/team-email-dropdown.tsx:39
|
||||
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx:61
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:243
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:123
|
||||
#: apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx:50
|
||||
#: apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx:99
|
||||
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:210
|
||||
@ -3134,10 +3122,6 @@ msgstr ""
|
||||
msgid "The document has been successfully moved to the selected team."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/completed.tsx:29
|
||||
msgid "The document is now completed, please follow any instructions provided within the parent application."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:159
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr ""
|
||||
@ -3313,7 +3297,7 @@ msgstr ""
|
||||
msgid "This passkey has already been registered."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:200
|
||||
#: apps/web/src/components/forms/signin.tsx:182
|
||||
msgid "This passkey is not configured for this application. Please login and add one in the user settings."
|
||||
msgstr ""
|
||||
|
||||
@ -3321,7 +3305,7 @@ msgstr ""
|
||||
msgid "This price includes minimum 5 seats."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:202
|
||||
#: apps/web/src/components/forms/signin.tsx:184
|
||||
msgid "This session has expired. Please try again."
|
||||
msgstr ""
|
||||
|
||||
@ -3400,10 +3384,6 @@ msgstr ""
|
||||
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/embed/authenticate.tsx:21
|
||||
msgid "To view this document you need to be signed into your account, please sign in to continue."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx:178
|
||||
msgid "Toggle the switch to hide your profile from the public."
|
||||
msgstr ""
|
||||
@ -3485,7 +3465,7 @@ msgstr ""
|
||||
msgid "Two factor authentication recovery codes are used to access your account in the event that you lose access to your authenticator app."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:436
|
||||
#: apps/web/src/components/forms/signin.tsx:414
|
||||
msgid "Two-Factor Authentication"
|
||||
msgstr ""
|
||||
|
||||
@ -3584,8 +3564,8 @@ msgstr ""
|
||||
msgid "Unable to setup two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:247
|
||||
#: apps/web/src/components/forms/signin.tsx:255
|
||||
#: apps/web/src/components/forms/signin.tsx:229
|
||||
#: apps/web/src/components/forms/signin.tsx:237
|
||||
msgid "Unable to sign in"
|
||||
msgstr ""
|
||||
|
||||
@ -3691,12 +3671,12 @@ msgid "Uploaded file not an allowed file type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:187
|
||||
#: apps/web/src/components/forms/signin.tsx:493
|
||||
#: apps/web/src/components/forms/signin.tsx:471
|
||||
msgid "Use Authenticator"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:185
|
||||
#: apps/web/src/components/forms/signin.tsx:491
|
||||
#: apps/web/src/components/forms/signin.tsx:469
|
||||
msgid "Use Backup Code"
|
||||
msgstr ""
|
||||
|
||||
@ -3915,9 +3895,9 @@ msgid "We encountered an unknown error while attempting to save your details. Pl
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/profile.tsx:89
|
||||
#: apps/web/src/components/forms/signin.tsx:272
|
||||
#: apps/web/src/components/forms/signin.tsx:287
|
||||
#: apps/web/src/components/forms/signin.tsx:303
|
||||
#: apps/web/src/components/forms/signin.tsx:254
|
||||
#: apps/web/src/components/forms/signin.tsx:267
|
||||
#: apps/web/src/components/forms/signin.tsx:281
|
||||
msgid "We encountered an unknown error while attempting to sign you In. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
@ -4001,8 +3981,6 @@ msgid "We were unable to setup two-factor authentication for your account. Pleas
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/direct-template.tsx:119
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:245
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:125
|
||||
msgid "We were unable to submit this document at this time. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ msgid "Advanced Options"
|
||||
msgstr "Advanced Options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:369
|
||||
msgid "Advanced settings"
|
||||
msgstr "Advanced settings"
|
||||
|
||||
@ -135,6 +135,14 @@ msgstr "Approver"
|
||||
msgid "Approving"
|
||||
msgstr "Approving"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:271
|
||||
msgid "Black"
|
||||
msgstr "Black"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:298
|
||||
msgid "Blue"
|
||||
msgstr "Blue"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
|
||||
msgid "Cancel"
|
||||
@ -162,7 +170,7 @@ msgid "Character Limit"
|
||||
msgstr "Character Limit"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:756
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:755
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
|
||||
@ -174,7 +182,7 @@ msgstr "Checkbox values"
|
||||
msgid "Clear filters"
|
||||
msgstr "Clear filters"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:321
|
||||
msgid "Clear Signature"
|
||||
msgstr "Clear Signature"
|
||||
|
||||
@ -191,7 +199,7 @@ msgid "Configure Direct Recipient"
|
||||
msgstr "Configure Direct Recipient"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:371
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Configure the {0} field"
|
||||
|
||||
@ -208,7 +216,7 @@ msgid "Custom Text"
|
||||
msgstr "Custom Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:652
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:651
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
@ -240,7 +248,7 @@ msgid "Drag & drop your PDF here."
|
||||
msgstr "Drag & drop your PDF here."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:782
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:781
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
|
||||
@ -252,7 +260,7 @@ msgstr "Dropdown options"
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:232
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:239
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:600
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:599
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
|
||||
msgid "Email"
|
||||
@ -307,6 +315,10 @@ msgstr "Global recipient action authentication"
|
||||
msgid "Go Back"
|
||||
msgstr "Go Back"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:307
|
||||
msgid "Green"
|
||||
msgstr "Green"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:72
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "I am a signer of this document"
|
||||
@ -362,7 +374,7 @@ msgstr "Min"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:626
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:625
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
|
||||
msgid "Name"
|
||||
@ -381,12 +393,12 @@ msgid "Needs to view"
|
||||
msgstr "Needs to view"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:465
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:464
|
||||
msgid "No recipient matching this description was found."
|
||||
msgstr "No recipient matching this description was found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:629
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:481
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:480
|
||||
msgid "No recipients with this role"
|
||||
msgstr "No recipients with this role"
|
||||
|
||||
@ -411,7 +423,7 @@ msgid "No value found."
|
||||
msgstr "No value found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:704
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:703
|
||||
msgid "Number"
|
||||
msgstr "Number"
|
||||
|
||||
@ -446,7 +458,7 @@ msgid "Placeholder"
|
||||
msgstr "Placeholder"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:730
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:729
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
|
||||
@ -472,6 +484,10 @@ msgstr "Receives copy"
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Recipient action authentication"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:289
|
||||
msgid "Red"
|
||||
msgstr "Red"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
@ -497,7 +513,7 @@ msgstr "Rows per page"
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:798
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:797
|
||||
msgid "Save Template"
|
||||
msgstr "Save Template"
|
||||
|
||||
@ -547,7 +563,7 @@ msgstr "Sign"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:548
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:547
|
||||
msgid "Signature"
|
||||
msgstr "Signature"
|
||||
|
||||
@ -593,7 +609,7 @@ msgid "Template title"
|
||||
msgstr "Template title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:678
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:677
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@ -728,6 +744,10 @@ msgstr "Viewer"
|
||||
msgid "Viewing"
|
||||
msgstr "Viewing"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
|
||||
msgid "White"
|
||||
msgstr "White"
|
||||
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
|
||||
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
|
||||
msgstr "You are about to send this document to the recipients. Are you sure you want to continue?"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -21,15 +21,15 @@ msgstr "\"{0}\" will appear on the document as it has a timezone of \"{timezone}
|
||||
msgid "\"{documentTitle}\" has been successfully deleted"
|
||||
msgstr "\"{documentTitle}\" has been successfully deleted"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:76
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:75
|
||||
msgid "({0}) has invited you to approve this document"
|
||||
msgstr "({0}) has invited you to approve this document"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:73
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:72
|
||||
msgid "({0}) has invited you to sign this document"
|
||||
msgstr "({0}) has invited you to sign this document"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:70
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:69
|
||||
msgid "({0}) has invited you to view this document"
|
||||
msgstr "({0}) has invited you to view this document"
|
||||
|
||||
@ -495,11 +495,11 @@ msgstr "An error occurred while uploading your document."
|
||||
#: apps/web/src/components/forms/public-profile-claim-dialog.tsx:113
|
||||
#: apps/web/src/components/forms/public-profile-form.tsx:104
|
||||
#: apps/web/src/components/forms/reset-password.tsx:87
|
||||
#: apps/web/src/components/forms/signin.tsx:248
|
||||
#: apps/web/src/components/forms/signin.tsx:256
|
||||
#: apps/web/src/components/forms/signin.tsx:270
|
||||
#: apps/web/src/components/forms/signin.tsx:285
|
||||
#: apps/web/src/components/forms/signin.tsx:301
|
||||
#: apps/web/src/components/forms/signin.tsx:230
|
||||
#: apps/web/src/components/forms/signin.tsx:238
|
||||
#: apps/web/src/components/forms/signin.tsx:252
|
||||
#: apps/web/src/components/forms/signin.tsx:265
|
||||
#: apps/web/src/components/forms/signin.tsx:279
|
||||
#: apps/web/src/components/forms/signup.tsx:113
|
||||
#: apps/web/src/components/forms/signup.tsx:128
|
||||
#: apps/web/src/components/forms/signup.tsx:142
|
||||
@ -603,7 +603,7 @@ msgid "Background Color"
|
||||
msgstr "Background Color"
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:167
|
||||
#: apps/web/src/components/forms/signin.tsx:473
|
||||
#: apps/web/src/components/forms/signin.tsx:451
|
||||
msgid "Backup Code"
|
||||
msgstr "Backup Code"
|
||||
|
||||
@ -628,6 +628,14 @@ msgstr "Basic details"
|
||||
msgid "Billing"
|
||||
msgstr "Billing"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:207
|
||||
#~ msgid "Black"
|
||||
#~ msgstr "Black"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:223
|
||||
#~ msgid "Blue"
|
||||
#~ msgstr "Blue"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx:101
|
||||
msgid "Browser"
|
||||
msgstr "Browser"
|
||||
@ -747,8 +755,6 @@ msgstr "Click to copy signing link for sending to recipient"
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:175
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:107
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:435
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:312
|
||||
msgid "Click to insert field"
|
||||
msgstr "Click to insert field"
|
||||
|
||||
@ -765,8 +771,6 @@ msgid "Close"
|
||||
msgstr "Close"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx:58
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:425
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:302
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:522
|
||||
msgid "Complete"
|
||||
msgstr "Complete"
|
||||
@ -1197,10 +1201,6 @@ msgstr "Document Cancelled"
|
||||
msgid "Document completed"
|
||||
msgstr "Document completed"
|
||||
|
||||
#: apps/web/src/app/embed/completed.tsx:16
|
||||
msgid "Document Completed!"
|
||||
msgstr "Document Completed!"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:142
|
||||
msgid "Document created"
|
||||
msgstr "Document created"
|
||||
@ -1392,13 +1392,11 @@ msgstr "Edit webhook"
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:220
|
||||
#: apps/web/src/app/(recipient)/d/[token]/configure-direct-template.tsx:118
|
||||
#: apps/web/src/app/(signing)/sign/[token]/email-field.tsx:126
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:376
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:254
|
||||
#: apps/web/src/components/(teams)/dialogs/add-team-email-dialog.tsx:169
|
||||
#: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:153
|
||||
#: apps/web/src/components/forms/forgot-password.tsx:81
|
||||
#: apps/web/src/components/forms/profile.tsx:122
|
||||
#: apps/web/src/components/forms/signin.tsx:326
|
||||
#: apps/web/src/components/forms/signin.tsx:304
|
||||
#: apps/web/src/components/forms/signup.tsx:180
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
@ -1562,14 +1560,12 @@ msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB"
|
||||
msgstr "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/forgot-password/page.tsx:21
|
||||
#: apps/web/src/components/forms/signin.tsx:358
|
||||
#: apps/web/src/components/forms/signin.tsx:336
|
||||
msgid "Forgot your password?"
|
||||
msgstr "Forgot your password?"
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:326
|
||||
#: apps/web/src/app/(signing)/sign/[token]/name-field.tsx:193
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:361
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:239
|
||||
#: apps/web/src/components/forms/profile.tsx:110
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:300
|
||||
msgid "Full Name"
|
||||
@ -1611,6 +1607,10 @@ msgstr "Go to owner"
|
||||
msgid "Go to your <0>public profile settings</0> to add documents."
|
||||
msgstr "Go to your <0>public profile settings</0> to add documents."
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:231
|
||||
#~ msgid "Green"
|
||||
#~ msgstr "Green"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/profile/page.tsx:29
|
||||
msgid "Here you can edit your personal details."
|
||||
msgstr "Here you can edit your personal details."
|
||||
@ -2007,8 +2007,6 @@ msgstr "New team owner"
|
||||
msgid "New Template"
|
||||
msgstr "New Template"
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:416
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:293
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:509
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
@ -2062,7 +2060,7 @@ msgstr "No value found."
|
||||
msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
msgstr "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:160
|
||||
#: apps/web/src/components/forms/signin.tsx:142
|
||||
msgid "Not supported"
|
||||
msgstr "Not supported"
|
||||
|
||||
@ -2120,7 +2118,7 @@ msgstr "Opened"
|
||||
msgid "Or"
|
||||
msgstr "Or"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:378
|
||||
#: apps/web/src/components/forms/signin.tsx:356
|
||||
msgid "Or continue with"
|
||||
msgstr "Or continue with"
|
||||
|
||||
@ -2137,7 +2135,7 @@ msgstr "Owner"
|
||||
msgid "Paid"
|
||||
msgstr "Paid"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:423
|
||||
#: apps/web/src/components/forms/signin.tsx:401
|
||||
msgid "Passkey"
|
||||
msgstr "Passkey"
|
||||
|
||||
@ -2170,14 +2168,14 @@ msgstr "Passkeys"
|
||||
msgid "Passkeys allow you to sign in and authenticate using biometrics, password managers, etc."
|
||||
msgstr "Passkeys allow you to sign in and authenticate using biometrics, password managers, etc."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:161
|
||||
#: apps/web/src/components/forms/signin.tsx:143
|
||||
msgid "Passkeys are not supported on this browser"
|
||||
msgstr "Passkeys are not supported on this browser"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:65
|
||||
#: apps/web/src/components/forms/password.tsx:123
|
||||
#: apps/web/src/components/forms/reset-password.tsx:110
|
||||
#: apps/web/src/components/forms/signin.tsx:344
|
||||
#: apps/web/src/components/forms/signin.tsx:322
|
||||
#: apps/web/src/components/forms/signup.tsx:196
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:332
|
||||
msgid "Password"
|
||||
@ -2300,7 +2298,7 @@ msgstr "Please provide a token from your authenticator, or a backup code."
|
||||
msgid "Please try again and make sure you enter the correct email address."
|
||||
msgstr "Please try again and make sure you enter the correct email address."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:203
|
||||
#: apps/web/src/components/forms/signin.tsx:185
|
||||
msgid "Please try again later or login using your normal details"
|
||||
msgstr "Please try again later or login using your normal details"
|
||||
|
||||
@ -2421,6 +2419,10 @@ msgstr "Recovery code copied"
|
||||
msgid "Recovery codes"
|
||||
msgstr "Recovery codes"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:215
|
||||
#~ msgid "Red"
|
||||
#~ msgstr "Red"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx:78
|
||||
#: apps/web/src/components/forms/signup.tsx:93
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:127
|
||||
@ -2710,11 +2712,6 @@ msgstr "Sign as {0} <0>({1})</0>"
|
||||
msgid "Sign as<0>{0} <1>({1})</1></0>"
|
||||
msgstr "Sign as<0>{0} <1>({1})</1></0>"
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:329
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:207
|
||||
msgid "Sign document"
|
||||
msgstr "Sign document"
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/document-action-auth-dialog.tsx:59
|
||||
msgid "Sign field"
|
||||
msgstr "Sign field"
|
||||
@ -2725,8 +2722,8 @@ msgid "Sign Here"
|
||||
msgstr "Sign Here"
|
||||
|
||||
#: apps/web/src/app/not-found.tsx:29
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
msgid "Sign In"
|
||||
msgstr "Sign In"
|
||||
|
||||
@ -2739,11 +2736,6 @@ msgstr "Sign in to your account"
|
||||
msgid "Sign Out"
|
||||
msgstr "Sign Out"
|
||||
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:350
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:228
|
||||
msgid "Sign the document to complete the process."
|
||||
msgstr "Sign the document to complete the process."
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx:72
|
||||
msgid "Sign up"
|
||||
msgstr "Sign up"
|
||||
@ -2766,8 +2758,6 @@ msgstr "Sign Up with OIDC"
|
||||
#: apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx:338
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signature-field.tsx:197
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signature-field.tsx:227
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:391
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:268
|
||||
#: apps/web/src/components/forms/profile.tsx:132
|
||||
msgid "Signature"
|
||||
msgstr "Signature"
|
||||
@ -2784,8 +2774,8 @@ msgstr "Signatures will appear once the document has been completed"
|
||||
msgid "Signed"
|
||||
msgstr "Signed"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
msgid "Signing in..."
|
||||
msgstr "Signing in..."
|
||||
|
||||
@ -2834,8 +2824,6 @@ msgstr "Site Settings"
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/layout-billing-banner.tsx:53
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/team-email-dropdown.tsx:39
|
||||
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx:61
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:243
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:123
|
||||
#: apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx:50
|
||||
#: apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx:99
|
||||
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:210
|
||||
@ -3129,10 +3117,6 @@ msgstr "The direct link has been copied to your clipboard"
|
||||
msgid "The document has been successfully moved to the selected team."
|
||||
msgstr "The document has been successfully moved to the selected team."
|
||||
|
||||
#: apps/web/src/app/embed/completed.tsx:29
|
||||
msgid "The document is now completed, please follow any instructions provided within the parent application."
|
||||
msgstr "The document is now completed, please follow any instructions provided within the parent application."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:159
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "The document was created but could not be sent to recipients."
|
||||
@ -3308,7 +3292,7 @@ msgstr "This link is invalid or has expired. Please contact your team to resend
|
||||
msgid "This passkey has already been registered."
|
||||
msgstr "This passkey has already been registered."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:200
|
||||
#: apps/web/src/components/forms/signin.tsx:182
|
||||
msgid "This passkey is not configured for this application. Please login and add one in the user settings."
|
||||
msgstr "This passkey is not configured for this application. Please login and add one in the user settings."
|
||||
|
||||
@ -3316,7 +3300,7 @@ msgstr "This passkey is not configured for this application. Please login and ad
|
||||
msgid "This price includes minimum 5 seats."
|
||||
msgstr "This price includes minimum 5 seats."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:202
|
||||
#: apps/web/src/components/forms/signin.tsx:184
|
||||
msgid "This session has expired. Please try again."
|
||||
msgstr "This session has expired. Please try again."
|
||||
|
||||
@ -3395,10 +3379,6 @@ msgstr "To gain access to your account, please confirm your email address by cli
|
||||
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/web/src/app/embed/authenticate.tsx:21
|
||||
msgid "To view this document you need to be signed into your account, please sign in to continue."
|
||||
msgstr "To view this document you need to be signed into your account, please sign in to continue."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx:178
|
||||
msgid "Toggle the switch to hide your profile from the public."
|
||||
msgstr "Toggle the switch to hide your profile from the public."
|
||||
@ -3480,7 +3460,7 @@ msgstr "Two factor authentication"
|
||||
msgid "Two factor authentication recovery codes are used to access your account in the event that you lose access to your authenticator app."
|
||||
msgstr "Two factor authentication recovery codes are used to access your account in the event that you lose access to your authenticator app."
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:436
|
||||
#: apps/web/src/components/forms/signin.tsx:414
|
||||
msgid "Two-Factor Authentication"
|
||||
msgstr "Two-Factor Authentication"
|
||||
|
||||
@ -3579,8 +3559,8 @@ msgstr "Unable to reset password"
|
||||
msgid "Unable to setup two-factor authentication"
|
||||
msgstr "Unable to setup two-factor authentication"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:247
|
||||
#: apps/web/src/components/forms/signin.tsx:255
|
||||
#: apps/web/src/components/forms/signin.tsx:229
|
||||
#: apps/web/src/components/forms/signin.tsx:237
|
||||
msgid "Unable to sign in"
|
||||
msgstr "Unable to sign in"
|
||||
|
||||
@ -3686,12 +3666,12 @@ msgid "Uploaded file not an allowed file type"
|
||||
msgstr "Uploaded file not an allowed file type"
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:187
|
||||
#: apps/web/src/components/forms/signin.tsx:493
|
||||
#: apps/web/src/components/forms/signin.tsx:471
|
||||
msgid "Use Authenticator"
|
||||
msgstr "Use Authenticator"
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:185
|
||||
#: apps/web/src/components/forms/signin.tsx:491
|
||||
#: apps/web/src/components/forms/signin.tsx:469
|
||||
msgid "Use Backup Code"
|
||||
msgstr "Use Backup Code"
|
||||
|
||||
@ -3910,9 +3890,9 @@ msgid "We encountered an unknown error while attempting to save your details. Pl
|
||||
msgstr "We encountered an unknown error while attempting to save your details. Please try again later."
|
||||
|
||||
#: apps/web/src/components/forms/profile.tsx:89
|
||||
#: apps/web/src/components/forms/signin.tsx:272
|
||||
#: apps/web/src/components/forms/signin.tsx:287
|
||||
#: apps/web/src/components/forms/signin.tsx:303
|
||||
#: apps/web/src/components/forms/signin.tsx:254
|
||||
#: apps/web/src/components/forms/signin.tsx:267
|
||||
#: apps/web/src/components/forms/signin.tsx:281
|
||||
msgid "We encountered an unknown error while attempting to sign you In. Please try again later."
|
||||
msgstr "We encountered an unknown error while attempting to sign you In. Please try again later."
|
||||
|
||||
@ -3996,8 +3976,6 @@ msgid "We were unable to setup two-factor authentication for your account. Pleas
|
||||
msgstr "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again."
|
||||
|
||||
#: apps/web/src/app/(recipient)/d/[token]/direct-template.tsx:119
|
||||
#: apps/web/src/app/embed/direct/[[...url]]/client.tsx:245
|
||||
#: apps/web/src/app/embed/sign/[[...url]]/client.tsx:125
|
||||
msgid "We were unable to submit this document at this time. Please try again later."
|
||||
msgstr "We were unable to submit this document at this time. Please try again later."
|
||||
|
||||
|
||||
@ -142,9 +142,8 @@ export const DocumentShareButton = ({
|
||||
<DialogTitle>Share your signing experience!</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
Rest assured, your document is strictly confidential and will never be shared. Only your
|
||||
signing experience will be highlighted. Share your personalized signing card to showcase
|
||||
your signature!
|
||||
Don't worry, the document you signed or sent wont be shared; only your signing
|
||||
experience is. Share your signing card and showcase your signature!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@ -241,7 +241,6 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
|
||||
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
}}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@ -20,16 +20,15 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/tsconfig": "*",
|
||||
"@types/luxon": "^3.3.2",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"react": "^18",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"react": "18.2.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@hookform/resolvers": "^3.3.0",
|
||||
"@lingui/macro": "^4.11.3",
|
||||
"@lingui/react": "^4.11.3",
|
||||
"@lingui/react": "^4.11.1",
|
||||
"@radix-ui/react-accordion": "^1.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.2",
|
||||
@ -64,12 +63,12 @@
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.2",
|
||||
"next": "14.2.6",
|
||||
"next": "14.0.3",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "^18",
|
||||
"react": "18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-pdf": "7.7.3",
|
||||
"react-rnd": "^10.4.1",
|
||||
@ -78,4 +77,4 @@
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,13 @@ import type { StrokeOptions } from 'perfect-freehand';
|
||||
import { getStroke } from 'perfect-freehand';
|
||||
|
||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getSvgPathFromStroke } from './helper';
|
||||
@ -36,6 +43,7 @@ export const SignaturePad = ({
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
const [lines, setLines] = useState<Point[][]>([]);
|
||||
const [currentLine, setCurrentLine] = useState<Point[]>([]);
|
||||
const [selectedColor, setSelectedColor] = useState('black');
|
||||
|
||||
const perfectFreehandOptions = useMemo(() => {
|
||||
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
|
||||
@ -85,6 +93,7 @@ export const SignaturePad = ({
|
||||
ctx.restore();
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
lines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -129,6 +138,7 @@ export const SignaturePad = ({
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
newLines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -237,7 +247,7 @@ export const SignaturePad = ({
|
||||
>
|
||||
<canvas
|
||||
ref={$el}
|
||||
className={cn('relative block dark:invert', className)}
|
||||
className={cn('relative block', className)}
|
||||
style={{ touchAction: 'none' }}
|
||||
onPointerMove={(event) => onMouseMove(event)}
|
||||
onPointerDown={(event) => onMouseDown(event)}
|
||||
@ -247,6 +257,61 @@ export const SignaturePad = ({
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="absolute right-2 top-2">
|
||||
<Select onValueChange={(value) => setSelectedColor(value)}>
|
||||
<SelectTrigger className="bg-background w-[90px]">
|
||||
<SelectValue placeholder="Color" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="black">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm"></div>
|
||||
<Trans>Black</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="white">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<div className="mr-2 h-5 w-5 rounded-full border-2 shadow-sm"></div>
|
||||
<Trans>White</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="red">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-red-500 shadow-sm"></div>
|
||||
<Trans>Red</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="blue">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-blue-500 shadow-sm"></div>
|
||||
<Trans>Blue</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="green">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-green-500 shadow-sm"></div>
|
||||
<Trans>Green</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -112,7 +112,6 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
signerToken:
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user