chore: merged main

This commit is contained in:
Catalin Pit
2024-09-11 08:40:23 +03:00
389 changed files with 19630 additions and 4241 deletions

View File

@ -0,0 +1,72 @@
---
title: 'Introducing Embedding Support for Documenso'
description: 'Embedding is now here! Learn how we built it and how it can be used to bring e-signing to your own applications.'
authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder'
date: 2024-09-06
tags:
- Development
---
When we first launched Documenso, one of the most requested features was embedding. We knew it was important and aligned with our desire to not just be a e-signing application but to instead provide the e-signature infrastructure for the web and beyond.
With that said, we decided to hold off initially so we could focus on building a solid, well-featured core application. Looking back, this was definitely the right call. Embedding is only as good as the features behind it, and we didn't want to release something that wasn't ready to meet user and developer expectations.
Over the past year, we've been busy adding tons of new features and reaching new levels of compliance, like 21 CFR Part 11. We've also introduced [new fields](/blog/introducing-advanced-signing-fields), [built out an API](/blog/public-api), [added webhooks, integrations with Zapier](/blog/launch-week-2-day-4), and a lot more.
Now that we've laid a solid foundation, it's finally time to focus on embedding, the top-requested feature from both our users and those self-hosting our platform.
## Why Embedding Took Time
In previous projects, Ive often seen embedding built by bundling components for use in a clients website or app. This method gives users maximum flexibility for styling and behavior, while avoiding certain cross-origin issues. However, it can also introduce problems like code conflicts or performance bottlenecks. For example, third-party tools such as Google Tag Manager (GTM) or other marketing scripts can interfere with your SDK. Additionally, the SDK must remain lightweight to avoid slowing down the clients page.
For Documenso, we decided to explore a different approach. After carefully researching our options, we opted for an iframe-based solution. While iframes are typically less flexible—especially when it comes to theming or passing pre-filled data containing personally identifiable information (PII)—we identified ways to mitigate these concerns.
One of the biggest challenges was ensuring that we could pass sensitive data, like emails for pre-filling forms, without exposing PII to our server. To solve this, we used [fragment identifiers](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) in the URL, which are processed client-side and never sent in network requests. This method ensures that PII is protected and not logged by our server or any intermediate web services.
### Using the PostMessage API for Communication
To maintain a high level of interactivity, our iframes communicate with the parent window using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This allows us to notify the parent app when specific events occur inside the iframe, creating a more dynamic user experience and bridging the gap between our iframe-based solution and typical fat SDKs.
Additionally, props are passed into the iframe via the [fragment identifier](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) of the URL. This avoids the need for complex two-way data synchronization between the parent and child frames, making the system stable and more reliable.
### Building the Embeds with Mitosis
Given that our iframe solution is quite lightweight, we saw this as a great opportunity to experiment with [Mitosis](https://mitosis.builder.io/) which would let us do something truly special. For those unfamiliar, Mitosis is a project by Builder.io that lets you write components once and then transpile them into a variety of frameworks like React, Vue, and Svelte.
We used Mitosis to build two key components: a direct template embed and a document signing embed. The direct template allows users to use a template as if it were an evergreen document—meaning that, when someone completes the template, a new document is automatically generated. This is the use case we expect most users to adopt for embedding. For more advanced workflows, we also offer a document signing embed, which can handle multi-recipient workflows and other complex scenarios intended for use in deeper, rich integrations.
Mitosis allowed us to quickly target several popular frameworks, including [React](https://www.npmjs.com/package/@documenso/embed-react), [Preact](https://www.npmjs.com/package/@documenso/embed-preact), [Vue](https://www.npmjs.com/package/@documenso/embed-vue), [Svelte](https://www.npmjs.com/package/@documenso/embed-svelte), and [SolidJS](https://www.npmjs.com/package/@documenso/embed-solid).
I had also hoped to include Angular, but while Mitosis makes it really easy to transpile component, we still have to take care of bundling and packaging the resulting component ourselves. While the above frameworks can all be bundled using Vite.js, Angular still has it's own set of tooling that we would need to learn and use. Given this constraint we opted to put Angular on hold for now while we wait for the newer Vite.js support to mature.
### Challenges and Lessons with Mitosis and more
While our experience with Mitosis was largely positive, there were some challenges along the way. For instance, certain state properties with the same names as props caused issues during the transpilation process, leading to type errors and unexpected transpilation results with some targets.
This was also a challenge since our initial implementation of the two components had some minor separation of concerns which also resulted in some transpilation issues with some targets. We addressed this by removing the separation of concerns for now since it was mostly for show rather than out of necessity.
On top of that, packaging and publishing the embeds posed its own set of challenges, particularly given the growing complexity of JavaScript package management. Tools like [Publint](https://www.npmjs.com/package/publint) helped streamline the process by ensuring we followed best practices for both CommonJS and ESM formats.
### To the Future, The Documenso Platform
With the embedding feature now in place, we're excited to continue expanding Documenso's capabilities. Embeds are just the beginning of what we're calling the Documenso platform. Through our user research, we've learned that while many businesses appreciate having a flexible e-signature solution, they're even more interested in using our tools to build signing functionality directly into their own apps—without worrying about the technical complexities of compliance and security that come with e-signing.
Over the coming months, we'll be working on enhancing our API, strengthening integrations with tools like Zapier, and improving our webhook system. Our goal is to give users the ability to embed e-signatures and document management wherever they need it, whether that's through self-hosting or by using Documenso as a platform. We can't wait to see how our users and self-hosters leverage these new capabilities!
### Ready to Get Started?
If you're ready to embed document signing into your own app or website, check out our [Embedding Documentation](https://docs.documenso.com/developers/embedding?utm_source=blog&utm_campaign=introducing-embedding) to see how easy it is to get started. You'll find everything you need to get started today!
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
autoPlay
loop
controls
/>
We're always here to help! If you have questions or need support, join our [Discord](https://documen.so/discord) or [book a demo](https://documen.so/book-a-demo). We'd love to hear how you're using Documenso or wanting to use Documenso to enhance your workflow.
Stay tuned for more updates as we continue to evolve the Documenso platform and make it even easier to bring document signing into your workflows.

View File

@ -8,6 +8,80 @@ Check out what's new in the latest version and read our thoughts on it. For more
---
# Documenso v1.7.0: Embedded Signing, Copy and Paste, and More
We're thrilled to announce the release of Documenso v1.7.0, packed with exciting new features and improvements that enhance document signing flexibility, user experience, and global accessibility.
We're excited to see what you'll create with this release and we'd love to hear your feedback. Let's dive into the highlights:
## 🌟 Key Features
### Embedded Signing Experience
Take your document signing to the next level with our new embedded signing feature. Now you can seamlessly integrate Documenso's signing process directly into your own website or application, providing a smooth, branded experience for your users.
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
controls
/>
Check out our [Embedding documentation](https://docs.documenso.com/developers/embedding) to learn more about how to get started.
### Copy and Paste Fields
Streamline your document preparation with our new copy and paste functionality for fields. This feature allows you to quickly duplicate fields across your document, saving time and ensuring consistency in your templates.
### Customizable Signature Colors
Recipients can now select a signature color from our list of available colors, supporting workflows where specific colors are required for each recipient, location, or document.
### Enhanced Internationalization (i18n)
Following on from our last release we've now expanded our i18n support to the main web application. We haven't yet added support for any additional languages but that will be coming quickly now that we have completed the hard work of wrapping all of our content in our new i18n system.
These enhancements make Documenso more accessible to users worldwide.
## 🔧 Other Improvements
- **API Enhancements**:
- New endpoint to prefill fields via API
- Updated createFields API endpoint for more flexibility
- Automatically set public profile URL for OIDC users
- **Security and Performance**:
- Document sealing moved to a background job for improved performance
- Disable 2FA with backup codes for enhanced account recovery options
- Extended lifespan for invites and confirmations
- **User Experience**:
- Updated email templates to reflect team-specific information
- Fixed issues with dialog closing on page refresh
- Improved field editing in document templates
- **Other Items**:
- Added Elestio as a one-click deploy option
- Updated README for manual self-hosting
- New environment variable for internal webapp URL configuration
## 📚 New Content
- [Advanced fields article to help you make the most of Documenso's capabilities](/blog/introducing-advanced-signing-fields)
- [Embedding blog post to guide you through how we implemented embedding](/blog/introducing-embedding)
## 👏 Community Contributions
A big thank you to our vibrant community! This release includes contributions from several new contributors, further enriching Documenso's capabilities.
We're excited to see how you'll use these new features to streamline your document workflows. As always, we appreciate your feedback and support in making Documenso the best open-source document signing solution available.
Enjoy exploring v1.7.0!
---
# Documenso v1.6.1: Internationalization, Enhanced OIDC, and More
We're excited to announce the release of Documenso v1.6.1, which brings several improvements to enhance your document signing experience. Here are the key updates:

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.6.1-rc.1",
"version": "1.7.1-rc.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -20,8 +20,8 @@
"@documenso/trpc": "*",
"@documenso/ui": "*",
"@hookform/resolvers": "^3.1.0",
"@lingui/macro": "^4.11.1",
"@lingui/react": "^4.11.1",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",
"@openstatus/react": "^0.0.3",
"cmdk": "^0.2.1",
"contentlayer": "^0.3.4",
@ -32,16 +32,16 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "14.0.3",
"next": "14.2.6",
"next-auth": "4.24.5",
"next-axiom": "^1.1.1",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3",
"react": "18.2.0",
"react": "^18",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0",
"react-dom": "^18",
"react-hook-form": "^7.43.9",
"react-icons": "^4.11.0",
"recharts": "^2.7.2",
@ -50,18 +50,10 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@lingui/loader": "^4.11.1",
"@lingui/swc-plugin": "4.0.6",
"@lingui/loader": "^4.11.3",
"@lingui/swc-plugin": "4.0.8",
"@types/node": "20.1.0",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7"
},
"overrides": {
"next-auth": {
"next": "$next"
},
"next-contentlayer": {
"next": "$next"
}
"@types/react": "^18",
"@types/react-dom": "^18"
}
}

View File

@ -2,6 +2,7 @@ declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?: string;
NEXT_PRIVATE_DATABASE_URL: string;

View File

@ -5,6 +5,8 @@ import { allDocuments } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { content: string } }) => {
@ -29,6 +31,8 @@ const mdxComponents: MDXComponents = {
* Will render the document if it exists, otherwise will return a 404.
*/
export default function ContentPage({ params }: { params: { content: string } }) {
setupI18nSSR();
const post = allDocuments.find((post) => post._raw.flattenedPath === params.content);
if (!post) {

View File

@ -7,6 +7,8 @@ import { ChevronLeft } from 'lucide-react';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { CallToAction } from '~/components/(marketing)/call-to-action';
export const dynamic = 'force-dynamic';
@ -47,6 +49,8 @@ const mdxComponents: MDXComponents = {
};
export default function BlogPostPage({ params }: { params: { post: string } }) {
setupI18nSSR();
const post = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
if (!post) {

View File

@ -5,6 +5,8 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64';
@ -46,8 +48,8 @@ export const SinglePlayerClient = () => {
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
fields: {
title: 'Add document',
description: 'Upload a document and add fields.',
title: msg`Add document`,
description: msg`Upload a document and add fields.`,
stepIndex: 1,
onBackStep: uploadedFile
? () => {
@ -58,8 +60,8 @@ export const SinglePlayerClient = () => {
onNextStep: () => setStep('sign'),
},
sign: {
title: 'Sign',
description: 'Enter your details.',
title: msg`Sign`,
description: msg`Enter your details.`,
stepIndex: 2,
onBackStep: () => setStep('fields'),
},

View File

@ -1,5 +1,7 @@
import type { Metadata } from 'next';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { SinglePlayerClient } from './client';
export const metadata: Metadata = {
@ -13,5 +15,7 @@ export const dynamic = 'force-dynamic';
// !: the Single Player Mode page. This regression was introduced during
// !: the upgrade of Next.js to v13.5.x.
export default function SingleplayerPage() {
setupI18nSSR();
return <SinglePlayerClient />;
}

View File

@ -1,7 +1,6 @@
import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { cookies, headers } from 'next/headers';
import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env';
@ -10,8 +9,6 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -59,25 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags();
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
// Should be safe to remove when we upgrade NextJS.
// https://github.com/vercel/next.js/pull/65008
// Currently if the middleware sets the cookie, it's not accessible in the cookies
// during the same render.
// So we go the roundabout way of checking the header for the set-cookie value.
if (!cookies().get('i18n')) {
const setCookieValue = headers().get('set-cookie');
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
if (i18nCookie) {
const i18n = i18nCookie.split('=')[1];
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
}
}
const { lang, i18n } = setupI18nSSR(overrideLang);
const { lang, locales, i18n } = setupI18nSSR();
return (
<html
@ -105,7 +84,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider>
<TrpcProvider>
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
<I18nClientProvider
initialLocaleData={{ lang, locales }}
initialMessages={i18n.messages}
>
{children}
</I18nClientProvider>
</TrpcProvider>

View File

@ -1,6 +1,6 @@
'use client';
import type { HTMLAttributes } from 'react';
import { type HTMLAttributes, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
@ -9,15 +9,15 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { FaXTwitter } from 'react-icons/fa6';
import { LiaDiscord } from 'react-icons/lia';
import { LuGithub } from 'react-icons/lu';
import { LuGithub, LuLanguages } from 'react-icons/lu';
import LogoImage from '@documenso/assets/logo.png';
import { cn } from '@documenso/ui/lib/utils';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
// import { StatusWidgetContainer } from './status-widget-container';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
@ -44,7 +44,9 @@ const FOOTER_LINKS = [
];
export const Footer = ({ className, ...props }: FooterProps) => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
return (
<div className={cn('border-t py-12', className)} {...props}>
@ -97,13 +99,22 @@ export const Footer = ({ className, ...props }: FooterProps) => {
</p>
<div className="flex flex-row-reverse items-center sm:flex-row">
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" />
<Button
className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2"
variant="ghost"
onClick={() => setLanguageSwitcherOpen(true)}
>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[i18n.locale]?.full || i18n.locale}
</Button>
<div className="flex flex-wrap">
<ThemeSwitcher />
</div>
</div>
</div>
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
</div>
);
};

View File

@ -1,71 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { CheckIcon } from 'lucide-react';
import { LuLanguages } from 'react-icons/lu';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
CommandDialog,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@documenso/ui/primitives/command';
type I18nSwitcherProps = {
className?: string;
};
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
const { i18n, _ } = useLingui();
const [open, setOpen] = useState(false);
const [value, setValue] = useState(i18n.locale);
const setLanguage = async (lang: string) => {
setValue(lang);
setOpen(false);
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
};
return (
<>
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={_(msg`Search languages...`)} />
<CommandList>
<CommandGroup>
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
key={language.short}
value={language.full}
onSelect={async () => setLanguage(language.short)}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
value === language.short ? 'opacity-100' : 'opacity-0',
)}
/>
{SUPPORTED_LANGUAGES[language.short].full}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
};

View File

@ -1,39 +0,0 @@
import { cookies } from 'next/headers';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
export default function middleware(req: NextRequest) {
const lang = extractSupportedLanguage({
headers: req.headers,
cookies: cookies(),
});
const response = NextResponse.next();
response.cookies.set('i18n', lang);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - ingest (analytics)
* - site.webmanifest
*/
{
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};