Compare commits
15 Commits
v1.9.1-rc.
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce94cf0051 | ||
|
|
bccf4cd368 | ||
|
|
c13988bb8f | ||
|
|
9f1831afcb | ||
|
|
574a7449fa | ||
|
|
1aee1bb4cd | ||
|
|
634dc2afd0 | ||
|
|
21d68f3275 | ||
|
|
63c98949bb | ||
|
|
4348a949dd | ||
|
|
2a098f89fa | ||
|
|
bb805ea93b | ||
|
|
cc8b972fbc | ||
|
|
b55c419074 | ||
|
|
f9e3993519 |
@@ -1,4 +1,4 @@
|
|||||||
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="https://documen.so/sign-everywhere">The Platform Plan</a>!
|
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="documen.so/sign-everywhere">The Platform Plan</a>!
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso-platform-plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso-platform-plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,5 @@
|
|||||||
"solid": "Solid Integration",
|
"solid": "Solid Integration",
|
||||||
"preact": "Preact Integration",
|
"preact": "Preact Integration",
|
||||||
"angular": "Angular Integration",
|
"angular": "Angular Integration",
|
||||||
"css-variables": "CSS Variables",
|
"css-variables": "CSS Variables"
|
||||||
"web-components": "Web Components"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ We support embedding across a range of popular JavaScript frameworks, including:
|
|||||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||||
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
||||||
| Web Components | [@documenso/embed-webcomponent](https://www.npmjs.com/package/@documenso/embed-webcomponent) |
|
|
||||||
|
|
||||||
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
||||||
|
|
||||||
@@ -167,7 +166,6 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
|||||||
- [Svelte](/developers/embedding/svelte)
|
- [Svelte](/developers/embedding/svelte)
|
||||||
- [Solid](/developers/embedding/solid)
|
- [Solid](/developers/embedding/solid)
|
||||||
- [Angular](/developers/embedding/angular)
|
- [Angular](/developers/embedding/angular)
|
||||||
- [Web Components](/developers/embedding/web-components)
|
|
||||||
|
|
||||||
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
||||||
|
|
||||||
@@ -179,5 +177,4 @@ If you're using **web components**, the integration process is slightly differen
|
|||||||
- [Solid Integration](/developers/embedding/solid)
|
- [Solid Integration](/developers/embedding/solid)
|
||||||
- [Preact Integration](/developers/embedding/preact)
|
- [Preact Integration](/developers/embedding/preact)
|
||||||
- [Angular Integration](/developers/embedding/angular)
|
- [Angular Integration](/developers/embedding/angular)
|
||||||
- [Web Components](/developers/embedding/web-components)
|
|
||||||
- [CSS Variables](/developers/embedding/css-variables)
|
- [CSS Variables](/developers/embedding/css-variables)
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
---
|
|
||||||
title: Web Components Integration
|
|
||||||
description: Learn how to use our embedding SDK via Web Components on a framework-less web application.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Web Components Integration
|
|
||||||
|
|
||||||
Our Web Components SDK provides a simple way to embed a signing experience within your framework-less web application. It supports both direct link templates and signing tokens.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install the SDK, run the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @documenso/embed-webcomponent
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in your html file, add the following to add the script, replacing the path with the proper path to the web component script.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="YOUR_PATH_HERE"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 `documenso-embed-direct-template` tag.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<documenso-embed-direct-template
|
|
||||||
token="YOUR_TOKEN_HERE"
|
|
||||||
</documenso-embed-direct-template>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
|
||||||
| token | string | The token for the document you want to embed |
|
|
||||||
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
|
|
||||||
| name | string (optional) | The name the signer that will be used by default for signing |
|
|
||||||
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
|
|
||||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
|
||||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
|
||||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
|
||||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
|
||||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
|
||||||
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
|
|
||||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
|
|
||||||
|
|
||||||
### Signing Token
|
|
||||||
|
|
||||||
If you have a signing token, you can provide it to the `documenso-embed-sign-document` tag.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<documenso-embed-sign-document
|
|
||||||
token="YOUR_TOKEN_HERE"
|
|
||||||
</documenso-embed-sign-document>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
| Attribute | 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 |
|
|
||||||
|
|
||||||
### Creating via JavaScript
|
|
||||||
|
|
||||||
You can also create the tag element using javascript, for dynamic generation of either modes. For example, this would add the sign document embed to the DOM.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
document.getElementById('my-wrapper-here').innerHTML = '';
|
|
||||||
|
|
||||||
const tag = document.createElement('documenso-embed-sign-document');
|
|
||||||
tag.setAttribute('token', data.token);
|
|
||||||
tag.style.width = '100%';
|
|
||||||
tag.style.height = '100%';
|
|
||||||
|
|
||||||
document.getElementById('my-wrapper-here').appendChild(tag);
|
|
||||||
```
|
|
||||||
@@ -21,25 +21,14 @@ Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) f
|
|||||||
|
|
||||||
## API V2 - Beta
|
## API V2 - Beta
|
||||||
|
|
||||||
<Callout type="warning">API V2 is currently beta, and will be subject to breaking changes</Callout>
|
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
|
||||||
|
|
||||||
Check out the [API V2 documentation](https://documen.so/api-v2-docs) for details about the API endpoints, request parameters, response formats, and authentication methods.
|
<Callout type="warning">
|
||||||
|
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
|
||||||
Our new API V2 supports the following typed SDKs:
|
|
||||||
|
|
||||||
- [TypeScript](https://github.com/documenso/sdk-typescript)
|
|
||||||
- [Python](https://github.com/documenso/sdk-python)
|
|
||||||
- [Go](https://github.com/documenso/sdk-go)
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
For the staging API, please use the following base URL:
|
|
||||||
`https://stg-app.documenso.dev/api/v2-beta/`
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
||||||
|
|
||||||
📖 [Documentation](https://documen.so/api-v2-docs)
|
|
||||||
|
|
||||||
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
||||||
|
|
||||||
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
||||||
|
|||||||
@@ -86,12 +86,11 @@ You can also set the recipient's role, which determines their actions and permis
|
|||||||
Documenso has 4 roles for recipients with different permissions and actions.
|
Documenso has 4 roles for recipients with different permissions and actions.
|
||||||
|
|
||||||
| Role | Function | Action required | Signature |
|
| Role | Function | Action required | Signature |
|
||||||
| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
||||||
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
||||||
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
||||||
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
||||||
| Assistant | Can help prepare the document by filling in fields on behalf of other signers. | Yes | No |
|
| BCC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
||||||
| CC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +56,7 @@ export const EditDocumentForm = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||||
|
|
||||||
@@ -134,6 +136,18 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: selfSignDocument } = trpc.document.selfSignDocument.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
documentId: initialDocument.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: setPasswordForDocument } =
|
const { mutateAsync: setPasswordForDocument } =
|
||||||
trpc.document.setPasswordForDocument.useMutation();
|
trpc.document.setPasswordForDocument.useMutation();
|
||||||
|
|
||||||
@@ -269,10 +283,22 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSameOwnerAsRecipient =
|
||||||
|
recipients.length === 1 && recipients[0].email === session?.user?.email;
|
||||||
|
|
||||||
|
if (hasSameOwnerAsRecipient) {
|
||||||
|
await selfSignDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/sign/${recipients[0].token}`);
|
||||||
|
} else {
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
// Router refresh is here to clear the router cache for when navigating to /documents.
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
setStep('subject');
|
setStep('subject');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
field.fieldMeta ?? {
|
|
||||||
type: 'checkbox',
|
|
||||||
values: [{ id: 1, checked: false, value: '' }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = parsedFieldMeta.values?.map((item) => ({
|
const values = parsedFieldMeta.values?.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
|||||||
export interface RejectDocumentDialogProps {
|
export interface RejectDocumentDialogProps {
|
||||||
document: Pick<Document, 'id'>;
|
document: Pick<Document, 'id'>;
|
||||||
token: string;
|
token: string;
|
||||||
onRejected?: (reason: string) => void | Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
|
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -80,11 +79,7 @@ export function RejectDocumentDialog({ document, token, onRejected }: RejectDocu
|
|||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
if (onRejected) {
|
|
||||||
await onRejected(reason);
|
|
||||||
} else {
|
|
||||||
router.push(`/sign/${token}/rejected`);
|
router.push(`/sign/${token}/rejected`);
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type EmbedDirectTemplateClientPageProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhiteLabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDirectTemplateClientPage = ({
|
export const EmbedDirectTemplateClientPage = ({
|
||||||
@@ -60,7 +60,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhiteLabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
}: EmbedDirectTemplateClientPageProps) => {
|
}: EmbedDirectTemplateClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -288,7 +288,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhiteLabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@@ -349,7 +349,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
||||||
@@ -360,34 +360,19 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Button
|
<LucideChevronDown
|
||||||
variant="outline"
|
className="text-muted-foreground h-5 w-5"
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : pendingFields.length > 0 ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
|
||||||
onClick={() => setIsExpanded(true)}
|
|
||||||
>
|
|
||||||
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<LucideChevronUp
|
||||||
variant="default"
|
className="text-muted-foreground h-5 w-5"
|
||||||
size="sm"
|
onClick={() => setIsExpanded(true)}
|
||||||
className="md:hidden"
|
/>
|
||||||
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={() => throttledOnCompleteClick()}
|
|
||||||
>
|
|
||||||
<Trans>Complete</Trans>
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@@ -56,16 +55,12 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
documentAuth: template.authOptions,
|
documentAuth: template.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(template),
|
isDocumentPlatform(template),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: template.userId,
|
userId: template.userId,
|
||||||
teamId: template.teamId ?? undefined,
|
teamId: template.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: template.userId,
|
|
||||||
teamId: template.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||||
@@ -110,10 +105,8 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={template.templateMeta}
|
metadata={template.templateMeta}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
/>
|
/>
|
||||||
</RecipientProvider>
|
</RecipientProvider>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { XCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
import type { Signature } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export type EmbedDocumentRejectedPageProps = {
|
|
||||||
name?: string;
|
|
||||||
signature?: Signature;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
|
|
||||||
return (
|
|
||||||
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="flex items-center gap-x-4">
|
|
||||||
<XCircle className="text-destructive h-10 w-10" />
|
|
||||||
|
|
||||||
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
|
||||||
<Trans>Document Rejected</Trans>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-destructive mt-4 flex items-center text-center text-sm">
|
|
||||||
<Trans>You have rejected this document</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
|
|
||||||
<Trans>
|
|
||||||
The document owner has been notified of your decision. They may contact you with further
|
|
||||||
instructions if necessary.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
|
|
||||||
<Trans>No further action is required from you at this time.</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -10,13 +10,7 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import {
|
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
type DocumentData,
|
|
||||||
type Field,
|
|
||||||
FieldType,
|
|
||||||
RecipientRole,
|
|
||||||
SigningStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
@@ -32,13 +26,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||||
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
import { EmbedDocumentRejected } from '../../rejected';
|
|
||||||
import { injectCss } from '../../util';
|
import { injectCss } from '../../util';
|
||||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||||
|
|
||||||
@@ -51,7 +43,7 @@ export type EmbedSignDocumentClientPageProps = {
|
|||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhitelabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +56,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhitelabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
}: EmbedSignDocumentClientPageProps) => {
|
}: EmbedSignDocumentClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@@ -83,9 +75,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
const [hasRejectedDocument, setHasRejectedDocument] = useState(
|
|
||||||
recipient.signingStatus === SigningStatus.REJECTED,
|
|
||||||
);
|
|
||||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||||
);
|
);
|
||||||
@@ -94,8 +83,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
|
||||||
|
|
||||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
@@ -174,25 +161,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDocumentRejected = (reason: string) => {
|
|
||||||
if (window.parent) {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
action: 'document-rejected',
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
documentId,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
reason,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'*',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasRejectedDocument(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
@@ -206,13 +174,12 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
// Since a recipient can be provided a name we can lock it without requiring
|
// 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.
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
setIsNameLocked(!!data.lockName);
|
setIsNameLocked(!!data.lockName);
|
||||||
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
|
||||||
|
|
||||||
if (data.darkModeDisabled) {
|
if (data.darkModeDisabled) {
|
||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhitelabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@@ -241,10 +208,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
if (hasRejectedDocument) {
|
|
||||||
return <EmbedDocumentRejected name={fullName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCompletedDocument) {
|
if (hasCompletedDocument) {
|
||||||
return (
|
return (
|
||||||
<EmbedDocumentCompleted
|
<EmbedDocumentCompleted
|
||||||
@@ -266,16 +229,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
{allowDocumentRejection && (
|
|
||||||
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
|
|
||||||
<RejectDocumentDialog
|
|
||||||
document={{ id: documentId }}
|
|
||||||
token={token}
|
|
||||||
onRejected={onDocumentRejected}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="embed--DocumentViewer flex-1">
|
<div className="embed--DocumentViewer flex-1">
|
||||||
@@ -288,7 +241,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
@@ -303,36 +256,19 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Button
|
<LucideChevronDown
|
||||||
variant="outline"
|
className="text-muted-foreground h-5 w-5"
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : pendingFields.length > 0 ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
|
||||||
onClick={() => setIsExpanded(true)}
|
|
||||||
>
|
|
||||||
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<LucideChevronUp
|
||||||
variant="default"
|
className="text-muted-foreground h-5 w-5"
|
||||||
size="sm"
|
onClick={() => setIsExpanded(true)}
|
||||||
className="md:hidden"
|
/>
|
||||||
disabled={
|
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
|
||||||
}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={() => throttledOnCompleteClick()}
|
|
||||||
>
|
|
||||||
<Trans>Complete</Trans>
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -484,7 +420,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
|
className="col-start-2"
|
||||||
disabled={
|
disabled={
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@@ -63,16 +62,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
return <EmbedPaywall />;
|
return <EmbedPaywall />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(document),
|
isDocumentPlatform(document),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: document.userId,
|
userId: document.userId,
|
||||||
teamId: document.teamId ?? undefined,
|
teamId: document.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: document.userId,
|
|
||||||
teamId: document.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
@@ -131,10 +126,8 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={document.documentMeta}
|
metadata={document.documentMeta}
|
||||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@@ -13,5 +13,4 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((value) => value || undefined),
|
.transform((value) => value || undefined),
|
||||||
lockName: z.boolean().optional().default(false),
|
lockName: z.boolean().optional().default(false),
|
||||||
allowDocumentRejection: z.boolean().optional(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -336,6 +336,16 @@ export const DocumentHistorySheet = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN }, ({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Signed by',
|
||||||
|
value: data.recipientEmail,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))
|
||||||
.with(
|
.with(
|
||||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
|
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
|
||||||
({ data }) => (
|
({ data }) => (
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -35722,6 +35722,21 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
|
"version": "14.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz",
|
||||||
|
"integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { expect, test } from '@playwright/test';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
||||||
|
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
@@ -612,7 +614,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(`/sign/${recipient?.token}`);
|
await page.goto(`/sign/${recipient!.token}`);
|
||||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
|
|
||||||
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||||
@@ -630,24 +632,22 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
await page.waitForURL(`/sign/${recipient?.token}/complete`);
|
await page.waitForURL(`/sign/${recipient!.token}/complete`);
|
||||||
await expect(page.getByText('Document Signed')).toBeVisible();
|
await expect(page.getByText('Document Signed')).toBeVisible();
|
||||||
|
|
||||||
const updatedRecipient = await prisma.recipient.findFirst({
|
const updatedRecipient = await getRecipientById({
|
||||||
where: { id: recipient?.id },
|
documentId: document.id,
|
||||||
|
id: recipient!.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the document to be signed.
|
// Wait for the document to be signed.
|
||||||
await page.waitForTimeout(5000);
|
await expect(async () => {
|
||||||
|
const signedDocument = await getDocumentById({ id: document.id, userId: user.id });
|
||||||
const finalDocument = await prisma.document.findFirst({
|
expect(signedDocument?.status).toBe(DocumentStatus.COMPLETED);
|
||||||
where: { id: createdDocument?.id },
|
}).toPass();
|
||||||
});
|
|
||||||
|
|
||||||
expect(finalDocument?.status).toBe(DocumentStatus.COMPLETED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
|
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
|
||||||
@@ -655,7 +655,7 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode',
|
|||||||
}) => {
|
}) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
|
|
||||||
const { document, recipients } = await seedPendingDocumentWithFullFields({
|
const { recipients } = await seedPendingDocumentWithFullFields({
|
||||||
owner: user,
|
owner: user,
|
||||||
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
||||||
fields: [FieldType.SIGNATURE],
|
fields: [FieldType.SIGNATURE],
|
||||||
@@ -682,3 +682,85 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode',
|
|||||||
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
|
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
|
||||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW]: should be able to self sign a document', async ({ page }) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentTitle = `Self-Signing-${Date.now()}.pdf`;
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
await page.getByLabel('Title').fill(documentTitle);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||||
|
|
||||||
|
const documentRecipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token, email, id: recipientId } = documentRecipients[0];
|
||||||
|
|
||||||
|
expect(documentRecipients.length).toBe(1);
|
||||||
|
expect(email).toBe(user.email);
|
||||||
|
|
||||||
|
await page.waitForURL(`/sign/${token}`);
|
||||||
|
await expect(page.getByRole('heading', { name: documentTitle })).toBeVisible();
|
||||||
|
|
||||||
|
const { status } = await getDocumentByToken(token);
|
||||||
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: { recipientId, documentId: document.id },
|
||||||
|
});
|
||||||
|
const recipientField = fields[0];
|
||||||
|
expect(recipientField).not.toBeNull();
|
||||||
|
|
||||||
|
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas#signature');
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
if (box) {
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(`/sign/${token}/complete`);
|
||||||
|
await expect(page.getByText('Document Signed')).toBeVisible();
|
||||||
|
|
||||||
|
const updatedRecipient = await getRecipientById({ documentId: document.id, id: recipientId });
|
||||||
|
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const signedDocument = await getDocumentById({ id: document.id, userId: user.id });
|
||||||
|
expect(signedDocument?.status).toBe(DocumentStatus.COMPLETED);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { stripe } from '@documenso/lib/server-only/stripe';
|
|||||||
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
||||||
|
|
||||||
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
||||||
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
|
const planTypes = typeof plan === 'string' ? [plan] : plan;
|
||||||
|
|
||||||
const prices = await stripe.prices.list({
|
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||||
|
|
||||||
|
const { data: prices } = await stripe.prices.search({
|
||||||
|
query,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prices.data.filter(
|
return prices.filter((price) => price.type === 'recurring');
|
||||||
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { Subscription } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';
|
|
||||||
|
|
||||||
export type IsCommunityPlanOptions = {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user or team is on the community plan.
|
|
||||||
*/
|
|
||||||
export const isCommunityPlan = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
}: IsCommunityPlanOptions): Promise<boolean> => {
|
|
||||||
let subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
if (teamId) {
|
|
||||||
subscriptions = await prisma.team
|
|
||||||
.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: teamId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
owner: {
|
|
||||||
include: {
|
|
||||||
subscriptions: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((team) => team.owner.subscriptions);
|
|
||||||
} else {
|
|
||||||
subscriptions = await prisma.user
|
|
||||||
.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
subscriptions: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((user) => user.subscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriptions.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const communityPlanPriceIds = await getCommunityPlanPriceIds();
|
|
||||||
|
|
||||||
return subscriptionsContainsActivePlan(subscriptions, communityPlanPriceIds);
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import {
|
import {
|
||||||
@@ -73,13 +72,6 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
|
||||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
|
||||||
message: 'Recipient has already rejected the document',
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export const findDocuments = async ({
|
|||||||
const searchFilter: Prisma.DocumentWhereInput = {
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
OR: [
|
OR: [
|
||||||
{ title: { contains: query, mode: 'insensitive' } },
|
{ title: { contains: query, mode: 'insensitive' } },
|
||||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
|
||||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -34,14 +34,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
userId: userId,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
recipients: {
|
recipients: {
|
||||||
some: {
|
some: {
|
||||||
@@ -96,23 +88,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
},
|
},
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
teamId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
177
packages/lib/server-only/document/self-sign-document.ts
Normal file
177
packages/lib/server-only/document/self-sign-document.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { jobs } from '../../jobs/client';
|
||||||
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
|
|
||||||
|
export type SelfSignDocumentOptions = {
|
||||||
|
documentId: number;
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
requestMetadata?: ApiRequestMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selfSignDocument = async ({
|
||||||
|
documentId,
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
requestMetadata,
|
||||||
|
}: SelfSignDocumentOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.findUnique({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
...(teamId
|
||||||
|
? {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userId,
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: {
|
||||||
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||||
|
},
|
||||||
|
documentMeta: true,
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.recipients.length === 0) {
|
||||||
|
throw new Error('Document has no recipients');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.recipients.length !== 1 || document.recipients[0].email !== user.email) {
|
||||||
|
throw new Error('Invalid document for self-signing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status === DocumentStatus.COMPLETED) {
|
||||||
|
throw new Error('Can not sign completed document');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { documentData } = document;
|
||||||
|
|
||||||
|
if (!documentData || !documentData.data) {
|
||||||
|
throw new Error('Document data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.formValues) {
|
||||||
|
const file = await getFile(documentData);
|
||||||
|
|
||||||
|
const prefilled = await insertFormValuesInPdf({
|
||||||
|
pdf: Buffer.from(file),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newDocumentData = await putPdfFile({
|
||||||
|
name: document.title,
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prisma.document.update({
|
||||||
|
where: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
documentDataId: newDocumentData.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(document, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientHasNoActionToTake =
|
||||||
|
document.recipients[0].role === RecipientRole.CC ||
|
||||||
|
document.recipients[0].signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
if (recipientHasNoActionToTake) {
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'internal.seal-document',
|
||||||
|
payload: {
|
||||||
|
documentId,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDocument = await prisma.$transaction(async (tx) => {
|
||||||
|
if (document.status === DocumentStatus.DRAFT) {
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN,
|
||||||
|
documentId: document.id,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
user,
|
||||||
|
data: {
|
||||||
|
recipientId: document.recipients[0].id,
|
||||||
|
recipientEmail: document.recipients[0].email,
|
||||||
|
recipientName: document.recipients[0].name,
|
||||||
|
recipientRole: document.recipients[0].role,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: document.recipients[0].id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedDocument;
|
||||||
|
};
|
||||||
@@ -94,7 +94,7 @@ export const sendDocument = async ({
|
|||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
if (!documentData.data) {
|
if (!documentData || !documentData.data) {
|
||||||
throw new Error('Document data not found');
|
throw new Error('Document data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthTypesSchema } from
|
|||||||
export const ZDocumentAuditLogTypeSchema = z.enum([
|
export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||||
// Document actions.
|
// Document actions.
|
||||||
'EMAIL_SENT',
|
'EMAIL_SENT',
|
||||||
|
'SELF_SIGN',
|
||||||
|
|
||||||
// Document modification events.
|
// Document modification events.
|
||||||
'FIELD_CREATED',
|
'FIELD_CREATED',
|
||||||
@@ -181,6 +182,14 @@ export const ZDocumentAuditLogEventEmailSentSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event: Self sign
|
||||||
|
*/
|
||||||
|
export const ZDocumentAuditLogSelfSignSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN),
|
||||||
|
data: ZBaseRecipientDataSchema,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event: Document completed.
|
* Event: Document completed.
|
||||||
*/
|
*/
|
||||||
@@ -566,6 +575,7 @@ export const ZDocumentAuditLogBaseSchema = z.object({
|
|||||||
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||||
z.union([
|
z.union([
|
||||||
ZDocumentAuditLogEventEmailSentSchema,
|
ZDocumentAuditLogEventEmailSentSchema,
|
||||||
|
ZDocumentAuditLogSelfSignSchema,
|
||||||
ZDocumentAuditLogEventDocumentCompletedSchema,
|
ZDocumentAuditLogEventDocumentCompletedSchema,
|
||||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentDeletedSchema,
|
ZDocumentAuditLogEventDocumentDeletedSchema,
|
||||||
|
|||||||
@@ -369,16 +369,6 @@ export const formatDocumentAuditLogAction = (
|
|||||||
identified: result,
|
identified: result,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => {
|
|
||||||
const userName = prefix || _(msg`Recipient`);
|
|
||||||
|
|
||||||
const result = msg`${userName} rejected the document`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
anonymous: result,
|
|
||||||
identified: result,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
||||||
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
|
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
|
||||||
identified: data.isResending
|
identified: data.isResending
|
||||||
@@ -389,6 +379,14 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Document completed`,
|
anonymous: msg`Document completed`,
|
||||||
identified: msg`Document completed`,
|
identified: msg`Document completed`,
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN }, () => ({
|
||||||
|
anonymous: msg`Self-signed document`,
|
||||||
|
identified: msg`${prefix} self-signed the document`,
|
||||||
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => ({
|
||||||
|
anonymous: msg`Document rejected`,
|
||||||
|
identified: msg`${prefix} rejected the document: ${data.reason}`,
|
||||||
|
}))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
|
|
||||||
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Session" DROP COLUMN "expires",
|
|
||||||
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
ADD COLUMN "ipAddress" TEXT,
|
|
||||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
ADD COLUMN "userAgent" TEXT;
|
|
||||||
@@ -270,9 +270,8 @@ model Account {
|
|||||||
scope String?
|
scope String?
|
||||||
id_token String? @db.Text
|
id_token String? @db.Text
|
||||||
session_state String?
|
session_state String?
|
||||||
password String?
|
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
}
|
}
|
||||||
@@ -281,13 +280,7 @@ model Session {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
sessionToken String @unique
|
sessionToken String @unique
|
||||||
userId Int
|
userId Int
|
||||||
|
expires DateTime
|
||||||
ipAddress String?
|
|
||||||
userAgent String?
|
|
||||||
expiresAt DateTime
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,6 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
|||||||
|
|
||||||
import { prisma } from '..';
|
import { prisma } from '..';
|
||||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||||
import { seedPendingDocument } from './documents';
|
|
||||||
import { seedDirectTemplate, seedTemplate } from './templates';
|
|
||||||
|
|
||||||
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
|
||||||
return prisma.documentData.create({
|
|
||||||
data: {
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: documentData,
|
|
||||||
initialData: documentData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const seedDatabase = async () => {
|
export const seedDatabase = async () => {
|
||||||
const examplePdf = fs
|
const examplePdf = fs
|
||||||
@@ -51,14 +39,24 @@ export const seedDatabase = async () => {
|
|||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= 4; i++) {
|
const examplePdfData = await prisma.documentData.upsert({
|
||||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
where: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.document.create({
|
await prisma.document.create({
|
||||||
data: {
|
data: {
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
title: `Example Document ${i}`,
|
title: 'Example Document',
|
||||||
documentDataId: documentData.id,
|
documentDataId: examplePdfData.id,
|
||||||
userId: exampleUser.id,
|
userId: exampleUser.id,
|
||||||
recipients: {
|
recipients: {
|
||||||
create: {
|
create: {
|
||||||
@@ -69,61 +67,6 @@ export const seedDatabase = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= 4; i++) {
|
|
||||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
|
||||||
|
|
||||||
await prisma.document.create({
|
|
||||||
data: {
|
|
||||||
source: DocumentSource.DOCUMENT,
|
|
||||||
title: `Document ${i}`,
|
|
||||||
documentDataId: documentData.id,
|
|
||||||
userId: adminUser.id,
|
|
||||||
recipients: {
|
|
||||||
create: {
|
|
||||||
name: String(exampleUser.name),
|
|
||||||
email: exampleUser.email,
|
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await seedPendingDocument(exampleUser, [adminUser], {
|
|
||||||
key: 'example-pending',
|
|
||||||
createDocumentOptions: {
|
|
||||||
title: 'Pending Document',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await seedPendingDocument(adminUser, [exampleUser], {
|
|
||||||
key: 'admin-pending',
|
|
||||||
createDocumentOptions: {
|
|
||||||
title: 'Pending Document',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
seedTemplate({
|
|
||||||
title: 'Template 1',
|
|
||||||
userId: exampleUser.id,
|
|
||||||
}),
|
|
||||||
seedDirectTemplate({
|
|
||||||
title: 'Direct Template 1',
|
|
||||||
userId: exampleUser.id,
|
|
||||||
}),
|
|
||||||
|
|
||||||
seedTemplate({
|
|
||||||
title: 'Template 1',
|
|
||||||
userId: adminUser.id,
|
|
||||||
}),
|
|
||||||
seedDirectTemplate({
|
|
||||||
title: 'Direct Template 1',
|
|
||||||
userId: adminUser.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const testUsers = [
|
const testUsers = [
|
||||||
'test@documenso.com',
|
'test@documenso.com',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/
|
|||||||
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
||||||
|
import { selfSignDocument } from '@documenso/lib/server-only/document/self-sign-document';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
ZMoveDocumentToTeamSchema,
|
ZMoveDocumentToTeamSchema,
|
||||||
ZResendDocumentMutationSchema,
|
ZResendDocumentMutationSchema,
|
||||||
ZSearchDocumentsMutationSchema,
|
ZSearchDocumentsMutationSchema,
|
||||||
|
ZSelfSignDocumentMutationSchema,
|
||||||
ZSetPasswordForDocumentMutationSchema,
|
ZSetPasswordForDocumentMutationSchema,
|
||||||
ZSetSigningOrderForDocumentMutationSchema,
|
ZSetSigningOrderForDocumentMutationSchema,
|
||||||
ZSuccessResponseSchema,
|
ZSuccessResponseSchema,
|
||||||
@@ -467,6 +469,28 @@ export const documentRouter = router({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
selfSignDocument: authenticatedProcedure
|
||||||
|
.input(ZSelfSignDocumentMutationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const { documentId, teamId } = input;
|
||||||
|
|
||||||
|
return await selfSignDocument({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
documentId,
|
||||||
|
teamId,
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'We were unable to self sign this document. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -293,6 +293,11 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZSelfSignDocumentMutationSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
teamId: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
|
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
|
||||||
|
|
||||||
export const ZSetPasswordForDocumentMutationSchema = z.object({
|
export const ZSetPasswordForDocumentMutationSchema = z.object({
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Type,
|
Type,
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { prop, sortBy } from 'remeda';
|
import { prop, sortBy } from 'remeda';
|
||||||
@@ -120,6 +121,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
teamId,
|
teamId,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { data: session } = useSession();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
||||||
@@ -558,6 +560,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
}, [recipientsByRole]);
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
|
const hasSameOwnerAsRecipient =
|
||||||
|
recipientsByRole.SIGNER.length === 1 &&
|
||||||
|
recipientsByRole.SIGNER[0].email === session?.user?.email;
|
||||||
|
|
||||||
const handleAdvancedSettings = () => {
|
const handleAdvancedSettings = () => {
|
||||||
setShowAdvancedSettings((prev) => !prev);
|
setShowAdvancedSettings((prev) => !prev);
|
||||||
};
|
};
|
||||||
@@ -1132,6 +1138,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
documentFlow.onBackStep?.();
|
documentFlow.onBackStep?.();
|
||||||
}}
|
}}
|
||||||
goBackLabel={canRenderBackButtonAsRemove ? msg`Remove` : undefined}
|
goBackLabel={canRenderBackButtonAsRemove ? msg`Remove` : undefined}
|
||||||
|
goNextLabel={hasSameOwnerAsRecipient ? msg`Sign` : undefined}
|
||||||
onGoNextClick={handleGoNextClick}
|
onGoNextClick={handleGoNextClick}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user