mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
feat: embed signing experience (#1322)
This commit is contained in:
@ -12,5 +12,6 @@
|
||||
"title": "API & Integration Guides"
|
||||
},
|
||||
"public-api": "Public API",
|
||||
"embedding": "Embedding",
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
}
|
||||
131
apps/documentation/pages/developers/embedding/index.mdx
Normal file
131
apps/documentation/pages/developers/embedding/index.mdx
Normal file
@ -0,0 +1,131 @@
|
||||
---
|
||||
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.
|
||||
77
apps/documentation/pages/developers/embedding/preact.mdx
Normal file
77
apps/documentation/pages/developers/embedding/preact.mdx
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
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 |
|
||||
77
apps/documentation/pages/developers/embedding/react.mdx
Normal file
77
apps/documentation/pages/developers/embedding/react.mdx
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
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 |
|
||||
77
apps/documentation/pages/developers/embedding/solid.mdx
Normal file
77
apps/documentation/pages/developers/embedding/solid.mdx
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
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 |
|
||||
79
apps/documentation/pages/developers/embedding/svelte.mdx
Normal file
79
apps/documentation/pages/developers/embedding/svelte.mdx
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
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 |
|
||||
79
apps/documentation/pages/developers/embedding/vue.mdx
Normal file
79
apps/documentation/pages/developers/embedding/vue.mdx
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
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 |
|
||||
BIN
apps/documentation/public/embedding/copy-recipient-token.png
Normal file
BIN
apps/documentation/public/embedding/copy-recipient-token.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
BIN
apps/documentation/public/embedding/enable-direct-link.png
Normal file
BIN
apps/documentation/public/embedding/enable-direct-link.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
apps/documentation/public/embedding/team-templates.png
Normal file
BIN
apps/documentation/public/embedding/team-templates.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@ -45,6 +45,7 @@
|
||||
"posthog-js": "^1.75.3",
|
||||
"posthog-node": "^3.1.1",
|
||||
"react": "^18",
|
||||
"react-call": "^1.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
|
||||
@ -94,7 +94,7 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
const token = await createDocumentFromDirectTemplate({
|
||||
const { token } = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
|
||||
@ -63,6 +63,7 @@ export const SigningPageView = ({
|
||||
{document.User.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
|
||||
32
apps/web/src/app/embed/authenticate.tsx
Normal file
32
apps/web/src/app/embed/authenticate.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
5
apps/web/src/app/embed/base-schema.ts
Normal file
5
apps/web/src/app/embed/base-schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZBaseEmbedDataSchema = z.object({
|
||||
css: z.string().optional().transform(value => value || undefined),
|
||||
});
|
||||
7
apps/web/src/app/embed/client-loading.tsx
Normal file
7
apps/web/src/app/embed/client-loading.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
33
apps/web/src/app/embed/completed.tsx
Normal file
33
apps/web/src/app/embed/completed.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
456
apps/web/src/app/embed/direct/[[...url]]/client.tsx
Normal file
456
apps/web/src/app/embed/direct/[[...url]]/client.tsx
Normal file
@ -0,0 +1,456 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
3
apps/web/src/app/embed/direct/[[...url]]/not-found.tsx
Normal file
3
apps/web/src/app/embed/direct/[[...url]]/not-found.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function EmbedDirectTemplateNotFound() {
|
||||
return <div>Not Found</div>
|
||||
}
|
||||
97
apps/web/src/app/embed/direct/[[...url]]/page.tsx
Normal file
97
apps/web/src/app/embed/direct/[[...url]]/page.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
20
apps/web/src/app/embed/direct/[[...url]]/schema.ts
Normal file
20
apps/web/src/app/embed/direct/[[...url]]/schema.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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>;
|
||||
185
apps/web/src/app/embed/document-fields.tsx
Normal file
185
apps/web/src/app/embed/document-fields.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
5
apps/web/src/app/embed/paywall.tsx
Normal file
5
apps/web/src/app/embed/paywall.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export const EmbedPaywall = () => {
|
||||
return <div>
|
||||
<h1>Paywall</h1>
|
||||
</div>
|
||||
}
|
||||
327
apps/web/src/app/embed/sign/[[...url]]/client.tsx
Normal file
327
apps/web/src/app/embed/sign/[[...url]]/client.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
3
apps/web/src/app/embed/sign/[[...url]]/not-found.tsx
Normal file
3
apps/web/src/app/embed/sign/[[...url]]/not-found.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function EmbedDirectTemplateNotFound() {
|
||||
return <div>Not Found</div>
|
||||
}
|
||||
95
apps/web/src/app/embed/sign/[[...url]]/page.tsx
Normal file
95
apps/web/src/app/embed/sign/[[...url]]/page.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
16
apps/web/src/app/embed/sign/[[...url]]/schema.ts
Normal file
16
apps/web/src/app/embed/sign/[[...url]]/schema.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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 { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@ -74,6 +74,7 @@ export type SignInFormProps = {
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
oidcProviderLabel?: string;
|
||||
returnTo?: string;
|
||||
};
|
||||
|
||||
export const SignInForm = ({
|
||||
@ -82,6 +83,7 @@ export const SignInForm = ({
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
oidcProviderLabel,
|
||||
returnTo,
|
||||
}: SignInFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -100,6 +102,22 @@ 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();
|
||||
|
||||
@ -157,7 +175,7 @@ export const SignInForm = ({
|
||||
|
||||
const result = await signIn('webauthn', {
|
||||
credential: JSON.stringify(credential),
|
||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||
callbackUrl,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
@ -210,7 +228,7 @@ export const SignInForm = ({
|
||||
|
||||
const result = await signIn('credentials', {
|
||||
...credentials,
|
||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||
callbackUrl,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
@ -259,7 +277,9 @@ export const SignInForm = ({
|
||||
|
||||
const onSignInWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH });
|
||||
await signIn('google', {
|
||||
callbackUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -273,7 +293,9 @@ export const SignInForm = ({
|
||||
|
||||
const onSignInWithOIDCClick = async () => {
|
||||
try {
|
||||
await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
|
||||
await signIn('oidc', {
|
||||
callbackUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
|
||||
@ -76,6 +76,20 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -457,6 +457,7 @@
|
||||
"posthog-js": "^1.75.3",
|
||||
"posthog-node": "^3.1.1",
|
||||
"react": "^18",
|
||||
"react-call": "^1.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
@ -27817,6 +27818,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-call": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-call/-/react-call-1.3.0.tgz",
|
||||
"integrity": "sha512-qlcxdNR6LHif2YoIYf/tlzrotuGzwRurF5oRrwL+rxX8bVfoID1/NmrDzEGiXUxmR+WidLGWTKKhe30guRQg0w==",
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
|
||||
60
packages/lib/client-only/hooks/use-throttle-fn.ts
Normal file
60
packages/lib/client-only/hooks/use-throttle-fn.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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];
|
||||
}
|
||||
@ -147,7 +147,7 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = result.Recipient[0];
|
||||
const [recipient] = result.Recipient;
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
'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, directRecipientToken } = await prisma.$transaction(async (tx) => {
|
||||
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
const documentData = await tx.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
@ -539,8 +539,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
token: createdDirectRecipient.token,
|
||||
documentId: document.id,
|
||||
directRecipientToken: createdDirectRecipient.token,
|
||||
recipientId: createdDirectRecipient.id,
|
||||
};
|
||||
});
|
||||
|
||||
@ -559,5 +560,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
// Log and reseal as required until we configure middleware.
|
||||
}
|
||||
|
||||
return directRecipientToken;
|
||||
return {
|
||||
token,
|
||||
documentId,
|
||||
recipientId,
|
||||
};
|
||||
};
|
||||
|
||||
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:75
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:76
|
||||
msgid "({0}) has invited you to approve this document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:72
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:73
|
||||
msgid "({0}) has invited you to sign this document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:69
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:70
|
||||
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: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/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/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:451
|
||||
#: apps/web/src/components/forms/signin.tsx:473
|
||||
msgid "Backup Code"
|
||||
msgstr ""
|
||||
|
||||
@ -752,6 +752,8 @@ 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 ""
|
||||
|
||||
@ -768,6 +770,8 @@ 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 ""
|
||||
@ -1198,6 +1202,10 @@ 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 ""
|
||||
@ -1389,11 +1397,13 @@ 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:304
|
||||
#: apps/web/src/components/forms/signin.tsx:326
|
||||
#: apps/web/src/components/forms/signup.tsx:180
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
@ -1557,12 +1567,14 @@ 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:336
|
||||
#: apps/web/src/components/forms/signin.tsx:358
|
||||
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"
|
||||
@ -2000,6 +2012,8 @@ 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 ""
|
||||
@ -2053,7 +2067,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:142
|
||||
#: apps/web/src/components/forms/signin.tsx:160
|
||||
msgid "Not supported"
|
||||
msgstr ""
|
||||
|
||||
@ -2111,7 +2125,7 @@ msgstr ""
|
||||
msgid "Or"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:356
|
||||
#: apps/web/src/components/forms/signin.tsx:378
|
||||
msgid "Or continue with"
|
||||
msgstr ""
|
||||
|
||||
@ -2128,7 +2142,7 @@ msgstr ""
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:401
|
||||
#: apps/web/src/components/forms/signin.tsx:423
|
||||
msgid "Passkey"
|
||||
msgstr ""
|
||||
|
||||
@ -2161,14 +2175,14 @@ msgstr ""
|
||||
msgid "Passkeys allow you to sign in and authenticate using biometrics, password managers, etc."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:143
|
||||
#: apps/web/src/components/forms/signin.tsx:161
|
||||
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:322
|
||||
#: apps/web/src/components/forms/signin.tsx:344
|
||||
#: apps/web/src/components/forms/signup.tsx:196
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:332
|
||||
msgid "Password"
|
||||
@ -2291,7 +2305,7 @@ msgstr ""
|
||||
msgid "Please try again and make sure you enter the correct email address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:185
|
||||
#: apps/web/src/components/forms/signin.tsx:203
|
||||
msgid "Please try again later or login using your normal details"
|
||||
msgstr ""
|
||||
|
||||
@ -2701,6 +2715,11 @@ 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 ""
|
||||
@ -2711,8 +2730,8 @@ msgid "Sign Here"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/app/not-found.tsx:29
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
msgid "Sign In"
|
||||
msgstr ""
|
||||
|
||||
@ -2725,6 +2744,11 @@ 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 ""
|
||||
@ -2747,6 +2771,8 @@ 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 ""
|
||||
@ -2763,8 +2789,8 @@ msgstr ""
|
||||
msgid "Signed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
msgid "Signing in..."
|
||||
msgstr ""
|
||||
|
||||
@ -2813,6 +2839,8 @@ 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
|
||||
@ -3106,6 +3134,10 @@ 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 ""
|
||||
@ -3281,7 +3313,7 @@ msgstr ""
|
||||
msgid "This passkey has already been registered."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:182
|
||||
#: apps/web/src/components/forms/signin.tsx:200
|
||||
msgid "This passkey is not configured for this application. Please login and add one in the user settings."
|
||||
msgstr ""
|
||||
|
||||
@ -3289,7 +3321,7 @@ msgstr ""
|
||||
msgid "This price includes minimum 5 seats."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:184
|
||||
#: apps/web/src/components/forms/signin.tsx:202
|
||||
msgid "This session has expired. Please try again."
|
||||
msgstr ""
|
||||
|
||||
@ -3368,6 +3400,10 @@ 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 ""
|
||||
@ -3449,7 +3485,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:414
|
||||
#: apps/web/src/components/forms/signin.tsx:436
|
||||
msgid "Two-Factor Authentication"
|
||||
msgstr ""
|
||||
|
||||
@ -3548,8 +3584,8 @@ msgstr ""
|
||||
msgid "Unable to setup two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:229
|
||||
#: apps/web/src/components/forms/signin.tsx:237
|
||||
#: apps/web/src/components/forms/signin.tsx:247
|
||||
#: apps/web/src/components/forms/signin.tsx:255
|
||||
msgid "Unable to sign in"
|
||||
msgstr ""
|
||||
|
||||
@ -3655,12 +3691,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:471
|
||||
#: apps/web/src/components/forms/signin.tsx:493
|
||||
msgid "Use Authenticator"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:185
|
||||
#: apps/web/src/components/forms/signin.tsx:469
|
||||
#: apps/web/src/components/forms/signin.tsx:491
|
||||
msgid "Use Backup Code"
|
||||
msgstr ""
|
||||
|
||||
@ -3879,9 +3915,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:254
|
||||
#: apps/web/src/components/forms/signin.tsx:267
|
||||
#: apps/web/src/components/forms/signin.tsx:281
|
||||
#: apps/web/src/components/forms/signin.tsx:272
|
||||
#: apps/web/src/components/forms/signin.tsx:287
|
||||
#: apps/web/src/components/forms/signin.tsx:303
|
||||
msgid "We encountered an unknown error while attempting to sign you In. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
@ -3965,6 +4001,8 @@ 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 ""
|
||||
|
||||
|
||||
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:75
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:76
|
||||
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:72
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:73
|
||||
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:69
|
||||
#: apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx:70
|
||||
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: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/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/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:451
|
||||
#: apps/web/src/components/forms/signin.tsx:473
|
||||
msgid "Backup Code"
|
||||
msgstr "Backup Code"
|
||||
|
||||
@ -747,6 +747,8 @@ 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"
|
||||
|
||||
@ -763,6 +765,8 @@ 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"
|
||||
@ -1193,6 +1197,10 @@ 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"
|
||||
@ -1384,11 +1392,13 @@ 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:304
|
||||
#: apps/web/src/components/forms/signin.tsx:326
|
||||
#: apps/web/src/components/forms/signup.tsx:180
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
@ -1552,12 +1562,14 @@ 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:336
|
||||
#: apps/web/src/components/forms/signin.tsx:358
|
||||
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"
|
||||
@ -1995,6 +2007,8 @@ 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"
|
||||
@ -2048,7 +2062,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:142
|
||||
#: apps/web/src/components/forms/signin.tsx:160
|
||||
msgid "Not supported"
|
||||
msgstr "Not supported"
|
||||
|
||||
@ -2106,7 +2120,7 @@ msgstr "Opened"
|
||||
msgid "Or"
|
||||
msgstr "Or"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:356
|
||||
#: apps/web/src/components/forms/signin.tsx:378
|
||||
msgid "Or continue with"
|
||||
msgstr "Or continue with"
|
||||
|
||||
@ -2123,7 +2137,7 @@ msgstr "Owner"
|
||||
msgid "Paid"
|
||||
msgstr "Paid"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:401
|
||||
#: apps/web/src/components/forms/signin.tsx:423
|
||||
msgid "Passkey"
|
||||
msgstr "Passkey"
|
||||
|
||||
@ -2156,14 +2170,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:143
|
||||
#: apps/web/src/components/forms/signin.tsx:161
|
||||
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:322
|
||||
#: apps/web/src/components/forms/signin.tsx:344
|
||||
#: apps/web/src/components/forms/signup.tsx:196
|
||||
#: apps/web/src/components/forms/v2/signup.tsx:332
|
||||
msgid "Password"
|
||||
@ -2286,7 +2300,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:185
|
||||
#: apps/web/src/components/forms/signin.tsx:203
|
||||
msgid "Please try again later or login using your normal details"
|
||||
msgstr "Please try again later or login using your normal details"
|
||||
|
||||
@ -2696,6 +2710,11 @@ 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"
|
||||
@ -2706,8 +2725,8 @@ msgid "Sign Here"
|
||||
msgstr "Sign Here"
|
||||
|
||||
#: apps/web/src/app/not-found.tsx:29
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
msgid "Sign In"
|
||||
msgstr "Sign In"
|
||||
|
||||
@ -2720,6 +2739,11 @@ 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"
|
||||
@ -2742,6 +2766,8 @@ 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"
|
||||
@ -2758,8 +2784,8 @@ msgstr "Signatures will appear once the document has been completed"
|
||||
msgid "Signed"
|
||||
msgstr "Signed"
|
||||
|
||||
#: apps/web/src/components/forms/signin.tsx:349
|
||||
#: apps/web/src/components/forms/signin.tsx:476
|
||||
#: apps/web/src/components/forms/signin.tsx:371
|
||||
#: apps/web/src/components/forms/signin.tsx:498
|
||||
msgid "Signing in..."
|
||||
msgstr "Signing in..."
|
||||
|
||||
@ -2808,6 +2834,8 @@ 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
|
||||
@ -3101,6 +3129,10 @@ 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."
|
||||
@ -3276,7 +3308,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:182
|
||||
#: apps/web/src/components/forms/signin.tsx:200
|
||||
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."
|
||||
|
||||
@ -3284,7 +3316,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:184
|
||||
#: apps/web/src/components/forms/signin.tsx:202
|
||||
msgid "This session has expired. Please try again."
|
||||
msgstr "This session has expired. Please try again."
|
||||
|
||||
@ -3363,6 +3395,10 @@ 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."
|
||||
@ -3444,7 +3480,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:414
|
||||
#: apps/web/src/components/forms/signin.tsx:436
|
||||
msgid "Two-Factor Authentication"
|
||||
msgstr "Two-Factor Authentication"
|
||||
|
||||
@ -3543,8 +3579,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:229
|
||||
#: apps/web/src/components/forms/signin.tsx:237
|
||||
#: apps/web/src/components/forms/signin.tsx:247
|
||||
#: apps/web/src/components/forms/signin.tsx:255
|
||||
msgid "Unable to sign in"
|
||||
msgstr "Unable to sign in"
|
||||
|
||||
@ -3650,12 +3686,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:471
|
||||
#: apps/web/src/components/forms/signin.tsx:493
|
||||
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:469
|
||||
#: apps/web/src/components/forms/signin.tsx:491
|
||||
msgid "Use Backup Code"
|
||||
msgstr "Use Backup Code"
|
||||
|
||||
@ -3874,9 +3910,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:254
|
||||
#: apps/web/src/components/forms/signin.tsx:267
|
||||
#: apps/web/src/components/forms/signin.tsx:281
|
||||
#: apps/web/src/components/forms/signin.tsx:272
|
||||
#: apps/web/src/components/forms/signin.tsx:287
|
||||
#: apps/web/src/components/forms/signin.tsx:303
|
||||
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."
|
||||
|
||||
@ -3960,6 +3996,8 @@ 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."
|
||||
|
||||
|
||||
@ -241,6 +241,7 @@ 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user