diff --git a/README.md b/README.md index 5239c79d0..77b023c4f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: The Platform Plan! +> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: The Platform Plan! Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt diff --git a/apps/documentation/pages/developers/_meta.json b/apps/documentation/pages/developers/_meta.json index a9f3c3823..0057496b3 100644 --- a/apps/documentation/pages/developers/_meta.json +++ b/apps/documentation/pages/developers/_meta.json @@ -14,4 +14,4 @@ "public-api": "Public API", "embedding": "Embedding", "webhooks": "Webhooks" -} \ No newline at end of file +} diff --git a/apps/documentation/pages/developers/embedding/_meta.json b/apps/documentation/pages/developers/embedding/_meta.json index 96806de3e..58bcdf546 100644 --- a/apps/documentation/pages/developers/embedding/_meta.json +++ b/apps/documentation/pages/developers/embedding/_meta.json @@ -6,5 +6,6 @@ "solid": "Solid Integration", "preact": "Preact Integration", "angular": "Angular Integration", - "css-variables": "CSS Variables" + "css-variables": "CSS Variables", + "web-components": "Web Components" } diff --git a/apps/documentation/pages/developers/embedding/index.mdx b/apps/documentation/pages/developers/embedding/index.mdx index 27d6f6f8f..2e93941ae 100644 --- a/apps/documentation/pages/developers/embedding/index.mdx +++ b/apps/documentation/pages/developers/embedding/index.mdx @@ -73,14 +73,15 @@ These customization options are available for both Direct Templates and Signing 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) | -| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) | +| 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) | +| 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. @@ -166,6 +167,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper - [Svelte](/developers/embedding/svelte) - [Solid](/developers/embedding/solid) - [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. @@ -177,4 +179,5 @@ If you're using **web components**, the integration process is slightly differen - [Solid Integration](/developers/embedding/solid) - [Preact Integration](/developers/embedding/preact) - [Angular Integration](/developers/embedding/angular) +- [Web Components](/developers/embedding/web-components) - [CSS Variables](/developers/embedding/css-variables) diff --git a/apps/documentation/pages/developers/embedding/web-components.mdx b/apps/documentation/pages/developers/embedding/web-components.mdx new file mode 100644 index 000000000..43ea08788 --- /dev/null +++ b/apps/documentation/pages/developers/embedding/web-components.mdx @@ -0,0 +1,89 @@ +--- +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 + +``` + +## 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 + +``` + +#### 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 + +``` + +#### 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); +``` diff --git a/apps/documentation/pages/developers/public-api/index.mdx b/apps/documentation/pages/developers/public-api/index.mdx index f2745ee82..050810863 100644 --- a/apps/documentation/pages/developers/public-api/index.mdx +++ b/apps/documentation/pages/developers/public-api/index.mdx @@ -21,14 +21,25 @@ Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) f ## API V2 - Beta -Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more. +API V2 is currently beta, and will be subject to breaking changes - - NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs) +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. + +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) + + + For the staging API, please use the following base URL: + `https://stg-app.documenso.dev/api/v2-beta/` 🚀 [V2 Announcement](https://documen.so/sdk-blog) +📖 [Documentation](https://documen.so/api-v2-docs) + 💬 [Leave Feedback](https://documen.so/sdk-feedback) 🔔 [Breaking Changes](https://documen.so/sdk-breaking) diff --git a/apps/documentation/pages/users/signing-documents/index.mdx b/apps/documentation/pages/users/signing-documents/index.mdx index a0a32399d..f37afc8f7 100644 --- a/apps/documentation/pages/users/signing-documents/index.mdx +++ b/apps/documentation/pages/users/signing-documents/index.mdx @@ -85,12 +85,13 @@ 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. -| Role | Function | Action required | Signature | -| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: | -| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes | -| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional | -| Viewer | Needs to confirm they viewed the document. | Yes | No | -| BCC | Receives a copy of the signed document after completion. No action is required. | No | No | +| Role | Function | Action required | Signature | +| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: | +| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes | +| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional | +| 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 | +| CC | Receives a copy of the signed document after completion. No action is required. | No | No | ### Fields diff --git a/apps/web/package.json b/apps/web/package.json index 2bc55c63b..34cc78689 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/web", - "version": "1.9.1-rc.1", + "version": "1.9.1-rc.7", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx b/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx index 627782838..a85d383c9 100644 --- a/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx @@ -44,7 +44,12 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie const [isPending, startTransition] = useTransition(); const { executeActionAuthProcedure } = useRequiredDocumentAuthContext(); - const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta); + const parsedFieldMeta = ZCheckboxFieldMeta.parse( + field.fieldMeta ?? { + type: 'checkbox', + values: [{ id: 1, checked: false, value: '' }], + }, + ); const values = parsedFieldMeta.values?.map((item) => ({ ...item, diff --git a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx index 547a346d8..110fa8f99 100644 --- a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx @@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer; export interface RejectDocumentDialogProps { document: Pick; token: string; + onRejected?: (reason: string) => void | Promise; } -export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) { +export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) { const { toast } = useToast(); const router = useRouter(); const searchParams = useSearchParams(); @@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr setIsOpen(false); - router.push(`/sign/${token}/rejected`); + if (onRejected) { + await onRejected(reason); + } else { + router.push(`/sign/${token}/rejected`); + } } catch (err) { toast({ title: 'Error', diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx index a2cdfe9c7..33c7745c0 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx @@ -182,6 +182,23 @@ export const SigningFieldContainer = ({ )} + {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && + field.fieldMeta?.label && ( +
+ {field.fieldMeta.label} +
+ )} + {children} diff --git a/apps/web/src/app/embed/direct/[[...url]]/client.tsx b/apps/web/src/app/embed/direct/[[...url]]/client.tsx index dbb74a36a..6b6dbc4c7 100644 --- a/apps/web/src/app/embed/direct/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/direct/[[...url]]/client.tsx @@ -49,7 +49,7 @@ export type EmbedDirectTemplateClientPageProps = { fields: Field[]; metadata?: DocumentMeta | TemplateMeta | null; hidePoweredBy?: boolean; - isPlatformOrEnterprise?: boolean; + allowWhiteLabelling?: boolean; }; export const EmbedDirectTemplateClientPage = ({ @@ -60,7 +60,7 @@ export const EmbedDirectTemplateClientPage = ({ fields, metadata, hidePoweredBy = false, - isPlatformOrEnterprise = false, + allowWhiteLabelling = false, }: EmbedDirectTemplateClientPageProps) => { const { _ } = useLingui(); const { toast } = useToast(); @@ -288,7 +288,7 @@ export const EmbedDirectTemplateClientPage = ({ document.documentElement.classList.add('dark-mode-disabled'); } - if (isPlatformOrEnterprise) { + if (allowWhiteLabelling) { injectCss({ css: data.css, cssVars: data.cssVars, @@ -349,7 +349,7 @@ export const EmbedDirectTemplateClientPage = ({ {/* Widget */}
@@ -360,19 +360,34 @@ export const EmbedDirectTemplateClientPage = ({ Sign document - + {isExpanded ? ( + + ) : pendingFields.length > 0 ? ( + + ) : ( + + )}
diff --git a/apps/web/src/app/embed/direct/[[...url]]/page.tsx b/apps/web/src/app/embed/direct/[[...url]]/page.tsx index f4ef467d2..97f7bb953 100644 --- a/apps/web/src/app/embed/direct/[[...url]]/page.tsx +++ b/apps/web/src/app/embed/direct/[[...url]]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'; 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 { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; @@ -55,12 +56,16 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem documentAuth: template.authOptions, }); - const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([ + const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([ isDocumentPlatform(template), isUserEnterprise({ userId: template.userId, teamId: template.teamId ?? undefined, }), + isUserCommunityPlan({ + userId: template.userId, + teamId: template.teamId ?? undefined, + }), ]); const isAccessAuthValid = match(derivedRecipientAccessAuth) @@ -105,8 +110,10 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem recipient={recipient} fields={fields} metadata={template.templateMeta} - hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy} - isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument} + hidePoweredBy={ + isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy + } + allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument} /> diff --git a/apps/web/src/app/embed/rejected.tsx b/apps/web/src/app/embed/rejected.tsx new file mode 100644 index 000000000..deb8bd026 --- /dev/null +++ b/apps/web/src/app/embed/rejected.tsx @@ -0,0 +1,40 @@ +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 ( +
+
+
+ + +

+ Document Rejected +

+
+ +
+ You have rejected this document +
+ +

+ + The document owner has been notified of your decision. They may contact you with further + instructions if necessary. + +

+ +

+ No further action is required from you at this time. +

+
+
+ ); +}; diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index e8ee6ad52..4650f60ab 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -10,7 +10,13 @@ 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, TemplateMeta } from '@documenso/prisma/client'; -import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client'; +import { + type DocumentData, + type Field, + FieldType, + RecipientRole, + SigningStatus, +} from '@documenso/prisma/client'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; @@ -26,11 +32,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; 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 { EmbedClientLoading } from '../../client-loading'; import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentFields } from '../../document-fields'; +import { EmbedDocumentRejected } from '../../rejected'; import { injectCss } from '../../util'; import { ZSignDocumentEmbedDataSchema } from './schema'; @@ -43,7 +51,7 @@ export type EmbedSignDocumentClientPageProps = { metadata?: DocumentMeta | TemplateMeta | null; isCompleted?: boolean; hidePoweredBy?: boolean; - isPlatformOrEnterprise?: boolean; + allowWhitelabelling?: boolean; allRecipients?: RecipientWithFields[]; }; @@ -56,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({ metadata, isCompleted, hidePoweredBy = false, - isPlatformOrEnterprise = false, + allowWhitelabelling = false, allRecipients = [], }: EmbedSignDocumentClientPageProps) => { const { _ } = useLingui(); @@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({ const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted); + const [hasRejectedDocument, setHasRejectedDocument] = useState( + recipient.signingStatus === SigningStatus.REJECTED, + ); const [selectedSignerId, setSelectedSignerId] = useState( allRecipients.length > 0 ? allRecipients[0].id : null, ); @@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({ const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); + const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); + const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; @@ -161,6 +174,25 @@ 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(() => { const hash = window.location.hash.slice(1); @@ -174,12 +206,13 @@ export const EmbedSignDocumentClientPage = ({ // 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); + setAllowDocumentRejection(!!data.allowDocumentRejection); if (data.darkModeDisabled) { document.documentElement.classList.add('dark-mode-disabled'); } - if (isPlatformOrEnterprise) { + if (allowWhitelabelling) { injectCss({ css: data.css, cssVars: data.cssVars, @@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); + if (hasRejectedDocument) { + return ; + } + if (hasCompletedDocument) { return ( {(!hasFinishedInit || !hasDocumentLoaded) && } + {allowDocumentRejection && ( +
+ +
+ )} +
{/* Viewer */}
@@ -241,7 +288,7 @@ export const EmbedSignDocumentClientPage = ({ {/* Widget */}
@@ -256,19 +303,36 @@ export const EmbedSignDocumentClientPage = ({ )} - + {isExpanded ? ( + + ) : pendingFields.length > 0 ? ( + + ) : ( + + )}
@@ -420,7 +484,7 @@ export const EmbedSignDocumentClientPage = ({ ) : (