diff --git a/.env.example b/.env.example
index a07419aaa..ed77d048a 100644
--- a/.env.example
+++ b/.env.example
@@ -10,12 +10,19 @@ NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
# [[AUTH OPTIONAL]]
+# Find documentation on setting up Google OAuth here:
+# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
NEXT_PRIVATE_OIDC_CLIENT_ID=""
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
+NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
+# This can be used to still allow signups for OIDC connections
+# when signup is disabled via `NEXT_PUBLIC_DISABLE_SIGNUP`
+NEXT_PRIVATE_OIDC_ALLOW_SIGNUP=""
+NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
# [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f5542fbb5..e6ff5d1a0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,12 +5,7 @@
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
- "eslint.validate": [
- "typescript",
- "typescriptreact",
- "javascript",
- "javascriptreact"
- ],
+ "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true,
@@ -20,4 +15,7 @@
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
-}
\ No newline at end of file
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
+}
diff --git a/README.md b/README.md
index 74e3bddc5..f32438800 100644
--- a/README.md
+++ b/README.md
@@ -261,6 +261,7 @@ npm run prisma:migrate-deploy
Finally, you can start it with:
```
+cd apps/web
npm run start
```
diff --git a/apps/documentation/pages/developers/self-hosting/_meta.json b/apps/documentation/pages/developers/self-hosting/_meta.json
index b0d771a3e..5f40bbbc2 100644
--- a/apps/documentation/pages/developers/self-hosting/_meta.json
+++ b/apps/documentation/pages/developers/self-hosting/_meta.json
@@ -1,5 +1,6 @@
{
"index": "Getting Started",
"signing-certificate": "Signing Certificate",
- "how-to": "How To"
-}
+ "how-to": "How To",
+ "setting-up-oauth-providers": "Setting up OAuth Providers"
+}
\ No newline at end of file
diff --git a/apps/documentation/pages/developers/self-hosting/setting-up-oauth-providers.mdx b/apps/documentation/pages/developers/self-hosting/setting-up-oauth-providers.mdx
new file mode 100644
index 000000000..0ba359142
--- /dev/null
+++ b/apps/documentation/pages/developers/self-hosting/setting-up-oauth-providers.mdx
@@ -0,0 +1,29 @@
+---
+title: Setting up OAuth Providers
+description: Learn how to set up OAuth providers for your own instance of Documenso.
+---
+
+## Google OAuth (Gmail)
+
+To use Google OAuth, you will need to create a Google Cloud Platform project and enable the Google Identity and Access Management (IAM) API. You will also need to create a new OAuth client ID and download the client secret.
+
+### Create and configure a new OAuth client ID
+
+1. Go to the [Google Cloud Platform Console](https://console.cloud.google.com/)
+2. From the projects list, select a project or create a new one
+3. If the APIs & services page isn't already open, open the console left side menu and select APIs & services
+4. On the left, click Credentials
+5. Click New Credentials, then select OAuth client ID
+6. When prompted to select an application type, select Web application
+7. Enter a name for your client ID, and click Create
+8. Click the download button to download the client secret
+9. Set the authorized javascript origins to `https://`
+10. Set the authorized redirect URIs to `https:///api/auth/callback/google`
+11. In the Documenso environment variables, set the following:
+
+```
+NEXT_PRIVATE_GOOGLE_CLIENT_ID=
+NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=
+```
+
+Finally verify the signing in with Google works by signing in with your Google account and checking the email address in your profile.
diff --git a/apps/documentation/pages/users/_meta.json b/apps/documentation/pages/users/_meta.json
index c5e3ce20a..3ccb8e7c6 100644
--- a/apps/documentation/pages/users/_meta.json
+++ b/apps/documentation/pages/users/_meta.json
@@ -1,5 +1,6 @@
{
"index": "Introduction",
+ "support": "Support",
"-- How To Use": {
"type": "separator",
"title": "How To Use"
@@ -13,6 +14,7 @@
"type": "separator",
"title": "Legal Overview"
},
+ "fair-use": "Fair Use Policy",
"licenses": "Licenses",
"compliance": "Compliance"
}
diff --git a/apps/documentation/pages/users/fair-use.mdx b/apps/documentation/pages/users/fair-use.mdx
new file mode 100644
index 000000000..2b2eb605a
--- /dev/null
+++ b/apps/documentation/pages/users/fair-use.mdx
@@ -0,0 +1,34 @@
+---
+title: Fair Use Policy
+description: Learn about our fair use policy, which enables us to have unlimited plans.
+---
+
+import { Callout } from 'nextra/components';
+
+# Fair Use Policy
+
+### Why
+
+We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using. This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
+
+### Spirit of the Plan
+
+> Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
+
+
+ What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
+ pricing. We won’t block your account without reaching out. [Message
+ us](mailto:support@documenso.com) for questions. It's probably fine, though.
+
+
+### DO
+
+- Sign as many documents with the individual plan for your single business or organization you are part of
+- Use the API and Zapier to automate all your signing to sign as much as possible
+- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
+
+### DON'T
+
+- Use the individual account's API to power a platform
+- Run a huge company, signing thousands of documents per day on a two-user team plan using the API
+- Let this policy make you overthink. If you are a paying customer, we want you to win, and it's probably fine
diff --git a/apps/documentation/pages/users/get-started/account-creation.mdx b/apps/documentation/pages/users/get-started/account-creation.mdx
index 3a60cee98..bf1f67983 100644
--- a/apps/documentation/pages/users/get-started/account-creation.mdx
+++ b/apps/documentation/pages/users/get-started/account-creation.mdx
@@ -3,7 +3,7 @@ title: Create Your Account
description: Learn how to create an account on Documenso.
---
-import { Steps } from 'nextra/components';
+import { Callout, Steps } from 'nextra/components';
# Create Your Account
@@ -14,6 +14,8 @@ The first step to start using Documenso is to pick a plan and create an account.
Explore each plan's features and choose the one that best suits your needs. The [pricing page](https://documen.so/pricing) has more information about the plans.
+All plans are subject to our [Fair Use Policy](/users/fair-use).
+
### Create an account
If you are unsure which plan to choose, you can start with the free plan and upgrade later.
diff --git a/apps/documentation/pages/users/support.mdx b/apps/documentation/pages/users/support.mdx
new file mode 100644
index 000000000..7eb00636f
--- /dev/null
+++ b/apps/documentation/pages/users/support.mdx
@@ -0,0 +1,38 @@
+---
+title: Support
+description: Learn what types of support we offer.
+---
+
+# Support
+
+## Community Support
+
+If you are a developer or free user, you can reach out to the community or raise an issue:
+
+### [Create Github Issues](https://github.com/documenso/documenso/issues)
+
+The community and the core team address GitHub issues. Be sure to check if a similar issue already exists. Please note that while we want to address everything immediately, we must prioritize.
+
+### [Join our Discord](https://documen.so/discord)
+
+You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
+
+## Paid Account Support
+
+### Email: support@documenso.com
+
+If you are paying customers facing issues, email our customer support, especially in urgent cases.
+
+### Private Discord channel
+
+If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
+
+## Enterprise Support
+
+### Email: support@documenso.com
+
+If you are paying customers facing issues, email our customer support, especially in urgent cases.
+
+### Slack
+
+If your team is on Slack, we can create a private workspace to support you more closely.
diff --git a/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx b/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx
index 540f7a0a5..b64953c8c 100644
--- a/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx
+++ b/apps/marketing/content/blog/announcing-vial-21-cfr-part-11.mdx
@@ -11,14 +11,7 @@ tags:
- Compliance
---
-
+
Vial.com uses Documenso for 21 CFR Part 11 compliant signing.
@@ -26,42 +19,40 @@ tags:
> TLDR; We launched Vial.com on Documenso and are open for 21 CFR Part 11 business.
# What is 21 CFR
-You have never heard of 21 CFR Part 11? You are in good company since most people haven't. If you have, you probably work in an industry regulated by the U.S. Food and Drug Administration (FDA). Title 21 of the Code of Federal Regulations (CFR) is dedicated to detailing FDA-regulated business, and sub-part 11 sets out guidelines for using electronic signatures in this highly regulated field. Hence, 21 CFR Part 11 is highly relevant for regulated industries that aim to employ digital signatures. The guidelines set out in 21 CFR Part 11 aim to provide trustworthy, reliable, and equivalent to paper records and handwritten signatures. All Industries that fall under the FDA's regulation, e.g. pharmaceuticals, biotechnology, medical devices, and biologics, must comply with these rules when choosing or creating systems for electronic signatures.
+
+You have never heard of 21 CFR Part 11? You are in good company since most people haven't. If you have, you probably work in an industry regulated by the U.S. Food and Drug Administration (FDA). Title 21 of the Code of Federal Regulations (CFR) is dedicated to detailing FDA-regulated business, and sub-part 11 sets out guidelines for using electronic signatures in this highly regulated field. Hence, 21 CFR Part 11 is highly relevant for regulated industries that aim to employ digital signatures. The guidelines set out in 21 CFR Part 11 aim to provide trustworthy, reliable, and equivalent to paper records and handwritten signatures. All Industries that fall under the FDA's regulation, e.g. pharmaceuticals, biotechnology, medical devices, and biologics, must comply with these rules when choosing or creating systems for electronic signatures.
Compliance with 21 CFR Part 11 is crucial for companies to use electronic records and signatures in their operations legally. It affects how companies manage documentation, conduct audits, and maintain regulatory submissions. Non-compliance can result in legal penalties, rejected submissions, and delays in product approvals, emphasizing the importance of adhering to these guidelines in FDA-regulated activities.
# Vial.com
+
Vial is a technology company on a mission to advance programs to market through computationally designed therapeutics and cost-effective clinical trials. It is imperative that Vial manages this process securely, effectively, and highly compliant. By leveraging it's modern platform, Vial aims to accelerate drug development and, ultimately, time to market for new therapies. You can learn more about them [here](https://vial.com/about-us).
[Together](https://documen.so/vial-documenso), Documenso and Vial set out to create the first open-source, 21 CFR Part 11 compliant signing solution. After iterating over the product together, Vial moved their operation from DocuSign, a known legacy signing provider, to a Documenso Enterprise plan. We are very happy to be able to support Vial’s mission by fulfilling our own: bringing open signing and all its innovation to where it's needed.
# 21 CFR Part 11 on Documenso Highlights
+
21 CFR Part 11 is a highly complex statute, and going into the all design rationales and the following implementation details, deserves its own article later. For now, I want to share a few notable highlights.
## The Full Experience
+
We implemented 21 CFR Part 11, keeping the main user experience of Documenso intact. Our 21 CFR module is not separate but natively integrated into all Documenso flows, thus not sacrificing usability for compliance. This also means most (if not all) advanced features we offer are usable in a compliant way. This prevents customers from being trapped in an anti-innovation bubble, not allowing access to new features for fear of non-compliance.
## Action Reauth Using Passkeys
-
+
+
Using passkeys (used here via fingerprint scanner) is the smoothest way to re-authenticate.
-
One of the requirements affecting day-to-day life the most is the requirement to actually reauthenticate every signature placed on a document. While we can't change that, we can help make the reauthentication as painless as possible. To this end, we opted for passkeys. While Documenso supports passkeys to log in, they are also supported to authenticate signing on a per-signature level as part of the Documenso Enterprise Plan. The user still has to authenticate every signature but can now do so from the comfort of their passkey provider, be that 1Password, their browser, or any other provider.
## Direct Links
+
We recently launched [Direct Template Links](https://documen.so/direct-links), a new way to let people sign and fill out forms. Links can be completed anytime, creating a new document in the process. Direct Links are also 21 CFR part 11 compliant, using action reauthentication, audit log, and all other compliance requirements.
# Documenso Enterprise Plan
+
With the successful launch of Vial, we are now open for business. 21 CFR Part 11 compliance is part of the Documenso Enterprise plan, which includes all regulations we currently support and upcoming additions. While the pricing depends heavily on your needs and scale, we offer fixed-price plans for better predictability for both sides. In our experience, volume-based pricing is a legacy headache we want to avoid.
If you are FDA-regulated and looking for a modern signing solution, we are happy to discuss your requirements in detail. You can write us (hi@documenso.com) or contact [our enterprise team](https://documen.so/21cfr) at any time or stage.
@@ -70,4 +61,3 @@ If you have any questions or comments, please reach out on [Twitter / X](https:/
Best from Hamburg\
Timur
-
diff --git a/apps/marketing/content/blog/introducing-advanced-signing-fields.mdx b/apps/marketing/content/blog/introducing-advanced-signing-fields.mdx
new file mode 100644
index 000000000..8e873c120
--- /dev/null
+++ b/apps/marketing/content/blog/introducing-advanced-signing-fields.mdx
@@ -0,0 +1,599 @@
+---
+title: 'Enhancing Document Signing: Introducing 5 New Advanced Fields'
+description: "Explore Documenso's new advanced signing fields, including improved text fields, numbers, radio buttons, checkboxes, and dropdowns. Learn about the development challenges we overcame and how these additions provide greater flexibility for document signing."
+authorName: 'Catalin Pit'
+authorImage: '/blog/blog-author-catalin.webp'
+authorRole: 'I like to code and write'
+date: 2024-08-09
+tags:
+ - Signing fields
+ - Development
+---
+
+Until recently, Documenso provided a set of 5 fields for document signing: signature, email, name, date, and a text field for additional information. While these fields covered the basic requirements for document signing, we recognized the need for more flexibility and variety.
+
+As a result, we've decided to introduce several additional fields, such as:
+
+- _(an improved)_ Text field
+- Number field
+- Radio field
+- Checkbox field
+- Dropdown/Select field
+
+These new fields bring more flexibility and variety to Documenso. As the document owner, they allow you to gather more specific or extra information from the signers.
+
+## New Fields Introduction
+
+Let's take a closer look at each new field type.
+
+### Text Field
+
+While the text field was previously available, it could not be configured. It was a simple input box where signers could enter a single line of text.
+
+The image illustrates the old text field in the document editor.
+
+
+
+The revamped text field now offers a range of configuration options, allowing you to:
+
+- Add a label, placeholder, default text, and character limit
+- Set the field as required or read-only
+
+
+
+On the signing side, the field remained mostly the same visually. The only thing that changed is the functionality, which needs to take into consideration the validation rules. For example, if the field is required, the signer must enter a value to sign it. Or, if the field has a character limit, the value entered by the signer shouldn't exceed the limit.
+
+The image below illustrates four different text fields with various configurations.
+
+
+
+The first text field has no default value ("Add text") or configuration. You can sign the field by entering any text.
+
+
+
+The second text field, "label-1"/"text-1", has the following configurations:
+
+- Label
+- Placeholder
+- Default text
+- Character limit
+
+Since there is a default value, the field auto-signs with that value. However, you can re-sign the field with a new value that doesn't exceed the character limit.
+
+
+
+The third field, "label-2"/"text-2", has the same configurations as the second one, with one addition - the `required` option is checked. When the field is marked as `required`, you must sign it before completing the document.
+
+Apart from that, it works like the second field.
+
+
+
+The fourth field, "label-3"/"text-3", has the same configurations as the second one, with one addition—`read-only` is checked. That means the field auto-signs with the default value, and you cannot modify it.
+
+#### Unsigned Fields
+
+You can unsign a field to change the value and sign it again. The unsigned state of the field varies depending on its configuration:
+
+- If the field has a label, it displays it instead of "Add text" when unsigned.
+- If the field has a default value, the default value will be shown when unsigned.
+- If the field has both a label and a default value, the label will take precedence and be displayed when unsigned.
+
+The image below shows the unsigned state of the text fields.
+
+
+
+The only exception is the fourth, read-only field, which cannot be unsigned or modified.
+
+### Number Field
+
+We also introduced a new "Number" field for inserting and signing documents with numeric values. This field helps collect quantities, measurements, and other data best represented as numbers.
+
+
+
+The "Number" field offers a range of configuration options, which allows you to:
+
+- Set a label, placeholder and default value
+- Specify the number format
+- Mark the field as _required_ or _read-only_
+- Specify minimum and maximum values
+
+The Number field looks and works similarly to the Text field. The difference is that it accepts only numeric values and has 2 additional configurations: the number format and the minimum and maximum values.
+
+### Radio Field
+
+Radio buttons allow signers to select a single option from a pre-defined list the document owner sets.
+
+Before sending the document for signing, you must add at least one radio option, which can contain a string or an empty value and can be checked or unchecked. However, it's important to note that only one option can be checked at a time.
+
+When it comes to field configuration, you can mark the field as _required_ or _read-only_.
+
+
+
+The image below shows what the signer sees after the document is sent for signing.
+
+
+
+Note: The image is modified to display both the unsigned and signed states of the field.
+
+Since the field has a preselected option (option `radio-val-2-checked`), it will automatically sign with that value and appear like the field marked with the number 1.
+
+If the field is not read-only, the signer can:
+
+- Unsign the field and choose another option by clicking on it.
+- Re-sign with the default value by refreshing the page when the field is unsigned.
+
+However, if the field is marked as read-only, the signer cannot modify the preselected value.
+
+### Dropdown/Select Field
+
+We have also introduced a new "Dropdown/Select" field that allows signers to pick an option from a pre-defined list of choices. This field type is ideal for scenarios with limited valid options, such as selecting a country, state, or category.
+
+When setting up a "Dropdown/Select" field, you can:
+
+- Add multiple options
+- Mark the field as _required_ or _read-only_
+- Pick a default option from the list of choices
+
+
+
+On the signing page, the "Dropdown/Select" field appears as shown below:
+
+
+
+Here's how the "Dropdown/Select" field works:
+
+- If no default value is set, the field will not auto-sign. The signer must click on the field and select an option from the dropdown list to sign it.
+- After signing, the field displays the selected value, similar to a signed text field.
+- If the field is marked as required, signers must select a value before completing the signing process.
+- If the field is marked as read-only, signers can view the selected value but cannot modify it.
+
+### Checkbox Field
+
+The last field introduced is the "Checkbox" field, which allows signers to select multiple options from a pre-defined list. This field is helpful for scenarios where signers need to choose multiple items or agree to several terms and conditions, for example.
+
+Before sending the document for signing, you must add at least one checkbox option. This option can contain a string or an empty value and can be checked or unchecked. Unlike the "Radio" field, the "Checkbox" field can have multiple checked options.
+
+Like other fields, you can mark the "Checkbox" as _required_ or _read-only_. In addition to that, it also has a validation field, and you can specify how many checkboxes the signer should sign:
+
+- Select at least X _(a number from 1 to 10)_
+- Select at most X _(a number from 1 to 10)_
+- Select exactly X _(a number from 1 to 10)_
+
+
+
+When a signer receives the document, they will see the "Checkbox" field as shown below:
+
+
+
+The image illustrates both field states - signed and un-signed. In this example, the 'Checkbox' field has two options checked by default, so it auto-signs.
+
+The field marked '1' appears when the signer visits the page for the first time or when the user refreshes the page and no option is selected. The field marked '2' displays the cleared state, where all choices have been deselected. This shows how the field looks when a user clears all selections.
+
+In this example, no validation rule has been set, allowing the signer to select any options. However, when a validation rule is applied, signers must meet the specified criteria to complete the signing process.
+
+## Development Challenges
+
+The introduction of these new fields wasn't without its challenges. The main challenges were:
+
+- Deciding how to store the new information for the fields in the database
+- Differentiation of recipients using colours
+- Storing the advanced settings for the local fields on the frontend
+- Implementing the Checkbox and Radio fields
+
+### 1st Challenge: Store New Field Information
+
+The first challenge was deciding how to store the extra information for each new field in the database. Each field has unique properties, with only `required` and `read-only` shared by all the advanced fields.
+
+The existing `Field` model in the database looks like this:
+
+```js
+model Field {
+ id Int @id @default(autoincrement())
+ secondaryId String @unique @default(cuid())
+ documentId Int?
+ templateId Int?
+ recipientId Int
+ type FieldType
+ page Int
+ positionX Decimal @default(0)
+ positionY Decimal @default(0)
+ width Decimal @default(-1)
+ height Decimal @default(-1)
+ customText String
+ inserted Boolean
+ Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
+ Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
+ Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
+ Signature Signature?
+
+ @@index([documentId])
+ @@index([templateId])
+ @@index([recipientId])
+}
+```
+
+Initially, we considered creating a new `FieldMeta` table with columns for each field property. However, this approach has 2 issues.
+
+First, the advanced fields only share two common properties: `required` and `read-only`. Since all the other properties are unique to each field type, this would result in many nullable columns in the `FieldMeta` model.
+
+Secondly, creating a new database table with columns for each field property and the associated relationships would increase the database complexity.
+
+As a result, we decided to look for another solution that would better work with our use case.
+
+### Solution: JSONB Field
+
+Since the advanced settings data is unique to each field, we decided to store it as JSON using PostgreSQL's `JSONB` data type. We added a new optional `fieldMeta` property of type `JSONB` to the Field model:
+
+```js
+model Field {
+ id Int @id @default(autoincrement())
+ secondaryId String @unique @default(cuid())
+ documentId Int?
+ templateId Int?
+ recipientId Int
+ type FieldType
+ page Int
+ positionX Decimal @default(0)
+ positionY Decimal @default(0)
+ width Decimal @default(-1)
+ height Decimal @default(-1)
+ customText String
+ inserted Boolean
+ Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
+ Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
+ Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
+ Signature Signature?
+ fieldMeta Json? <<<<<----- added this
+
+ @@index([documentId])
+ @@index([templateId])
+ @@index([recipientId])
+}
+```
+
+This approach allows us to store each field's settings as a JSON object. We use Zod schemas to parse and validate the field metadata when reading from or writing to the database to ensure data integrity.
+
+This approach has several benefits:
+
+- **Consistency**: The application uses the same Zod schema to retrieve and insert data into the database. That means the data is consistent throughout the app.
+- **Type safety**: By parsing the data with Zod, we can guarantee that the data matches the expected types and structure. We can also use Zod's `infer` utility to enable strong typing and autocompletion.
+- **Better error handling**: Zod provides thorough error messages indicating which part of the data is invalid. That makes it easier & faster to debug and fix issues.
+- **Maintainability**: Reusing the same Zod schema for retrieving and inserting data into the database makes the data structure easier to maintain.
+
+However, using `JSONB` also has drawbacks like data querying. Since the data is stored as JSON (more specifically, in binary format), complex queries can be less efficient compared to querying normalized relational data. On top of that, querying data requires specific operators and functions, such as `->`, `->>`, `@>`, and `?`. This makes the querying more verbose and less intuitive, and hence, it requires more finesse.
+
+Another drawback is the storage overhead. `JSONB` data is stored in a binary format, which can result in some storage overhead compared to normalized relational data. In cases where the JSON data is large or contains a lot of redundant information, the storage overhead can be significant.
+
+Despite these drawbacks, the `JSONB` type suits our use case, as the field meta information is relatively small and doesn't require complex querying. The flexibility of `JSONB` matches the dynamic nature of the fieldMeta field.
+
+> Postgres provides 2 fields for storing JSON data — `json` and `jsonb`. For more information, you can [check out the documentation](https://www.postgresql.org/docs/current/datatype-json.html).
+
+### 2nd Challenge: Storing Fields' Advanced Settings on Frontend
+
+The next challenge was finding the best way to store the advanced field settings entered by users.
+
+Currently, the app only saves the fields and associated settings to the database when the user moves to the next step.
+
+
+
+The fields are stored locally until the user proceeds to the next step. This means all fields and their settings are lost when the user:
+
+- Closes the advanced settings tab
+- Refreshes the page
+- Closes the tab
+- Navigates to the previous step
+
+In the future, we plan to improve this flow and save the fields on blur, preserving user data even if they navigate away. However, until then, we needed a solution to save the advanced settings when the user closes the settings tab.
+
+### Solution: Local Storage
+
+Our temporary solution is to store the advanced settings in local storage, as the fields are only available locally. If the fields were saved in the database, we could store the advanced settings alongside them.
+
+
+
+Since the fields are not saved in the database, we must persist the data until the user moves to the next step, at which point the data is saved to the database. Storing the data in local storage allows users to open, close, and configure various fields in the advanced settings tab without losing information.
+
+When the user proceeds to the next step, the fields and their advanced settings are saved into the database, and the local storage is cleared.
+
+We also recognized the dangers of saving data to local storage, as users could modify it and break the application. As a result, we have implemented extensive checks on both the backend and frontend, in addition to parsing and validating data with Zod.
+
+However, this solution has limitations. The data is still lost when the user:
+
+- Refreshes the page
+- Navigates to the previous step
+- Closes the browser
+
+In these cases, the fields are wiped from the document. A future improvement to save fields to the database on blur will solve this issue.
+
+### 3rd Challenge: Radio and Checkbox Fields
+
+Implementing the Radio and Checkbox fields was challenging from both logical and design perspectives. Both fields can contain empty and non-empty values, and the Checkbox field allows users to select multiple empty/non-empty values.
+
+
+
+The image above shows the Radio and Checkbox fields in the document editor. The Radio field on the left-hand side has 4 options, 1 of which is checked. The Checkbox field on the right-hand side has 4 options, 2 of which are checked.
+
+The Radio field was easier to implement because users can only select one option, resulting in simpler logic. The signer clicks on an option to choose it, and the field auto-signs with that value. To change the selection, the user clicks another option, un-signing the field and re-signing it with the new value.
+
+The Checkbox field was more challenging because:
+
+- Signers can select multiple options simultaneously, resulting in the field containing multiple values.
+- It can have validation rules (e.g., selecting at least, at most, or exactly X options).
+- Users can check/uncheck options by clicking them or clear the field with a button.
+
+These factors make the Checkbox field more complex and challenging to implement correctly.
+
+### Solution
+
+Instead of focusing on a specific solution, we'll discuss the general implementation and its most challenging aspects. I'll include a link to the complete implementation for each field so you can check it out.
+
+**Radio Field**
+
+The way signing works for the Radio field is to pull the data from the database and display the available options. If the field has a default value set by the document sender, it auto-signs with that value.
+
+```ts
+...
+ const values = parsedFieldMeta.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+...
+ const shouldAutoSignField =
+ (!field.inserted && selectedOption) ||
+ (!field.inserted && defaultValue) ||
+ (!field.inserted && parsedFieldMeta.readOnly && defaultValue);
+...
+
+ useEffect(() => {
+ if (shouldAutoSignField) {
+ void executeActionAuthProcedure({
+ onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
+ actionTarget: field.type,
+ });
+ }
+ }, [selectedOption, field]);
+```
+
+> You can see the complete implementation of the radio field in the [radio-field.tsx]() file.
+
+If the field is not read-only and the user clicks on another option, the field un-signs and re-signs with the new value. Read-only fields cannot be modified.
+
+The value is saved in the database whenever the field is signed, whether by auto-signing or user. Similarly, the value is removed from the database when the field is unsigned.
+
+Since the Radio field can contain empty values, we map over the values and replace the empty ones with a unique string `empty-value-${item.id}`. This is because the empty string is not a valid value for the field, and we need to differentiate between empty and non-empty values.
+
+**Checkbox Field**
+
+The Checkbox field implementation is similar to the Radio field, with the main differences being:
+
+- Checkbox fields can contain multiple values.
+- Checkbox fields have validation rules that need to be enforced.
+
+```ts
+...
+ const values = parsedFieldMeta.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+
+ const [checkedValues, setCheckedValues] = useState(
+ values
+ ?.map((item) =>
+ item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
+ )
+ .filter(Boolean) || [],
+ );
+...
+```
+
+As with the Radio field, we map over the values and replace empty ones with a unique string. We also keep track of the checked values to display the field correctly and validate them against the validation rules.
+
+```ts
+...
+ const values = parsedFieldMeta.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+
+ const [checkedValues, setCheckedValues] = useState(
+ values
+ ?.map((item) =>
+ item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
+ )
+ .filter(Boolean) || [],
+ );
+
+ const checkboxValidationRule = parsedFieldMeta.validationRule;
+ const checkboxValidationLength = parsedFieldMeta.validationLength;
+ const validationSign = checkboxValidationSigns.find(
+ (sign) => sign.label === checkboxValidationRule,
+ );
+...
+```
+
+Then, we retrieve the validation rule and length from the database and find the corresponding validation sign (e.g., ">=", "=", "\<=") based on the rule label. The `checkboxValidationSigns` array maps rule labels to their corresponding signs.
+
+```ts
+export const checkboxValidationSigns = [
+ {
+ label: 'Select at least',
+ value: '>=',
+ },
+ {
+ label: 'Select exactly',
+ value: '=',
+ },
+ {
+ label: 'Select at most',
+ value: '<=',
+ },
+];
+```
+
+We then check if the length condition is met based on the validation rule, sign, and length. If met, the user can proceed with signing the field. Otherwise, they need to select the correct number of options.
+
+```ts
+...
+ const values = parsedFieldMeta.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+
+ const [checkedValues, setCheckedValues] = useState(
+ values
+ ?.map((item) =>
+ item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
+ )
+ .filter(Boolean) || [],
+ );
+
+ const checkboxValidationRule = parsedFieldMeta.validationRule;
+ const checkboxValidationLength = parsedFieldMeta.validationLength;
+ const validationSign = checkboxValidationSigns.find(
+ (sign) => sign.label === checkboxValidationRule,
+ );
+
+ const isLengthConditionMet = useMemo(() => {
+ if (!validationSign) return true;
+ return (
+ (validationSign.value === '>=' && checkedValues.length >= (checkboxValidationLength || 0)) ||
+ (validationSign.value === '=' && checkedValues.length === (checkboxValidationLength || 0)) ||
+ (validationSign.value === '<=' && checkedValues.length <= (checkboxValidationLength || 0))
+ );
+ }, [checkedValues, validationSign, checkboxValidationLength]);
+...
+```
+
+In summary, the Checkbox field allows signers to select multiple options, with the field automatically signing based on these selections. Signers can un-sign the field by deselecting options or clearing all selections. The system enforces validation rules throughout this process, ensuring signers select the required number of options to sign the field successfully.
+
+> You can see the complete implementation of the checkbox field in the [checkbox-field.tsx]() file.
+
+### 4th Challenge: Recipients' Colors
+
+Another challenge we faced was using colours to differentiate recipients. We needed to dynamically generate and reuse the same Tailwind classes across several components. However, TailwindCSS only includes the CSS classes used in the project, discarding unused ones from the final build. This resulted in colours not being applied to the components, as the classes were not used in the code.
+
+The images below illustrate the recipients' colours in 2 different states.
+
+In the first image, the "Signature" field on the right is in the active state (blue), triggered when the user clicks the field to drag it onto the document. The signature field on the left, placed on the document, is in the normal state.
+
+The first image illustrates the "Signature" field in the active state, triggered when the user clicks on it.
+
+
+
+The second image shows the "Signature" field in the normal state.
+
+
+
+The document editor consists of various components (fields, recipients, etc.), meaning the same colours and code are reused across multiple components.
+
+```ts
+export const combinedStyles = {
+ 'orange-500': {
+ ringColor: 'ring-orange-500/30 ring-offset-orange-500',
+ borderWithHover: 'border-orange-500 hover:border-orange-500',
+ ...,
+ },
+ 'green-500': {
+ ringColor: 'ring-green-500/30 ring-offset-green-500',
+ borderWithHover: 'border-green-500 hover:border-green-500',
+ ...,
+ },
+ 'blue-500': {
+ ringColor: 'ring-blue-500/30 ring-offset-blue-500',
+ borderWithHover: 'border-blue-500 hover:border-blue-500',
+ ...,
+ 'gray-500': {
+ ringColor: 'ring-gray-500/30 ring-offset-gray-500',
+ borderWithHover: 'border-gray-500 hover:border-gray-500',
+ ...,
+ },
+ ...,
+};
+
+export const MyComponent = () => {
+ const selectedSignerStyles = useSelectedSignerStyles(selectedSigner, combinedStyles);
+
+ return (
+
+
Hello
+
+ );
+};
+```
+
+The code above shows a naive solution using a `combinedStyles` object containing TailwindCSS classes for various component styles (ring, border, hover, etc.).
+
+Components would use custom hooks to apply appropriate styles based on the selected recipient. For example, recipient 1 would use `green-500` styles, turning all related elements green.
+
+
+
+The problem with this approach is that we can't import the `combinedStyles` object into other components because TailwindCSS will remove the unused classes. That means we had to copy and paste the same object into multiple files. As a result, it pollutes the codebase with duplicated code, which makes it harder to maintain and scale the code. As the application grows, the `combinedStyles` object will become larger and more complex. Moreover, it's not very flexible, as it doesn't allow for easy customization of the colours.
+
+While this approach works, there is a more efficient and scalable solution.
+
+### Solution: Modularise the Logic and Use CSS Variables
+
+To address the challenge of reusing colours across components, we moved the colours and associated hooks to a separate file, defining styles only in this file and accessing them from components through custom hooks.
+
+```ts
+export const SIGNER_COLOR_STYLES = {
+ green: {
+ default: {
+ background: 'bg-[hsl(var(--signer-green))]',
+ base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]',
+ fieldItem:
+ 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10',
+ fieldItemInitials:
+ 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]',
+ comboxBoxItem: 'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
+ },
+ },
+
+ ...
+};
+
+export type CombinedStylesKey = keyof typeof SIGNER_COLOR_STYLES;
+
+export const AVAILABLE_SIGNER_COLORS = [
+ 'green',
+ 'blue',
+ 'purple',
+ 'orange',
+ 'yellow',
+ 'pink',
+] as const satisfies CombinedStylesKey[];
+
+export const useSignerColors = (index: number) => {
+ const key = AVAILABLE_SIGNER_COLORS[index % AVAILABLE_SIGNER_COLORS.length];
+
+ return SIGNER_COLOR_STYLES[key];
+};
+
+export const getSignerColorStyles = (index: number) => {
+ return useSignerColors(index);
+};
+```
+
+> The file was truncated for readability. You can see the complete code in the [signer-colors.ts](https://github.com/documenso/documenso/blob/main/packages/ui/lib/signer-colors.ts) file from the Documenso repository.
+
+The `SIGNER_COLOR_STYLES` object contains the styles for each colour, such as the background, border, and hover colours. Based on the signer's index, the `useSignerColors` hook gets the styles for a specific colour. The `getSignerColorStyles` function is a helper function that returns the styles for a particular signer.
+
+Now, the components can access the colours and styles using custom hooks. For example, to get the styles for a specific signer, the component can call the `useSignerColors` hook with the signer's index.
+
+```ts
+const signerStyles = useSignerColors(recipientIndex);
+```
+
+The hook will return the styles for that signer, which can then be applied to the component. For example, you can access the signer's background colour using `signerStyles.default.background`.
+
+This approach makes managing the colours and styles easier, as they are defined in a single file. Changing or adding colours can be done in one place, making the code more modular and reusable.
+
+We also opted for CSS variables to define colours, allowing more flexibility and consistency in styling. A single CSS variable for each colour can cover a wide range of states without relying on multiple TailwindCSS classes. For example, you can easily set the opacity and lightness of colour without using multiple classes. CSS variables help align colours with our brand guidelines while simplifying the overall styling process.
+
+## The End
+
+We're happy to see the new advanced fields released because they offer our users more flexibility, variety, and customization options. Implementing the new fields came with its challenges, but we overcame them and learned from them. We're excited to continue enhancing Documenso and providing our users with the best document signing experience.
diff --git a/apps/marketing/content/changelog.mdx b/apps/marketing/content/changelog.mdx
index dd261529c..9116b676c 100644
--- a/apps/marketing/content/changelog.mdx
+++ b/apps/marketing/content/changelog.mdx
@@ -8,7 +8,64 @@ Check out what's new in the latest version and read our thoughts on it. For more
---
-## v1.6.0: Enhancing Team Collaboration and User Experience (latest)
+# 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:
+
+## 🌟 Key Features
+
+### New Initials Field Type
+
+We've added a new field type for initials, giving you more options for document customization. This feature allows signers to quickly initial documents, adding an extra layer of verification to your signing process.
+
+### Internationalization Support
+
+We've taken a big step towards making Documenso accessible to a global audience by adding i18n (internationalization) support for our marketing pages and adding translations to support multiple languages.
+
+While this is just a small step towards a fully multilingual Documenso, it's a significant step towards making our platform more accessible to a global audience.
+
+Using our new knowledge and findings from the marketing implementation, we aim to tackle our web application in the near future for a fully global Documenso.
+
+### Enhanced OpenID Connect (OIDC) Integration
+
+For our self-hosted users leveraging OIDC for authentication:
+
+- Now supports OIDC-only signup
+- Added trust for email addresses from OIDC providers
+- The OIDC sign-in button text is now configurable
+
+## 🔧 Other Improvements
+
+- **UI Enhancements**:
+
+ - Fixed display issues with field names/labels in dark mode
+ - Improved truncation of titles to prevent UI breaks
+
+- **User Experience**:
+
+ - The signup option is now shown only to users without existing accounts
+ - Fixed issues with radio and checkbox fields having empty values
+
+- **API and Security**:
+
+ - Fixed a bug in the date format API
+ - Improved URL parsing for enhanced security
+ - Added support for dynamic external IDs for direct templates
+
+- **Document Management**:
+ - Resolved an issue with downloading audit log certificates
+
+We've also made various other minor fixes and improvements to ensure a smoother Documenso experience.
+
+## 👏 Community Contributions
+
+A big thank you to our growing community! This release includes contributions from several new contributors, showcasing the power of open-source collaboration.
+
+We appreciate your continued support and feedback as we work to make Documenso the best document signing solution available. Enjoy the new features and improvements in v1.6.1!
+
+---
+
+## v1.6.0: Enhancing Team Collaboration and User Experience
### Released 23th July 2024
diff --git a/apps/marketing/package.json b/apps/marketing/package.json
index b11e39b08..fb6478fca 100644
--- a/apps/marketing/package.json
+++ b/apps/marketing/package.json
@@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@@ -64,4 +64,4 @@
"next": "$next"
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/marketing/public/blog/advanced-fields/checkbox-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/checkbox-field-advanced-settings.webp
new file mode 100644
index 000000000..ad0d45914
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/checkbox-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/checkbox-sign-page.webp b/apps/marketing/public/blog/advanced-fields/checkbox-sign-page.webp
new file mode 100644
index 000000000..45dcd8dba
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/checkbox-sign-page.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/documenso-advanced-fields-signing.webp b/apps/marketing/public/blog/advanced-fields/documenso-advanced-fields-signing.webp
new file mode 100644
index 000000000..2c5353d53
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/documenso-advanced-fields-signing.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/documenso-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/documenso-field-advanced-settings.webp
new file mode 100644
index 000000000..b3f492703
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/documenso-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/field-active-state.webp b/apps/marketing/public/blog/advanced-fields/field-active-state.webp
new file mode 100644
index 000000000..1d86d9e40
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/field-active-state.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/first-text-field-input.webp b/apps/marketing/public/blog/advanced-fields/first-text-field-input.webp
new file mode 100644
index 000000000..d0fd9d344
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/first-text-field-input.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/number-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/number-field-advanced-settings.webp
new file mode 100644
index 000000000..79b68d9ae
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/number-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/old-text-field.jpeg b/apps/marketing/public/blog/advanced-fields/old-text-field.jpeg
new file mode 100644
index 000000000..557c5cdf5
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/old-text-field.jpeg differ
diff --git a/apps/marketing/public/blog/advanced-fields/radio-and-checkbox-fields.webp b/apps/marketing/public/blog/advanced-fields/radio-and-checkbox-fields.webp
new file mode 100644
index 000000000..11ec94c7b
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/radio-and-checkbox-fields.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/radio-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/radio-field-advanced-settings.webp
new file mode 100644
index 000000000..21181956b
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/radio-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/radio-field-sign-page.webp b/apps/marketing/public/blog/advanced-fields/radio-field-sign-page.webp
new file mode 100644
index 000000000..0a917a2d2
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/radio-field-sign-page.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/recipient-colour-example.webp b/apps/marketing/public/blog/advanced-fields/recipient-colour-example.webp
new file mode 100644
index 000000000..5f49a5465
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/recipient-colour-example.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/recipient-colours.webp b/apps/marketing/public/blog/advanced-fields/recipient-colours.webp
new file mode 100644
index 000000000..af8c6303f
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/recipient-colours.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/second-text-field-input.webp b/apps/marketing/public/blog/advanced-fields/second-text-field-input.webp
new file mode 100644
index 000000000..e5cca0da2
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/second-text-field-input.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/select-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/select-field-advanced-settings.webp
new file mode 100644
index 000000000..f8940dc77
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/select-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/select-field-sign-page.webp b/apps/marketing/public/blog/advanced-fields/select-field-sign-page.webp
new file mode 100644
index 000000000..a900226ae
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/select-field-sign-page.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/text-field-advanced-settings.webp b/apps/marketing/public/blog/advanced-fields/text-field-advanced-settings.webp
new file mode 100644
index 000000000..4ac2232d5
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/text-field-advanced-settings.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/text-field-signing.webp b/apps/marketing/public/blog/advanced-fields/text-field-signing.webp
new file mode 100644
index 000000000..c219686fd
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/text-field-signing.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/third-text-field-input.webp b/apps/marketing/public/blog/advanced-fields/third-text-field-input.webp
new file mode 100644
index 000000000..6289650e7
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/third-text-field-input.webp differ
diff --git a/apps/marketing/public/blog/advanced-fields/unsigned-text-fields.webp b/apps/marketing/public/blog/advanced-fields/unsigned-text-fields.webp
new file mode 100644
index 000000000..9dac2ecba
Binary files /dev/null and b/apps/marketing/public/blog/advanced-fields/unsigned-text-fields.webp differ
diff --git a/apps/marketing/src/app/(marketing)/blog/page.tsx b/apps/marketing/src/app/(marketing)/blog/page.tsx
index 39f3bbd74..4974a2399 100644
--- a/apps/marketing/src/app/(marketing)/blog/page.tsx
+++ b/apps/marketing/src/app/(marketing)/blog/page.tsx
@@ -42,7 +42,7 @@ export default function BlogPage() {
>
+
+ event('view-demo')}
+ >
+ Book a Demo
+
+ Want to learn more about Documenso and how it works? Book a demo today! Our founders
+ will walk you through the application and answer any questions you may have regarding
+ usage, integration, and more.
+
+
{
const { toast } = useToast();
const { getFlag } = useFeatureFlags();
@@ -369,7 +371,7 @@ export const SignInForm = ({
onClick={onSignInWithOIDCClick}
>
- OIDC
+ {oidcProviderLabel || 'OIDC'}
)}
diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx
index b80c6fea8..86f6eef56 100644
--- a/apps/web/src/components/partials/not-found.tsx
+++ b/apps/web/src/components/partials/not-found.tsx
@@ -29,6 +29,10 @@ export default function NotFoundPartial({ children }: NotFoundPartialProps) {
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
+ style={{
+ mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
+ WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
+ }}
priority
/>
diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts
index 31f6e9ea3..811ddbda0 100644
--- a/apps/web/src/pages/api/auth/[...nextauth].ts
+++ b/apps/web/src/pages/api/auth/[...nextauth].ts
@@ -6,6 +6,7 @@ import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-cu
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
+import { slugify } from '@documenso/lib/utils/slugify';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
@@ -60,13 +61,41 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
},
});
},
- linkAccount: async ({ user }) => {
+ linkAccount: async ({ user, account, profile }) => {
const userId = typeof user.id === 'string' ? parseInt(user.id) : user.id;
- if (isNaN(userId)) {
+ if (Number.isNaN(userId)) {
return;
}
+ // If the user is linking an OIDC account and the email verified date is set then update it in the db.
+ if (account.provider === 'oidc' && profile.emailVerified !== null) {
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ emailVerified: profile.emailVerified,
+ },
+ });
+ }
+
+ // auto set public profile name
+ if (account.provider === 'oidc' && user.name && 'url' in user && !user.url) {
+ let counter = 1;
+ let url = slugify(user.name);
+
+ while (await prisma.user.findFirst({ where: { url } })) {
+ url = `${slugify(user.name)}-${counter}`;
+ counter++;
+ }
+
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ url,
+ },
+ });
+ }
+
await prisma.userSecurityAuditLog.create({
data: {
userId,
diff --git a/package-lock.json b/package-lock.json
index d4f8b058e..c64912062 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"workspaces": [
"apps/*",
"packages/*"
@@ -80,7 +80,7 @@
},
"apps/marketing": {
"name": "@documenso/marketing",
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/assets": "*",
@@ -424,7 +424,7 @@
},
"apps/web": {
"name": "@documenso/web",
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",
diff --git a/package.json b/package.json
index 3bbdac637..2c362c513 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "1.6.0",
+ "version": "1.6.1-rc.1",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",
@@ -80,4 +80,4 @@
"trigger.dev": {
"endpointId": "documenso-app"
}
-}
\ No newline at end of file
+}
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index 6bcd767e9..74afa80c0 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -2,6 +2,9 @@ import { createNextRoute } from '@ts-rest/next';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
+import '@documenso/lib/constants/time-zones';
+import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
@@ -222,6 +225,36 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}
+ const dateFormat = body.meta.dateFormat
+ ? DATE_FORMATS.find((format) => format.label === body.meta.dateFormat)
+ : DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
+ const timezone = body.meta.timezone
+ ? TIME_ZONES.find((tz) => tz === body.meta.timezone)
+ : DEFAULT_DOCUMENT_TIME_ZONE;
+
+ const isDateFormatValid = body.meta.dateFormat
+ ? DATE_FORMATS.some((format) => format.label === dateFormat?.label)
+ : true;
+ const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
+
+ if (!isDateFormatValid) {
+ return {
+ status: 400,
+ body: {
+ message: 'Invalid date format. Please provide a valid date format',
+ },
+ };
+ }
+
+ if (!isTimeZoneValid) {
+ return {
+ status: 400,
+ body: {
+ message: 'Invalid timezone. Please provide a valid timezone',
+ },
+ };
+ }
+
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
@@ -244,7 +277,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
- ...body.meta,
+ subject: body.meta.subject,
+ message: body.meta.message,
+ timezone,
+ dateFormat: dateFormat?.value,
+ redirectUrl: body.meta.redirectUrl,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts
index dddc0a0e1..90d3f65bf 100644
--- a/packages/api/v1/schema.ts
+++ b/packages/api/v1/schema.ts
@@ -1,5 +1,9 @@
+import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import { z } from 'zod';
+import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
+import '@documenso/lib/constants/time-zones';
+import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZUrlSchema } from '@documenso/lib/schemas/common';
import {
DocumentDataType,
@@ -11,6 +15,8 @@ import {
TemplateType,
} from '@documenso/prisma/client';
+extendZodWithOpenApi(z);
+
export const ZNoBodyMutationSchema = null;
/**
@@ -97,8 +103,19 @@ export const ZCreateDocumentMutationSchema = z.object({
.object({
subject: z.string(),
message: z.string(),
- timezone: z.string(),
- dateFormat: z.string(),
+ timezone: z.string().default(DEFAULT_DOCUMENT_TIME_ZONE).openapi({
+ description:
+ 'The timezone of the date. Must be one of the options listed in the list below.',
+ enum: TIME_ZONES,
+ }),
+ dateFormat: z
+ .string()
+ .default(DEFAULT_DOCUMENT_DATE_FORMAT)
+ .openapi({
+ description:
+ 'The format of the date. Must be one of the options listed in the list below.',
+ enum: DATE_FORMATS.map((format) => format.value),
+ }),
redirectUrl: z.string(),
})
.partial(),
diff --git a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts
index c35ad759d..c2e6883da 100644
--- a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts
+++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts
@@ -449,14 +449,18 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
await page.waitForURL('https://documenso.com');
- // Check if document has been signed
- const { status: completedStatus } = await getDocumentByToken(token);
- expect(completedStatus).toBe(DocumentStatus.COMPLETED);
+ await expect(async () => {
+ // Check if document has been signed
+ const { status: completedStatus } = await getDocumentByToken(token);
+
+ expect(completedStatus).toBe(DocumentStatus.COMPLETED);
+ }).toPass();
});
test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', async ({ page }) => {
const user = await seedUser();
- const customDate = DateTime.utc().toFormat('yyyy-MM-dd hh:mm a');
+
+ const now = DateTime.utc();
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: user,
@@ -488,9 +492,14 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
},
});
- expect(field?.customText).toBe(customDate);
+ const insertedDate = DateTime.fromFormat(field?.customText ?? '', 'yyyy-MM-dd hh:mm a');
- // Check if document has been signed
- const { status: completedStatus } = await getDocumentByToken(token);
- expect(completedStatus).toBe(DocumentStatus.COMPLETED);
+ expect(Math.abs(insertedDate.diff(now).minutes)).toBeLessThanOrEqual(1);
+
+ await expect(async () => {
+ // Check if document has been signed
+ const { status: completedStatus } = await getDocumentByToken(token);
+
+ expect(completedStatus).toBe(DocumentStatus.COMPLETED);
+ }).toPass();
});
diff --git a/packages/app-tests/e2e/templates/direct-templates.spec.ts b/packages/app-tests/e2e/templates/direct-templates.spec.ts
index 5b822dbcf..bffd89266 100644
--- a/packages/app-tests/e2e/templates/direct-templates.spec.ts
+++ b/packages/app-tests/e2e/templates/direct-templates.spec.ts
@@ -237,8 +237,10 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
for (const template of [personalDirectTemplate, teamDirectTemplate]) {
await page.goto(`${WEBAPP_BASE_URL}${formatDocumentsPath(template.team?.url)}`);
- // Check that the document is in the 'All' tab.
- await checkDocumentTabCount(page, 'Completed', 1);
+ await expect(async () => {
+ // Check that the document is in the 'All' tab.
+ await checkDocumentTabCount(page, 'Completed', 1);
+ }).toPass();
}
});
diff --git a/packages/ee/server-only/limits/provider/client.tsx b/packages/ee/server-only/limits/provider/client.tsx
index fdc00b439..624b61d98 100644
--- a/packages/ee/server-only/limits/provider/client.tsx
+++ b/packages/ee/server-only/limits/provider/client.tsx
@@ -1,6 +1,6 @@
'use client';
-import { createContext, useContext, useEffect, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { equals } from 'remeda';
@@ -8,7 +8,7 @@ import { getLimits } from '../client';
import { FREE_PLAN_LIMITS } from '../constants';
import type { TLimitsResponseSchema } from '../schema';
-export type LimitsContextValue = TLimitsResponseSchema;
+export type LimitsContextValue = TLimitsResponseSchema & { refreshLimits: () => Promise };
const LimitsContext = createContext(null);
@@ -23,7 +23,7 @@ export const useLimits = () => {
};
export type LimitsProviderProps = {
- initialValue?: LimitsContextValue;
+ initialValue?: TLimitsResponseSchema;
teamId?: number;
children?: React.ReactNode;
};
@@ -38,7 +38,7 @@ export const LimitsProvider = ({
}: LimitsProviderProps) => {
const [limits, setLimits] = useState(() => initialValue);
- const refreshLimits = async () => {
+ const refreshLimits = useCallback(async () => {
const newLimits = await getLimits({ teamId });
setLimits((oldLimits) => {
@@ -48,11 +48,11 @@ export const LimitsProvider = ({
return newLimits;
});
- };
+ }, [teamId]);
useEffect(() => {
void refreshLimits();
- }, []);
+ }, [refreshLimits]);
useEffect(() => {
const onFocus = () => {
@@ -64,7 +64,16 @@ export const LimitsProvider = ({
return () => {
window.removeEventListener('focus', onFocus);
};
- }, []);
+ }, [refreshLimits]);
- return {children};
+ return (
+
+ {children}
+
+ );
};
diff --git a/packages/ee/server-only/limits/provider/server.tsx b/packages/ee/server-only/limits/provider/server.tsx
index b7cde3573..969361060 100644
--- a/packages/ee/server-only/limits/provider/server.tsx
+++ b/packages/ee/server-only/limits/provider/server.tsx
@@ -3,7 +3,6 @@
import { headers } from 'next/headers';
import { getLimits } from '../client';
-import type { LimitsContextValue } from './client';
import { LimitsProvider as ClientLimitsProvider } from './client';
export type LimitsProviderProps = {
@@ -14,7 +13,7 @@ export type LimitsProviderProps = {
export const LimitsProvider = async ({ children, teamId }: LimitsProviderProps) => {
const requestHeaders = Object.fromEntries(headers().entries());
- const limits: LimitsContextValue = await getLimits({ headers: requestHeaders, teamId });
+ const limits = await getLimits({ headers: requestHeaders, teamId });
return (
diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx
index b99b1a1b4..644addb71 100644
--- a/packages/email/template-components/template-document-invite.tsx
+++ b/packages/email/template-components/template-document-invite.tsx
@@ -12,6 +12,8 @@ export interface TemplateDocumentInviteProps {
assetBaseUrl: string;
role: RecipientRole;
selfSigner: boolean;
+ isTeamInvite: boolean;
+ teamName?: string;
}
export const TemplateDocumentInvite = ({
@@ -21,6 +23,8 @@ export const TemplateDocumentInvite = ({
assetBaseUrl,
role,
selfSigner,
+ isTeamInvite,
+ teamName,
}: TemplateDocumentInviteProps) => {
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
@@ -36,6 +40,12 @@ export const TemplateDocumentInvite = ({
{`"${documentName}"`}
>
+ ) : isTeamInvite ? (
+ <>
+ {`${inviterName} on behalf of ${teamName} has invited you to ${actionVerb.toLowerCase()}`}
+
+ {`"${documentName}"`}
+ >
) : (
<>
{`${inviterName} has invited you to ${actionVerb.toLowerCase()}`}
diff --git a/packages/email/templates/confirm-team-email.tsx b/packages/email/templates/confirm-team-email.tsx
index 5752f806d..552a079f8 100644
--- a/packages/email/templates/confirm-team-email.tsx
+++ b/packages/email/templates/confirm-team-email.tsx
@@ -91,6 +91,9 @@ export const ConfirmTeamEmailTemplate = ({
Allow document recipients to reply directly to this email address
+
+ Send documents on behalf of the team using the email address
+
diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx
index 52a40d804..7e845126b 100644
--- a/packages/email/templates/document-invite.tsx
+++ b/packages/email/templates/document-invite.tsx
@@ -23,6 +23,9 @@ export type DocumentInviteEmailTemplateProps = Partial {
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
const previewText = selfSigner
? `Please ${action} your document ${documentName}`
+ : isTeamInvite
+ ? `${inviterName} on behalf of ${teamName} has invited you to ${action} ${documentName}`
: `${inviterName} has invited you to ${action} ${documentName}`;
const getAssetUrl = (path: string) => {
@@ -76,6 +83,8 @@ export const DocumentInviteEmailTemplate = ({
assetBaseUrl={assetBaseUrl}
role={role}
selfSigner={selfSigner}
+ isTeamInvite={isTeamInvite}
+ teamName={teamName}
/>
diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts
index 4df19b407..a8b5f31a2 100644
--- a/packages/lib/constants/auth.ts
+++ b/packages/lib/constants/auth.ts
@@ -18,6 +18,8 @@ export const IS_OIDC_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
);
+export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL;
+
export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: string } = {
[UserSecurityAuditLogType.ACCOUNT_SSO_LINK]: 'Linked account to SSO',
[UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE]: 'Profile updated',
diff --git a/packages/lib/constants/url-regex.ts b/packages/lib/constants/url-regex.ts
deleted file mode 100644
index 1dfb70ad3..000000000
--- a/packages/lib/constants/url-regex.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const URL_REGEX =
- /^(https?):\/\/(?:www\.)?(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
diff --git a/packages/lib/jobs/client.ts b/packages/lib/jobs/client.ts
index 9899cc791..1e3717927 100644
--- a/packages/lib/jobs/client.ts
+++ b/packages/lib/jobs/client.ts
@@ -4,6 +4,7 @@ import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-sig
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
+import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
/**
* The `as const` assertion is load bearing as it provides the correct level of type inference for
@@ -15,6 +16,7 @@ export const jobsClient = new JobClient([
SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
+ SEAL_DOCUMENT_JOB_DEFINITION,
] as const);
export const jobs = jobsClient;
diff --git a/packages/lib/jobs/client/inngest.ts b/packages/lib/jobs/client/inngest.ts
index 1841b3876..7ac81271e 100644
--- a/packages/lib/jobs/client/inngest.ts
+++ b/packages/lib/jobs/client/inngest.ts
@@ -26,8 +26,8 @@ export class InngestJobProvider extends BaseJobProvider {
static getInstance() {
if (!this._instance) {
const client = new InngestClient({
- id: 'documenso-app',
- eventKey: process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
+ id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app',
+ eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
});
this._instance = new InngestJobProvider({ client });
diff --git a/packages/lib/jobs/definitions/emails/send-signing-email.ts b/packages/lib/jobs/definitions/emails/send-signing-email.ts
index 0244df34f..43ad730c6 100644
--- a/packages/lib/jobs/definitions/emails/send-signing-email.ts
+++ b/packages/lib/jobs/definitions/emails/send-signing-email.ts
@@ -58,6 +58,12 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
},
include: {
documentMeta: true,
+ team: {
+ select: {
+ teamEmail: true,
+ name: true,
+ },
+ },
},
}),
prisma.recipient.findFirstOrThrow({
@@ -67,7 +73,7 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
}),
]);
- const { documentMeta } = document;
+ const { documentMeta, team } = document;
if (recipient.role === RecipientRole.CC) {
return;
@@ -75,6 +81,7 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
const customEmail = document?.documentMeta;
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
+ const isTeamDocument = document.teamId !== null;
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
@@ -96,6 +103,11 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
emailSubject = `Please ${recipientActionVerb} this document created by your direct template`;
}
+ if (isTeamDocument && team) {
+ emailSubject = `${team.name} invited you to ${recipientActionVerb} a document`;
+ emailMessage = `${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
+ }
+
const customEmailTemplate = {
'signer.name': name,
'signer.email': email,
@@ -108,12 +120,15 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
- inviterEmail: user.email,
+ inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
+ isTeamInvite: isTeamDocument,
+ teamName: team?.name,
+ teamEmail: team?.teamEmail?.email,
});
await io.runTask('send-signing-email', async () => {
diff --git a/packages/lib/jobs/definitions/internal/seal-document.ts b/packages/lib/jobs/definitions/internal/seal-document.ts
new file mode 100644
index 000000000..f37b390d1
--- /dev/null
+++ b/packages/lib/jobs/definitions/internal/seal-document.ts
@@ -0,0 +1,250 @@
+import { nanoid } from 'nanoid';
+import path from 'node:path';
+import { PDFDocument } from 'pdf-lib';
+import { z } from 'zod';
+
+import { prisma } from '@documenso/prisma';
+import {
+ DocumentStatus,
+ RecipientRole,
+ SigningStatus,
+ WebhookTriggerEvents,
+} from '@documenso/prisma/client';
+import { signPdf } from '@documenso/signing';
+
+import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
+import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
+import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
+import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
+import { flattenForm } from '../../../server-only/pdf/flatten-form';
+import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
+import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
+import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
+import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
+import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
+import { getFile } from '../../../universal/upload/get-file';
+import { putPdfFile } from '../../../universal/upload/put-file';
+import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
+import { type JobDefinition } from '../../client/_internal/job';
+
+const SEAL_DOCUMENT_JOB_DEFINITION_ID = 'internal.seal-document';
+
+const SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA = z.object({
+ documentId: z.number(),
+ sendEmail: z.boolean().optional(),
+ isResealing: z.boolean().optional(),
+ requestMetadata: ZRequestMetadataSchema.optional(),
+});
+
+export const SEAL_DOCUMENT_JOB_DEFINITION = {
+ id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
+ name: 'Seal Document',
+ version: '1.0.0',
+ trigger: {
+ name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
+ schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
+ },
+ handler: async ({ payload, io }) => {
+ const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
+
+ const document = await prisma.document.findFirstOrThrow({
+ where: {
+ id: documentId,
+ Recipient: {
+ every: {
+ signingStatus: SigningStatus.SIGNED,
+ },
+ },
+ },
+ include: {
+ Recipient: true,
+ },
+ });
+
+ // Seems silly but we need to do this in case the job is re-ran
+ // after it has already run through the update task further below.
+ // eslint-disable-next-line @typescript-eslint/require-await
+ const documentStatus = await io.runTask('get-document-status', async () => {
+ return document.status;
+ });
+
+ // This is the same case as above.
+ // eslint-disable-next-line @typescript-eslint/require-await
+ const documentDataId = await io.runTask('get-document-data-id', async () => {
+ return document.documentDataId;
+ });
+
+ const documentData = await prisma.documentData.findFirst({
+ where: {
+ id: documentDataId,
+ },
+ });
+
+ if (!documentData) {
+ throw new Error(`Document ${document.id} has no document data`);
+ }
+
+ const recipients = await prisma.recipient.findMany({
+ where: {
+ documentId: document.id,
+ role: {
+ not: RecipientRole.CC,
+ },
+ },
+ });
+
+ if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
+ throw new Error(`Document ${document.id} has unsigned recipients`);
+ }
+
+ const fields = await prisma.field.findMany({
+ where: {
+ documentId: document.id,
+ },
+ include: {
+ Signature: true,
+ },
+ });
+
+ if (fields.some((field) => !field.inserted)) {
+ throw new Error(`Document ${document.id} has unsigned fields`);
+ }
+
+ if (isResealing) {
+ // If we're resealing we want to use the initial data for the document
+ // so we aren't placing fields on top of eachother.
+ documentData.data = documentData.initialData;
+ }
+
+ const pdfData = await getFile(documentData);
+ const certificateData = await getCertificatePdf({ documentId }).catch(() => null);
+
+ const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
+ const pdfDoc = await PDFDocument.load(pdfData);
+
+ // Normalize and flatten layers that could cause issues with the signature
+ normalizeSignatureAppearances(pdfDoc);
+ flattenForm(pdfDoc);
+ flattenAnnotations(pdfDoc);
+
+ if (certificateData) {
+ const certificateDoc = await PDFDocument.load(certificateData);
+
+ const certificatePages = await pdfDoc.copyPages(
+ certificateDoc,
+ certificateDoc.getPageIndices(),
+ );
+
+ certificatePages.forEach((page) => {
+ pdfDoc.addPage(page);
+ });
+ }
+
+ for (const field of fields) {
+ await insertFieldInPDF(pdfDoc, field);
+ }
+
+ // Re-flatten the form to handle our checkbox and radio fields that
+ // create native arcoFields
+ flattenForm(pdfDoc);
+
+ const pdfBytes = await pdfDoc.save();
+ const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
+
+ const { name, ext } = path.parse(document.title);
+
+ const documentData = await putPdfFile({
+ name: `${name}_signed${ext}`,
+ type: 'application/pdf',
+ arrayBuffer: async () => Promise.resolve(pdfBuffer),
+ });
+
+ return documentData.id;
+ });
+
+ const postHog = PostHogServerClient();
+
+ if (postHog) {
+ postHog.capture({
+ distinctId: nanoid(),
+ event: 'App: Document Sealed',
+ properties: {
+ documentId: document.id,
+ },
+ });
+ }
+
+ await io.runTask('update-document', async () => {
+ await prisma.$transaction(async (tx) => {
+ const newData = await tx.documentData.findFirstOrThrow({
+ where: {
+ id: newDataId,
+ },
+ });
+
+ await tx.document.update({
+ where: {
+ id: document.id,
+ },
+ data: {
+ status: DocumentStatus.COMPLETED,
+ completedAt: new Date(),
+ },
+ });
+
+ await tx.documentData.update({
+ where: {
+ id: documentData.id,
+ },
+ data: {
+ data: newData.data,
+ },
+ });
+
+ await tx.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
+ documentId: document.id,
+ requestMetadata,
+ user: null,
+ data: {
+ transactionId: nanoid(),
+ },
+ }),
+ });
+ });
+ });
+
+ await io.runTask('send-completed-email', async () => {
+ let shouldSendCompletedEmail = sendEmail && !isResealing;
+
+ if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
+ shouldSendCompletedEmail = sendEmail;
+ }
+
+ if (shouldSendCompletedEmail) {
+ await sendCompletedEmail({ documentId, requestMetadata });
+ }
+ });
+
+ const updatedDocument = await prisma.document.findFirstOrThrow({
+ where: {
+ id: document.id,
+ },
+ include: {
+ documentData: true,
+ Recipient: true,
+ },
+ });
+
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
+ data: updatedDocument,
+ userId: updatedDocument.userId,
+ teamId: updatedDocument.teamId ?? undefined,
+ });
+ },
+} as const satisfies JobDefinition<
+ typeof SEAL_DOCUMENT_JOB_DEFINITION_ID,
+ z.infer
+>;
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index b0d27589c..fa4413115 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -161,7 +161,10 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
id: profile.sub,
email: profile.email || profile.preferred_username,
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
- emailVerified: profile.email_verified ? new Date().toISOString() : null,
+ emailVerified:
+ process.env.NEXT_PRIVATE_OIDC_SKIP_VERIFY === 'true' || profile.email_verified
+ ? new Date().toISOString()
+ : null,
};
},
},
@@ -361,6 +364,12 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
},
async signIn({ user }) {
+ // This statement appears above so we can stil allow `oidc` connections
+ // while other signups are disabled.
+ if (env('NEXT_PRIVATE_OIDC_ALLOW_SIGNUP') === 'true') {
+ return true;
+ }
+
// We do this to stop OAuth providers from creating an account
// when signups are disabled
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
diff --git a/packages/lib/schemas/common.ts b/packages/lib/schemas/common.ts
index 101aeeff5..c63b9b399 100644
--- a/packages/lib/schemas/common.ts
+++ b/packages/lib/schemas/common.ts
@@ -1,12 +1,12 @@
import { z } from 'zod';
-import { URL_REGEX } from '../constants/url-regex';
+import { isValidRedirectUrl } from '../utils/is-valid-redirect-url';
/**
* Note this allows empty strings.
*/
export const ZUrlSchema = z
.string()
- .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
- message: 'Please enter a valid URL',
+ .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
+ message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
});
diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts
index 29d17cc50..c84b6f40e 100644
--- a/packages/lib/server-only/document/complete-document-with-token.ts
+++ b/packages/lib/server-only/document/complete-document-with-token.ts
@@ -1,5 +1,3 @@
-'use server';
-
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
@@ -7,9 +5,9 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
+import { jobs } from '../../jobs/client';
import type { TRecipientActionAuth } from '../../types/document-auth';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
-import { sealDocument } from './seal-document';
import { sendPendingEmail } from './send-pending-email';
export type CompleteDocumentWithTokenOptions = {
@@ -45,8 +43,6 @@ export const completeDocumentWithToken = async ({
documentId,
requestMetadata,
}: CompleteDocumentWithTokenOptions) => {
- 'use server';
-
const document = await getDocument({ token, documentId });
if (document.status !== DocumentStatus.PENDING) {
@@ -149,7 +145,13 @@ export const completeDocumentWithToken = async ({
});
if (haveAllRecipientsSigned) {
- await sealDocument({ documentId: document.id, requestMetadata });
+ await jobs.triggerJob({
+ name: 'internal.seal-document',
+ payload: {
+ documentId: document.id,
+ requestMetadata,
+ },
+ });
}
const updatedDocument = await getDocument({ token, documentId });
diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx
index c8a51cac4..8ea39445c 100644
--- a/packages/lib/server-only/document/resend-document.tsx
+++ b/packages/lib/server-only/document/resend-document.tsx
@@ -58,10 +58,17 @@ export const resendDocument = async ({
},
},
documentMeta: true,
+ team: {
+ select: {
+ teamEmail: true,
+ name: true,
+ },
+ },
},
});
const customEmail = document?.documentMeta;
+ const isTeamDocument = document?.team !== null;
if (!document) {
throw new Error('Document not found');
@@ -90,9 +97,21 @@ export const resendDocument = async ({
const { email, name } = recipient;
const selfSigner = email === user.email;
- const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[
- recipient.role
- ].actionVerb.toLowerCase()} it.`;
+ const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
+ const recipientActionVerb = actionVerb.toLowerCase();
+
+ let emailMessage = customEmail?.message || '';
+ let emailSubject = `Reminder: Please ${recipientActionVerb} this document`;
+
+ if (selfSigner) {
+ emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
+ emailSubject = `Reminder: Please ${recipientActionVerb} your document`;
+ }
+
+ if (isTeamDocument && document.team) {
+ emailSubject = `Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`;
+ emailMessage = `${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
+ }
const customEmailTemplate = {
'signer.name': name,
@@ -106,23 +125,16 @@ export const resendDocument = async ({
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
- inviterEmail: user.email,
+ inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
- customBody: renderCustomEmailTemplate(
- selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '',
- customEmailTemplate,
- ),
+ customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
+ isTeamInvite: isTeamDocument,
+ teamName: document.team?.name,
});
- const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
-
- const emailSubject = selfSigner
- ? `Reminder: Please ${actionVerb.toLowerCase()} your document`
- : `Reminder: Please ${actionVerb.toLowerCase()} this document`;
-
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts
index b937b33ea..7c0c3bced 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -1,5 +1,3 @@
-'use server';
-
import { nanoid } from 'nanoid';
import path from 'node:path';
import { PDFDocument } from 'pdf-lib';
@@ -36,8 +34,6 @@ export const sealDocument = async ({
isResealing = false,
requestMetadata,
}: SealDocumentOptions) => {
- 'use server';
-
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx
index 10ec3bc44..81b8e4292 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -1,4 +1,3 @@
-import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
@@ -7,7 +6,7 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
-import { jobsClient } from '../../jobs/client';
+import { jobs } from '../../jobs/client';
import { getFile } from '../../universal/upload/get-file';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -141,7 +140,7 @@ export const sendDocument = async ({
return;
}
- await jobsClient.triggerJob({
+ await jobs.triggerJob({
name: 'send.signing.requested.email',
payload: {
userId,
@@ -160,7 +159,13 @@ export const sendDocument = async ({
);
if (allRecipientsHaveNoActionToTake) {
- await sealDocument({ documentId, requestMetadata });
+ await jobs.triggerJob({
+ name: 'internal.seal-document',
+ payload: {
+ documentId,
+ requestMetadata,
+ },
+ });
// Keep the return type the same for the `sendDocument` method
return await prisma.document.findFirstOrThrow({
diff --git a/packages/lib/server-only/field/sign-field-with-token.ts b/packages/lib/server-only/field/sign-field-with-token.ts
index aa596dc37..087de646b 100644
--- a/packages/lib/server-only/field/sign-field-with-token.ts
+++ b/packages/lib/server-only/field/sign-field-with-token.ts
@@ -231,10 +231,17 @@ export const signFieldWithToken = async ({
type,
data: signatureImageAsBase64 || typedSignature || '',
}))
- .with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({
- type,
- data: updatedField.customText,
- }))
+ .with(
+ FieldType.DATE,
+ FieldType.EMAIL,
+ FieldType.NAME,
+ FieldType.TEXT,
+ FieldType.INITIALS,
+ (type) => ({
+ type,
+ data: updatedField.customText,
+ }),
+ )
.with(
FieldType.NUMBER,
FieldType.RADIO,
diff --git a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
index 1b6150fb9..56c2e32cf 100644
--- a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
+++ b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import type { Browser } from 'playwright';
-import { chromium } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { encryptSecondaryData } from '../crypto/encrypt';
@@ -10,6 +9,8 @@ export type GetCertificatePdfOptions = {
};
export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions) => {
+ const { chromium } = await import('playwright');
+
const encryptedId = encryptSecondaryData({
data: documentId.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
index 401e03b05..9a1441650 100644
--- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts
+++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
@@ -133,9 +133,14 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
throw new Error('Invalid checkbox field meta');
}
+ const values = meta.data.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+
const selected = field.customText.split(',');
- for (const [index, item] of (meta.data.values ?? []).entries()) {
+ for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * 16;
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
@@ -169,9 +174,14 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
throw new Error('Invalid radio field meta');
}
+ const values = meta?.data.values?.map((item) => ({
+ ...item,
+ value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
+ }));
+
const selected = field.customText.split(',');
- for (const [index, item] of (meta.data.values ?? []).entries()) {
+ for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * 16;
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts
index e7f119dfa..5827fb76c 100644
--- a/packages/lib/server-only/template/create-document-from-direct-template.ts
+++ b/packages/lib/server-only/template/create-document-from-direct-template.ts
@@ -44,6 +44,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
directRecipientName?: string;
directRecipientEmail: string;
directTemplateToken: string;
+ directTemplateExternalId?: string;
signedFieldValues: TSignFieldWithTokenMutationSchema[];
templateUpdatedAt: Date;
requestMetadata: RequestMetadata;
@@ -63,6 +64,7 @@ export const createDocumentFromDirectTemplate = async ({
directRecipientName: initialDirectRecipientName,
directRecipientEmail,
directTemplateToken,
+ directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
requestMetadata,
@@ -227,6 +229,7 @@ export const createDocumentFromDirectTemplate = async ({
title: template.title,
createdAt: initialRequestTime,
status: DocumentStatus.PENDING,
+ externalId: directTemplateExternalId,
documentDataId: documentData.id,
authOptions: createDocumentAuthOptions({
globalAccessAuth: templateAuthOptions.globalAccessAuth,
@@ -465,6 +468,7 @@ export const createDocumentFromDirectTemplate = async ({
.with(
FieldType.DATE,
FieldType.EMAIL,
+ FieldType.INITIALS,
FieldType.NAME,
FieldType.TEXT,
FieldType.NUMBER,
diff --git a/packages/lib/translations/de/common.po b/packages/lib/translations/de/common.po
index cc0e99d1a..2ccc01506 100644
--- a/packages/lib/translations/de/common.po
+++ b/packages/lib/translations/de/common.po
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2024-07-26 06:04\n"
+"PO-Revision-Date: 2024-08-20 14:03\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -17,3 +17,4 @@ msgstr ""
"X-Crowdin-Language: de\n"
"X-Crowdin-File: common.po\n"
"X-Crowdin-File-ID: 4\n"
+
diff --git a/packages/lib/translations/de/marketing.js b/packages/lib/translations/de/marketing.js
index 35d08cae3..3830fbe4e 100644
--- a/packages/lib/translations/de/marketing.js
+++ b/packages/lib/translations/de/marketing.js
@@ -1 +1 @@
-/*eslint-disable*/module.exports={messages:JSON.parse("{\"J/hVSQ\":[[\"0\"]],\"u0zktA\":\"5 Standarddokumente pro Monat\",\"rKtmiD\":\"5 Benutzer inbegriffen\",\"vaHmll\":\"Eine 10x bessere Signaturerfahrung.\",\"gBefbz\":[\"Mehr Benutzer hinzufügen für \",[\"0\"]],\"XkF8tv\":\"Alle unsere Kennzahlen, Finanzen und Erkenntnisse sind öffentlich. Wir glauben an Transparenz und möchten unsere Reise mit Ihnen teilen. Mehr erfahren Sie hier: <0>Ankündigung Offene Kennzahlen0>\",\"tkQ/WI\":\"Erhobener Betrag\",\"qOMroC\":\"API-Zugriff\",\"FNv8t7\":\"Schön.\",\"W/TUoX\":\"Weil Unterschriften gefeiert werden sollten. Deshalb kümmern wir uns um jedes kleinste Detail in unserem Produkt.\",\"astDB+\":\"Blog\",\"7zGun7\":\"Aufbauen oben drauf.\",\"fxgcNV\":\"Kann ich Documenso kommerziell nutzen?\",\"V+D/YP\":\"Karrieren\",\"CWe7wB\":\"Änderungsprotokoll\",\"JZbmjL\":\"Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können.\",\"chL5IG\":\"Gemeinschaft\",\"p5+XQN\":\"Fertige Dokumente\",\"NApCXa\":\"Fertige Dokumente pro Monat\",\"z5kV0h\":\"Verbindungen\",\"YcfUZ9\":\"Kontaktiere uns\",\"1NJjIG\":\"Erstellen Sie Verbindungen und Automatisierungen mit Zapier und mehr, um sich mit Ihren Lieblingstools zu integrieren.\",\"rr83qK\":\"Erstellen Sie Ihr Konto und beginnen Sie mit der Nutzung modernster Dokumentensignaturen. Offene und schöne Signaturen sind zum Greifen nah.\",\"75ojt0\":\"Kunden mit einer aktiven Abonnements.\",\"pF9qTh\":\"Anpassen und erweitern.\",\"f8fH8W\":\"Design\",\"W6qD1T\":\"Entwickelt für jede Phase Ihrer Reise.\",\"K6KbY4\":\"Direktlink\",\"aLD+Td\":\"Documenso ist eine Gemeinschaftsanstrengung, um ein offenes und lebendiges Ökosystem um ein Werkzeug zu schaffen, das jeder frei nutzen und anpassen kann. Indem wir wirklich offen sind, wollen wir vertrauenswürdige Infrastruktur für die Zukunft des Internets schaffen.\",\"32yG8y\":\"Documenso auf X\",\"+1xAO7\":\"Unterschriften,<0/>endlich Open Source.\",\"TvY/XA\":\"Dokumentation\",\"tSS7hj\":\"Betten Sie Documenso ganz einfach in Ihr Produkt ein. Kopieren und fügen Sie einfach unser React-Widget in Ihre Anwendung ein.\",\"BWMGM4\":\"Einfaches Teilen (Bald).\",\"V6EY8B\":\"E-Mail- und Discord-Support\",\"C0/bri\":\"Beteiligung\",\"8Zy3YU\":\"Enterprise-Konformität, Lizenz- oder technische Bedürfnisse?\",\"ZSW8id\":\"Alles, was Sie für ein großartiges Signaturerlebnis benötigen.\",\"sXswT6\":\"Schnell.\",\"cT9Z9e\":\"Schneller, intelligenter und schöner.\",\"k/ANik\":\"Finanzen\",\"I7Exsw\":\"Folgen Sie uns auf X\",\"f3Botn\":\"Für Unternehmen, die über mehrere Teams skalieren möchten.\",\"y2DcZj\":\"Für kleine Teams und Einzelpersonen mit grundlegenden Bedürfnissen.\",\"2POOFK\":\"Kostenlos\",\"OdieZe\":\"Aus dem Blog\",\"IPgkVQ\":\"Vollzeit\",\"aSWzT9\":\"Lassen Sie sich bezahlen (Bald).\",\"ZDIydz\":\"Loslegen\",\"c3b0B0\":\"Loslegen\",\"pS8wej\":\"Fangen Sie heute an.\",\"7FPIvI\":\"Erhalten Sie die neuesten Nachrichten von Documenso, einschließlich Produkt-Updates, Team-Ankündigungen und mehr!\",\"kV0qBq\":\"GitHub: Gesamte PRs zusammengeführt\",\"652R6j\":\"GitHub: Gesamte offene Issues\",\"R1aJ0W\":\"GitHub: Gesamtanzahl Sterne\",\"P1ovAc\":\"Globale Gehaltsbänder\",\"IAq/yr\":\"Wachstum\",\"Xi7f+z\":\"Wie kann ich beitragen?\",\"9VGuMA\":\"Wie gehen Sie mit meinen Daten um?\",\"fByw/g\":\"Einzelperson\",\"Csm+TN\":\"Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezahlen machen müssen.\",\"phSPy7\":\"Integriert sich mit all Ihren Lieblingstools.\",\"pfjrI2\":\"Gibt es mehr?\",\"LOyqaC\":\"Es liegt an Ihnen. Entweder klonen Sie unser Repository oder nutzen unsere einfach zu bedienende Hosting-Lösung.\",\"PCgMVa\":\"Eintrittsdatum\",\"TgL4dH\":\"Treten Sie der Open Signing-Bewegung bei\",\"wJijgU\":\"Standort\",\"OIowgO\":\"Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit.\",\"GHelWd\":\"Zusammengeführte PRs\",\"vXBVQZ\":\"Zusammengeführte PRs\",\"+8Nek/\":\"Monatlich\",\"6YtxFj\":\"Name\",\"CtgXe4\":\"Neue Benutzer\",\"OpNhRn\":\"Keine Kreditkarte erforderlich\",\"6C9AxJ\":\"Keine Kreditkarte erforderlich\",\"igwAqT\":\"Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!\",\"jjAtjQ\":\"Offene Issues\",\"b76QYo\":\"Open Source oder Hosted.\",\"OWsQIe\":\"Offenes Startup\",\"Un80BR\":\"OSS-Freunde\",\"6zNyfI\":\"Unsere benutzerdefinierten Vorlagen verfügen über intelligente Regeln, die Ihnen Zeit und Energie sparen können.\",\"+OmhKD\":\"Unsere Enterprise-Lizenz ist ideal für große Organisationen, die auf Documenso für all ihre Signaturanforderungen umsteigen möchten. Sie ist sowohl für unser Cloud-Angebot als auch für selbstgehostete Setups verfügbar und bietet eine breite Palette an Compliance- und Verwaltungsfunktionen.\",\"eK0veR\":\"Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features.\",\"I2ufwS\":\"Unsere selbstgehostete Option ist ideal für kleine Teams und Einzelpersonen, die eine einfache Lösung benötigen. Sie können unser docker-basiertes Setup verwenden, um in wenigen Minuten loszulegen. Übernehmen Sie die Kontrolle mit vollständiger Anpassbarkeit und Datenhoheit.\",\"F9564X\":\"Teilzeit\",\"qJVkX+\":\"Premium Profilname\",\"aHCEmh\":\"Preise\",\"rjGI/Q\":\"Datenschutz\",\"vERlcd\":\"Profil\",\"77/8W2\":\"React Widget (Demnächst).\",\"OYoVNk\":\"Erhalten Sie Ihren persönlichen Link zum Teilen mit allen, die Ihnen wichtig sind.\",\"GDvlUT\":\"Rolle\",\"bUqwb8\":\"Gehalt\",\"GNfoAO\":\"Sparen Sie $60 oder $120\",\"StoBff\":\"Sprachen suchen...\",\"dhi4w4\":\"Sicher. Unsere Rechenzentren befinden sich in Frankfurt (Deutschland) und bieten uns die besten lokalen Datenschutzgesetze. Uns ist die sensible Natur unserer Daten sehr bewusst und wir folgen bewährten Praktiken, um die Sicherheit und Integrität der uns anvertrauten Daten zu gewährleisten.\",\"kZBxnz\":\"Überall senden, verbinden, empfangen und einbetten.\",\"eSfS30\":\"Dienstalter\",\"aoDa18\":\"Shop\",\"5lWFkC\":\"Anmelden\",\"e+RpCP\":\"Registrieren\",\"4yiZOB\":\"Signaturprozess\",\"RkUXMm\":\"Jetzt registrieren\",\"omz3DH\":\"Intelligent.\",\"AvYbUL\":\"Auf GitHub favorisieren\",\"y2dGtU\":\"Favoriten\",\"uAQUqI\":\"Status\",\"XYLcNv\":\"Support\",\"KM6m8p\":\"Team\",\"lm5v+6\":\"Team-Posteingang\",\"CAL6E9\":\"Teams\",\"w4nM1s\":\"Vorlagen-Shop (Demnächst).\",\"yFoQ27\":\"Das ist großartig. Sie können sich die aktuellen <0>Issues0> ansehen und unserer <1>Discord-Community1> beitreten, um auf dem neuesten Stand zu bleiben, was die aktuellen Prioritäten sind. In jedem Fall sind wir eine offene Gemeinschaft und begrüßen jegliche Beiträge, technische und nicht-technische ❤️\",\"GE1BlA\":\"Diese Seite entwickelt sich weiter, während wir lernen, was ein großartiges Signing-Unternehmen ausmacht. Wir werden sie aktualisieren, wenn wir mehr zu teilen haben.\",\"MHrjPM\":\"Titel\",\"2YvdxE\":\"Insgesamt Abgeschlossene Dokumente\",\"8e4lIo\":\"Insgesamt Kunden\",\"bPpoCb\":\"Insgesamt Finanzierungsvolumen\",\"vb0Q0/\":\"Gesamtanzahl der Benutzer\",\"mgQhDS\":\"Wirklich Ihr Eigenes.\",\"4McJfQ\":\"Probieren Sie unseren Gratisplan aus\",\"9mkNAn\":\"Twitter-Statistiken\",\"CHzOWB\":\"Unbegrenzte Dokumente pro Monat\",\"BOV7DD\":\"Bis zu 10 Empfänger pro Dokument\",\"vdAd7c\":\"Die Nutzung unserer gehosteten Version ist der einfachste Weg, um zu starten. Sie können einfach abonnieren und mit der Unterzeichnung Ihrer Dokumente beginnen. Wir kümmern uns um die Infrastruktur, damit Sie sich auf Ihr Geschäft konzentrieren können. Zudem profitieren Sie bei der Nutzung unserer gehosteten Version von unseren vertrauenswürdigen Signaturzertifikaten, die Ihnen helfen, Vertrauen bei Ihren Kunden aufzubauen.\",\"W2nDs0\":\"Alle Statistiken anzeigen\",\"WMfAK8\":\"Wir helfen Ihnen gerne unter <0>support@documenso.com0> oder <1>in unserem Discord-Support-Kanal1>. Bitte senden Sie Lucas oder Timur eine Nachricht, um dem Kanal beizutreten, falls Sie noch kein Mitglied sind.\",\"ZaMyxU\":\"Was ist der Unterschied zwischen den Plänen?\",\"8GpyFt\":\"Wenn es um das Senden oder Empfangen eines Vertrags geht, können Sie auf blitzschnelle Geschwindigkeiten zählen.\",\"HEDnID\":\"Wo kann ich Unterstützung bekommen?\",\"sib3h3\":\"Warum sollte ich Documenso gegenüber DocuSign oder einem anderen Signatur-Tool bevorzugen?\",\"cVPDPt\":\"Warum sollte ich Ihren Hosting-Service nutzen?\",\"zkWmBh\":\"Jährlich\",\"8AKApo\":\"Ja! Documenso wird unter der GNU AGPL V3 Open-Source-Lizenz angeboten. Das bedeutet, dass Sie es kostenlos nutzen und sogar an Ihre Bedürfnisse anpassen können, solange Sie Ihre Änderungen unter derselben Lizenz veröffentlichen.\",\"rzQpex\":\"Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatzbereite gehostete Version nutzen. Die gehostete Version bietet zusätzlichen Support, schmerzfreie Skalierbarkeit und mehr. Frühzeitige Anwender erhalten in diesem Jahr Zugriff auf alle Funktionen, die wir entwickeln, ohne zusätzliche Kosten! Für immer! Ja, das beinhaltet später mehrere Benutzer pro Konto. Wenn Sie Documenso für Ihr Unternehmen möchten, sprechen wir gerne über Ihre Bedürfnisse.\",\"1j9aoC\":\"Ihr Browser unterstützt das Video-Tag nicht.\"}")};
\ No newline at end of file
+/*eslint-disable*/module.exports={messages:JSON.parse("{\"J/hVSQ\":[[\"0\"]],\"u0zktA\":\"5 Standarddokumente pro Monat\",\"rKtmiD\":\"5 Benutzer inbegriffen\",\"vaHmll\":\"Eine 10x bessere Signaturerfahrung.\",\"gBefbz\":[\"Mehr Benutzer hinzufügen für \",[\"0\"]],\"XkF8tv\":\"Alle unsere Kennzahlen, Finanzen und Erkenntnisse sind öffentlich. Wir glauben an Transparenz und möchten unsere Reise mit Ihnen teilen. Mehr erfahren Sie hier: <0>Ankündigung Offene Kennzahlen0>\",\"tkQ/WI\":\"Erhobener Betrag\",\"qOMroC\":\"API-Zugriff\",\"FNv8t7\":\"Schön.\",\"W/TUoX\":\"Weil Unterschriften gefeiert werden sollten. Deshalb kümmern wir uns um jedes kleinste Detail in unserem Produkt.\",\"astDB+\":\"Blog\",\"7zGun7\":\"Aufbauen oben drauf.\",\"fxgcNV\":\"Kann ich Documenso kommerziell nutzen?\",\"V+D/YP\":\"Karrieren\",\"CWe7wB\":\"Änderungsprotokoll\",\"JZbmjL\":\"Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können.\",\"chL5IG\":\"Gemeinschaft\",\"p5+XQN\":\"Fertige Dokumente\",\"NApCXa\":\"Fertige Dokumente pro Monat\",\"z5kV0h\":\"Verbindungen\",\"YcfUZ9\":\"Kontaktiere uns\",\"1NJjIG\":\"Erstellen Sie Verbindungen und Automatisierungen mit Zapier und mehr, um sich mit Ihren Lieblingstools zu integrieren.\",\"rr83qK\":\"Erstellen Sie Ihr Konto und beginnen Sie mit der Nutzung modernster Dokumentensignaturen. Offene und schöne Signaturen sind zum Greifen nah.\",\"75ojt0\":\"Kunden mit einer aktiven Abonnements.\",\"pF9qTh\":\"Anpassen und erweitern.\",\"f8fH8W\":\"Design\",\"W6qD1T\":\"Entwickelt für jede Phase Ihrer Reise.\",\"K6KbY4\":\"Direktlink\",\"aLD+Td\":\"Documenso ist eine Gemeinschaftsanstrengung, um ein offenes und lebendiges Ökosystem um ein Werkzeug zu schaffen, das jeder frei nutzen und anpassen kann. Indem wir wirklich offen sind, wollen wir vertrauenswürdige Infrastruktur für die Zukunft des Internets schaffen.\",\"32yG8y\":\"Documenso auf X\",\"+1xAO7\":\"Unterschriften,<0/>endlich Open Source.\",\"TvY/XA\":\"Dokumentation\",\"tSS7hj\":\"Betten Sie Documenso ganz einfach in Ihr Produkt ein. Kopieren und fügen Sie einfach unser React-Widget in Ihre Anwendung ein.\",\"BWMGM4\":\"Einfaches Teilen (Bald).\",\"LRAhFG\":\"Easy Sharing.\",\"V6EY8B\":\"E-Mail- und Discord-Support\",\"C0/bri\":\"Beteiligung\",\"8Zy3YU\":\"Enterprise-Konformität, Lizenz- oder technische Bedürfnisse?\",\"ZSW8id\":\"Alles, was Sie für ein großartiges Signaturerlebnis benötigen.\",\"sXswT6\":\"Schnell.\",\"cT9Z9e\":\"Schneller, intelligenter und schöner.\",\"k/ANik\":\"Finanzen\",\"I7Exsw\":\"Folgen Sie uns auf X\",\"f3Botn\":\"Für Unternehmen, die über mehrere Teams skalieren möchten.\",\"y2DcZj\":\"Für kleine Teams und Einzelpersonen mit grundlegenden Bedürfnissen.\",\"2POOFK\":\"Kostenlos\",\"OdieZe\":\"Aus dem Blog\",\"IPgkVQ\":\"Vollzeit\",\"aSWzT9\":\"Lassen Sie sich bezahlen (Bald).\",\"ZDIydz\":\"Loslegen\",\"c3b0B0\":\"Loslegen\",\"pS8wej\":\"Fangen Sie heute an.\",\"7FPIvI\":\"Erhalten Sie die neuesten Nachrichten von Documenso, einschließlich Produkt-Updates, Team-Ankündigungen und mehr!\",\"kV0qBq\":\"GitHub: Gesamte PRs zusammengeführt\",\"652R6j\":\"GitHub: Gesamte offene Issues\",\"R1aJ0W\":\"GitHub: Gesamtanzahl Sterne\",\"P1ovAc\":\"Globale Gehaltsbänder\",\"IAq/yr\":\"Wachstum\",\"Xi7f+z\":\"Wie kann ich beitragen?\",\"9VGuMA\":\"Wie gehen Sie mit meinen Daten um?\",\"fByw/g\":\"Einzelperson\",\"Csm+TN\":\"Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezahlen machen müssen.\",\"phSPy7\":\"Integriert sich mit all Ihren Lieblingstools.\",\"pfjrI2\":\"Gibt es mehr?\",\"LOyqaC\":\"Es liegt an Ihnen. Entweder klonen Sie unser Repository oder nutzen unsere einfach zu bedienende Hosting-Lösung.\",\"PCgMVa\":\"Eintrittsdatum\",\"TgL4dH\":\"Treten Sie der Open Signing-Bewegung bei\",\"wJijgU\":\"Standort\",\"OIowgO\":\"Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit.\",\"GHelWd\":\"Zusammengeführte PRs\",\"vXBVQZ\":\"Zusammengeführte PRs\",\"+8Nek/\":\"Monatlich\",\"6YtxFj\":\"Name\",\"CtgXe4\":\"Neue Benutzer\",\"OpNhRn\":\"Keine Kreditkarte erforderlich\",\"6C9AxJ\":\"Keine Kreditkarte erforderlich\",\"igwAqT\":\"Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!\",\"jjAtjQ\":\"Offene Issues\",\"b76QYo\":\"Open Source oder Hosted.\",\"OWsQIe\":\"Offenes Startup\",\"Un80BR\":\"OSS-Freunde\",\"6zNyfI\":\"Unsere benutzerdefinierten Vorlagen verfügen über intelligente Regeln, die Ihnen Zeit und Energie sparen können.\",\"+OmhKD\":\"Unsere Enterprise-Lizenz ist ideal für große Organisationen, die auf Documenso für all ihre Signaturanforderungen umsteigen möchten. Sie ist sowohl für unser Cloud-Angebot als auch für selbstgehostete Setups verfügbar und bietet eine breite Palette an Compliance- und Verwaltungsfunktionen.\",\"eK0veR\":\"Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features.\",\"I2ufwS\":\"Unsere selbstgehostete Option ist ideal für kleine Teams und Einzelpersonen, die eine einfache Lösung benötigen. Sie können unser docker-basiertes Setup verwenden, um in wenigen Minuten loszulegen. Übernehmen Sie die Kontrolle mit vollständiger Anpassbarkeit und Datenhoheit.\",\"F9564X\":\"Teilzeit\",\"qJVkX+\":\"Premium Profilname\",\"aHCEmh\":\"Preise\",\"rjGI/Q\":\"Datenschutz\",\"vERlcd\":\"Profil\",\"77/8W2\":\"React Widget (Demnächst).\",\"OYoVNk\":\"Erhalten Sie Ihren persönlichen Link zum Teilen mit allen, die Ihnen wichtig sind.\",\"GDvlUT\":\"Rolle\",\"bUqwb8\":\"Gehalt\",\"GNfoAO\":\"Sparen Sie $60 oder $120\",\"StoBff\":\"Sprachen suchen...\",\"dhi4w4\":\"Sicher. Unsere Rechenzentren befinden sich in Frankfurt (Deutschland) und bieten uns die besten lokalen Datenschutzgesetze. Uns ist die sensible Natur unserer Daten sehr bewusst und wir folgen bewährten Praktiken, um die Sicherheit und Integrität der uns anvertrauten Daten zu gewährleisten.\",\"kZBxnz\":\"Überall senden, verbinden, empfangen und einbetten.\",\"eSfS30\":\"Dienstalter\",\"aoDa18\":\"Shop\",\"5lWFkC\":\"Anmelden\",\"e+RpCP\":\"Registrieren\",\"4yiZOB\":\"Signaturprozess\",\"RkUXMm\":\"Jetzt registrieren\",\"omz3DH\":\"Intelligent.\",\"AvYbUL\":\"Auf GitHub favorisieren\",\"y2dGtU\":\"Favoriten\",\"uAQUqI\":\"Status\",\"XYLcNv\":\"Support\",\"KM6m8p\":\"Team\",\"lm5v+6\":\"Team-Posteingang\",\"CAL6E9\":\"Teams\",\"w4nM1s\":\"Vorlagen-Shop (Demnächst).\",\"yFoQ27\":\"Das ist großartig. Sie können sich die aktuellen <0>Issues0> ansehen und unserer <1>Discord-Community1> beitreten, um auf dem neuesten Stand zu bleiben, was die aktuellen Prioritäten sind. In jedem Fall sind wir eine offene Gemeinschaft und begrüßen jegliche Beiträge, technische und nicht-technische ❤️\",\"GE1BlA\":\"Diese Seite entwickelt sich weiter, während wir lernen, was ein großartiges Signing-Unternehmen ausmacht. Wir werden sie aktualisieren, wenn wir mehr zu teilen haben.\",\"MHrjPM\":\"Titel\",\"2YvdxE\":\"Insgesamt Abgeschlossene Dokumente\",\"8e4lIo\":\"Insgesamt Kunden\",\"bPpoCb\":\"Insgesamt Finanzierungsvolumen\",\"vb0Q0/\":\"Gesamtanzahl der Benutzer\",\"mgQhDS\":\"Wirklich Ihr Eigenes.\",\"4McJfQ\":\"Probieren Sie unseren Gratisplan aus\",\"9mkNAn\":\"Twitter-Statistiken\",\"CHzOWB\":\"Unbegrenzte Dokumente pro Monat\",\"BOV7DD\":\"Bis zu 10 Empfänger pro Dokument\",\"vdAd7c\":\"Die Nutzung unserer gehosteten Version ist der einfachste Weg, um zu starten. Sie können einfach abonnieren und mit der Unterzeichnung Ihrer Dokumente beginnen. Wir kümmern uns um die Infrastruktur, damit Sie sich auf Ihr Geschäft konzentrieren können. Zudem profitieren Sie bei der Nutzung unserer gehosteten Version von unseren vertrauenswürdigen Signaturzertifikaten, die Ihnen helfen, Vertrauen bei Ihren Kunden aufzubauen.\",\"W2nDs0\":\"Alle Statistiken anzeigen\",\"WMfAK8\":\"Wir helfen Ihnen gerne unter <0>support@documenso.com0> oder <1>in unserem Discord-Support-Kanal1>. Bitte senden Sie Lucas oder Timur eine Nachricht, um dem Kanal beizutreten, falls Sie noch kein Mitglied sind.\",\"ZaMyxU\":\"Was ist der Unterschied zwischen den Plänen?\",\"8GpyFt\":\"Wenn es um das Senden oder Empfangen eines Vertrags geht, können Sie auf blitzschnelle Geschwindigkeiten zählen.\",\"HEDnID\":\"Wo kann ich Unterstützung bekommen?\",\"sib3h3\":\"Warum sollte ich Documenso gegenüber DocuSign oder einem anderen Signatur-Tool bevorzugen?\",\"cVPDPt\":\"Warum sollte ich Ihren Hosting-Service nutzen?\",\"zkWmBh\":\"Jährlich\",\"8AKApo\":\"Ja! Documenso wird unter der GNU AGPL V3 Open-Source-Lizenz angeboten. Das bedeutet, dass Sie es kostenlos nutzen und sogar an Ihre Bedürfnisse anpassen können, solange Sie Ihre Änderungen unter derselben Lizenz veröffentlichen.\",\"rzQpex\":\"Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatzbereite gehostete Version nutzen. Die gehostete Version bietet zusätzlichen Support, schmerzfreie Skalierbarkeit und mehr. Frühzeitige Anwender erhalten in diesem Jahr Zugriff auf alle Funktionen, die wir entwickeln, ohne zusätzliche Kosten! Für immer! Ja, das beinhaltet später mehrere Benutzer pro Konto. Wenn Sie Documenso für Ihr Unternehmen möchten, sprechen wir gerne über Ihre Bedürfnisse.\",\"1j9aoC\":\"Ihr Browser unterstützt das Video-Tag nicht.\"}")};
\ No newline at end of file
diff --git a/packages/lib/translations/de/marketing.po b/packages/lib/translations/de/marketing.po
index 9fc0985ce..8a7fec45b 100644
--- a/packages/lib/translations/de/marketing.po
+++ b/packages/lib/translations/de/marketing.po
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2024-07-26 06:04\n"
+"PO-Revision-Date: 2024-08-20 14:03\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -30,7 +30,7 @@ msgstr "5 Standarddokumente pro Monat"
msgid "5 Users Included"
msgstr "5 Benutzer inbegriffen"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:30
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:34
msgid "A 10x better signing experience."
msgstr "Eine 10x bessere Signaturerfahrung."
@@ -52,11 +52,11 @@ msgstr "Erhobener Betrag"
msgid "API Access"
msgstr "API-Zugriff"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:63
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:67
msgid "Beautiful."
msgstr "Schön."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:65
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:69
msgid "Because signing should be celebrated. That’s why we care about the smallest detail in our product."
msgstr "Weil Unterschriften gefeiert werden sollten. Deshalb kümmern wir uns um jedes kleinste Detail in unserem Produkt."
@@ -66,7 +66,7 @@ msgstr "Weil Unterschriften gefeiert werden sollten. Deshalb kümmern wir uns um
msgid "Blog"
msgstr "Blog"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:60
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:64
msgid "Build on top."
msgstr "Aufbauen oben drauf."
@@ -82,7 +82,7 @@ msgstr "Karrieren"
msgid "Changelog"
msgstr "Änderungsprotokoll"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:81
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:85
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können."
@@ -98,7 +98,7 @@ msgstr "Fertige Dokumente"
msgid "Completed Documents per Month"
msgstr "Fertige Dokumente pro Monat"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:61
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:65
msgid "Connections"
msgstr "Verbindungen"
@@ -106,7 +106,7 @@ msgstr "Verbindungen"
msgid "Contact Us"
msgstr "Kontaktiere uns"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:63
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:67
msgid "Create connections and automations with Zapier and more to integrate with your favorite tools."
msgstr "Erstellen Sie Verbindungen und Automatisierungen mit Zapier und mehr, um sich mit Ihren Lieblingstools zu integrieren."
@@ -118,7 +118,7 @@ msgstr "Erstellen Sie Ihr Konto und beginnen Sie mit der Nutzung modernster Doku
msgid "Customers with an Active Subscriptions."
msgstr "Kunden mit einer aktiven Abonnements."
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:29
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:33
msgid "Customise and expand."
msgstr "Anpassen und erweitern."
@@ -130,7 +130,7 @@ msgstr "Design"
msgid "Designed for every stage of your journey."
msgstr "Entwickelt für jede Phase Ihrer Reise."
-#: apps/marketing/src/components/(marketing)/carousel.tsx:37
+#: apps/marketing/src/components/(marketing)/carousel.tsx:40
msgid "Direct Link"
msgstr "Direktlink"
@@ -142,7 +142,7 @@ msgstr "Documenso ist eine Gemeinschaftsanstrengung, um ein offenes und lebendig
msgid "Documenso on X"
msgstr "Documenso auf X"
-#: apps/marketing/src/components/(marketing)/hero.tsx:100
+#: apps/marketing/src/components/(marketing)/hero.tsx:104
msgid "Document signing,<0/>finally open source."
msgstr "Unterschriften,<0/>endlich Open Source."
@@ -152,13 +152,17 @@ msgstr "Unterschriften,<0/>endlich Open Source."
msgid "Documentation"
msgstr "Dokumentation"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:106
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:110
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Betten Sie Documenso ganz einfach in Ihr Produkt ein. Kopieren und fügen Sie einfach unser React-Widget in Ihre Anwendung ein."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
-msgid "Easy Sharing (Soon)."
-msgstr "Einfaches Teilen (Bald)."
+#~ msgid "Easy Sharing (Soon)."
+#~ msgstr "Easy Sharing (Soon)."
+
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
+msgid "Easy Sharing."
+msgstr "Einfaches Teilen."
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:148
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:192
@@ -177,11 +181,11 @@ msgstr "Enterprise-Konformität, Lizenz- oder technische Bedürfnisse?"
msgid "Everything you need for a great signing experience."
msgstr "Alles, was Sie für ein großartiges Signaturerlebnis benötigen."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:41
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:45
msgid "Fast."
msgstr "Schnell."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:32
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:36
msgid "Faster, smarter and more beautiful."
msgstr "Schneller, intelligenter und schöner."
@@ -217,7 +221,7 @@ msgstr "Aus dem Blog"
msgid "Full-Time"
msgstr "Vollzeit"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:83
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:87
msgid "Get paid (Soon)."
msgstr "Lassen Sie sich bezahlen (Bald)."
@@ -269,11 +273,11 @@ msgstr "Wie gehen Sie mit meinen Daten um?"
msgid "Individual"
msgstr "Einzelperson"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:85
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:89
msgid "Integrated payments with Stripe so you don’t have to worry about getting paid."
msgstr "Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezahlen machen müssen."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:31
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:35
msgid "Integrates with all your favourite tools."
msgstr "Integriert sich mit all Ihren Lieblingstools."
@@ -281,7 +285,7 @@ msgstr "Integriert sich mit all Ihren Lieblingstools."
msgid "Is there more?"
msgstr "Gibt es mehr?"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:40
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:44
msgid "It’s up to you. Either clone our repository or rely on our easy to use hosting solution."
msgstr "Es liegt an Ihnen. Entweder klonen Sie unser Repository oder nutzen unsere einfach zu bedienende Hosting-Lösung."
@@ -297,7 +301,7 @@ msgstr "Treten Sie der Open Signing-Bewegung bei"
msgid "Location"
msgstr "Standort"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:62
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:66
msgid "Make it your own through advanced customization and adjustability."
msgstr "Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit."
@@ -328,7 +332,7 @@ msgid "No credit card required"
msgstr "Keine Kreditkarte erforderlich"
#: apps/marketing/src/components/(marketing)/callout.tsx:29
-#: apps/marketing/src/components/(marketing)/hero.tsx:121
+#: apps/marketing/src/components/(marketing)/hero.tsx:125
msgid "No Credit Card required"
msgstr "Keine Kreditkarte erforderlich"
@@ -341,7 +345,7 @@ msgstr "Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!
msgid "Open Issues"
msgstr "Offene Issues"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:38
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:42
msgid "Open Source or Hosted."
msgstr "Open Source oder Hosted."
@@ -356,7 +360,7 @@ msgstr "Offenes Startup"
msgid "OSS Friends"
msgstr "OSS-Freunde"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:87
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:91
msgid "Our custom templates come with smart rules that can help you save time and energy."
msgstr "Unsere benutzerdefinierten Vorlagen verfügen über intelligente Regeln, die Ihnen Zeit und Energie sparen können."
@@ -392,15 +396,15 @@ msgstr "Preise"
msgid "Privacy"
msgstr "Datenschutz"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:55
+#: apps/marketing/src/components/(marketing)/carousel.tsx:58
msgid "Profile"
msgstr "Profil"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:104
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:108
msgid "React Widget (Soon)."
msgstr "React Widget (Demnächst)."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:44
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:48
msgid "Receive your personal link to share with everyone you care about."
msgstr "Erhalten Sie Ihren persönlichen Link zum Teilen mit allen, die Ihnen wichtig sind."
@@ -425,7 +429,7 @@ msgstr "Sprachen suchen..."
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "Sicher. Unsere Rechenzentren befinden sich in Frankfurt (Deutschland) und bieten uns die besten lokalen Datenschutzgesetze. Uns ist die sensible Natur unserer Daten sehr bewusst und wir folgen bewährten Praktiken, um die Sicherheit und Integrität der uns anvertrauten Daten zu gewährleisten."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:33
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:37
msgid "Send, connect, receive and embed everywhere."
msgstr "Überall senden, verbinden, empfangen und einbetten."
@@ -447,7 +451,7 @@ msgstr "Anmelden"
msgid "Sign up"
msgstr "Registrieren"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:19
+#: apps/marketing/src/components/(marketing)/carousel.tsx:22
msgid "Signing Process"
msgstr "Signaturprozess"
@@ -457,11 +461,11 @@ msgstr "Signaturprozess"
msgid "Signup Now"
msgstr "Jetzt registrieren"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:85
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:89
msgid "Smart."
msgstr "Intelligent."
-#: apps/marketing/src/components/(marketing)/hero.tsx:128
+#: apps/marketing/src/components/(marketing)/hero.tsx:132
msgid "Star on GitHub"
msgstr "Auf GitHub favorisieren"
@@ -487,12 +491,12 @@ msgstr "Team"
msgid "Team Inbox"
msgstr "Team-Posteingang"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:25
+#: apps/marketing/src/components/(marketing)/carousel.tsx:28
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:162
msgid "Teams"
msgstr "Teams"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:79
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:83
msgid "Template Store (Soon)."
msgstr "Vorlagen-Shop (Demnächst)."
@@ -528,12 +532,12 @@ msgstr "Insgesamt Finanzierungsvolumen"
msgid "Total Users"
msgstr "Gesamtanzahl der Benutzer"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:27
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:31
msgid "Truly your own."
msgstr "Wirklich Ihr Eigenes."
#: apps/marketing/src/components/(marketing)/callout.tsx:27
-#: apps/marketing/src/components/(marketing)/hero.tsx:119
+#: apps/marketing/src/components/(marketing)/hero.tsx:123
msgid "Try our Free Plan"
msgstr "Probieren Sie unseren Gratisplan aus"
@@ -566,7 +570,7 @@ msgstr "Wir helfen Ihnen gerne unter <0>support@documenso.com0> oder <1>in uns
msgid "What is the difference between the plans?"
msgstr "Was ist der Unterschied zwischen den Plänen?"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:43
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:47
msgid "When it comes to sending or receiving a contract, you can count on lightning-fast speeds."
msgstr "Wenn es um das Senden oder Empfangen eines Vertrags geht, können Sie auf blitzschnelle Geschwindigkeiten zählen."
@@ -594,6 +598,7 @@ msgstr "Ja! Documenso wird unter der GNU AGPL V3 Open-Source-Lizenz angeboten. D
msgid "You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs."
msgstr "Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatzbereite gehostete Version nutzen. Die gehostete Version bietet zusätzlichen Support, schmerzfreie Skalierbarkeit und mehr. Frühzeitige Anwender erhalten in diesem Jahr Zugriff auf alle Funktionen, die wir entwickeln, ohne zusätzliche Kosten! Für immer! Ja, das beinhaltet später mehrere Benutzer pro Konto. Wenn Sie Documenso für Ihr Unternehmen möchten, sprechen wir gerne über Ihre Bedürfnisse."
-#: apps/marketing/src/components/(marketing)/carousel.tsx:258
+#: apps/marketing/src/components/(marketing)/carousel.tsx:265
msgid "Your browser does not support the video tag."
msgstr "Ihr Browser unterstützt das Video-Tag nicht."
+
diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po
index 2e315afce..c8c7cd6bb 100644
--- a/packages/lib/translations/de/web.po
+++ b/packages/lib/translations/de/web.po
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2024-07-26 06:04\n"
+"PO-Revision-Date: 2024-08-20 14:03\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -17,3 +17,4 @@ msgstr ""
"X-Crowdin-Language: de\n"
"X-Crowdin-File: web.po\n"
"X-Crowdin-File-ID: 8\n"
+
diff --git a/packages/lib/translations/en/marketing.js b/packages/lib/translations/en/marketing.js
index 89ef46516..7bca3ac53 100644
--- a/packages/lib/translations/en/marketing.js
+++ b/packages/lib/translations/en/marketing.js
@@ -1 +1 @@
-/*eslint-disable*/module.exports={messages:JSON.parse("{\"J/hVSQ\":[[\"0\"]],\"u0zktA\":\"5 standard documents per month\",\"rKtmiD\":\"5 Users Included\",\"vaHmll\":\"A 10x better signing experience.\",\"gBefbz\":[\"Add More Users for \",[\"0\"]],\"XkF8tv\":\"All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics0>\",\"tkQ/WI\":\"Amount Raised\",\"qOMroC\":\"API Access\",\"FNv8t7\":\"Beautiful.\",\"W/TUoX\":\"Because signing should be celebrated. That’s why we care about the smallest detail in our product.\",\"astDB+\":\"Blog\",\"7zGun7\":\"Build on top.\",\"fxgcNV\":\"Can I use Documenso commercially?\",\"V+D/YP\":\"Careers\",\"CWe7wB\":\"Changelog\",\"JZbmjL\":\"Choose a template from the community app store. Or submit your own template for others to use.\",\"chL5IG\":\"Community\",\"p5+XQN\":\"Completed Documents\",\"NApCXa\":\"Completed Documents per Month\",\"z5kV0h\":\"Connections\",\"YcfUZ9\":\"Contact Us\",\"1NJjIG\":\"Create connections and automations with Zapier and more to integrate with your favorite tools.\",\"rr83qK\":\"Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp.\",\"75ojt0\":\"Customers with an Active Subscriptions.\",\"pF9qTh\":\"Customise and expand.\",\"f8fH8W\":\"Design\",\"W6qD1T\":\"Designed for every stage of your journey.\",\"K6KbY4\":\"Direct Link\",\"aLD+Td\":\"Documenso is a community effort to create an open and vibrant ecosystem around a tool, everybody is free to use and adapt. By being truly open we want to create trusted infrastructure for the future of the internet.\",\"32yG8y\":\"Documenso on X\",\"+1xAO7\":\"Document signing,<0/>finally open source.\",\"TvY/XA\":\"Documentation\",\"tSS7hj\":\"Easily embed Documenso into your product. Simply copy and paste our react widget into your application.\",\"BWMGM4\":\"Easy Sharing (Soon).\",\"V6EY8B\":\"Email and Discord Support\",\"C0/bri\":\"Engagement\",\"8Zy3YU\":\"Enterprise Compliance, License or Technical Needs?\",\"ZSW8id\":\"Everything you need for a great signing experience.\",\"sXswT6\":\"Fast.\",\"cT9Z9e\":\"Faster, smarter and more beautiful.\",\"k/ANik\":\"Finances\",\"I7Exsw\":\"Follow us on X\",\"f3Botn\":\"For companies looking to scale across multiple teams.\",\"y2DcZj\":\"For small teams and individuals with basic needs.\",\"2POOFK\":\"Free\",\"OdieZe\":\"From the blog\",\"IPgkVQ\":\"Full-Time\",\"aSWzT9\":\"Get paid (Soon).\",\"ZDIydz\":\"Get started\",\"c3b0B0\":\"Get Started\",\"pS8wej\":\"Get started today.\",\"7FPIvI\":\"Get the latest news from Documenso, including product updates, team announcements and more!\",\"kV0qBq\":\"GitHub: Total Merged PRs\",\"652R6j\":\"GitHub: Total Open Issues\",\"R1aJ0W\":\"GitHub: Total Stars\",\"P1ovAc\":\"Global Salary Bands\",\"IAq/yr\":\"Growth\",\"Xi7f+z\":\"How can I contribute?\",\"9VGuMA\":\"How do you handle my data?\",\"fByw/g\":\"Individual\",\"Csm+TN\":\"Integrated payments with Stripe so you don’t have to worry about getting paid.\",\"phSPy7\":\"Integrates with all your favourite tools.\",\"pfjrI2\":\"Is there more?\",\"LOyqaC\":\"It’s up to you. Either clone our repository or rely on our easy to use hosting solution.\",\"PCgMVa\":\"Join Date\",\"TgL4dH\":\"Join the Open Signing Movement\",\"wJijgU\":\"Location\",\"OIowgO\":\"Make it your own through advanced customization and adjustability.\",\"GHelWd\":\"Merged PR's\",\"vXBVQZ\":\"Merged PRs\",\"+8Nek/\":\"Monthly\",\"6YtxFj\":\"Name\",\"CtgXe4\":\"New Users\",\"OpNhRn\":\"No credit card required\",\"6C9AxJ\":\"No Credit Card required\",\"igwAqT\":\"None of these work for you? Try self-hosting!\",\"jjAtjQ\":\"Open Issues\",\"b76QYo\":\"Open Source or Hosted.\",\"OWsQIe\":\"Open Startup\",\"Un80BR\":\"OSS Friends\",\"6zNyfI\":\"Our custom templates come with smart rules that can help you save time and energy.\",\"+OmhKD\":\"Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features.\",\"eK0veR\":\"Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features.\",\"I2ufwS\":\"Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership.\",\"F9564X\":\"Part-Time\",\"qJVkX+\":\"Premium Profile Name\",\"aHCEmh\":\"Pricing\",\"rjGI/Q\":\"Privacy\",\"vERlcd\":\"Profile\",\"77/8W2\":\"React Widget (Soon).\",\"OYoVNk\":\"Receive your personal link to share with everyone you care about.\",\"GDvlUT\":\"Role\",\"bUqwb8\":\"Salary\",\"GNfoAO\":\"Save $60 or $120\",\"StoBff\":\"Search languages...\",\"dhi4w4\":\"Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us.\",\"kZBxnz\":\"Send, connect, receive and embed everywhere.\",\"eSfS30\":\"Seniority\",\"aoDa18\":\"Shop\",\"5lWFkC\":\"Sign in\",\"e+RpCP\":\"Sign up\",\"4yiZOB\":\"Signing Process\",\"RkUXMm\":\"Signup Now\",\"omz3DH\":\"Smart.\",\"AvYbUL\":\"Star on GitHub\",\"y2dGtU\":\"Stars\",\"uAQUqI\":\"Status\",\"XYLcNv\":\"Support\",\"KM6m8p\":\"Team\",\"lm5v+6\":\"Team Inbox\",\"CAL6E9\":\"Teams\",\"w4nM1s\":\"Template Store (Soon).\",\"yFoQ27\":\"That's awesome. You can take a look at the current <0>Issues0> and join our <1>Discord Community1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️\",\"GE1BlA\":\"This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share.\",\"MHrjPM\":\"Title\",\"2YvdxE\":\"Total Completed Documents\",\"8e4lIo\":\"Total Customers\",\"bPpoCb\":\"Total Funding Raised\",\"vb0Q0/\":\"Total Users\",\"mgQhDS\":\"Truly your own.\",\"4McJfQ\":\"Try our Free Plan\",\"9mkNAn\":\"Twitter Stats\",\"CHzOWB\":\"Unlimited Documents per Month\",\"BOV7DD\":\"Up to 10 recipients per document\",\"vdAd7c\":\"Using our hosted version is the easiest way to get started, you can simply subscribe and start signing your documents. We take care of the infrastructure, so you can focus on your business. Additionally, when using our hosted version you benefit from our trusted signing certificates which helps you to build trust with your customers.\",\"W2nDs0\":\"View all stats\",\"WMfAK8\":\"We are happy to assist you at <0>support@documenso.com0> or <1>in our Discord-Support-Channel1> please message either Lucas or Timur to get added to the channel if you are not already a member.\",\"ZaMyxU\":\"What is the difference between the plans?\",\"8GpyFt\":\"When it comes to sending or receiving a contract, you can count on lightning-fast speeds.\",\"HEDnID\":\"Where can I get support?\",\"sib3h3\":\"Why should I prefer Documenso over DocuSign or some other signing tool?\",\"cVPDPt\":\"Why should I use your hosting service?\",\"zkWmBh\":\"Yearly\",\"8AKApo\":\"Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you can use it for free and even modify it to fit your needs, as long as you publish your changes under the same license.\",\"rzQpex\":\"You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs.\",\"1j9aoC\":\"Your browser does not support the video tag.\"}")};
\ No newline at end of file
+/*eslint-disable*/module.exports={messages:JSON.parse("{\"J/hVSQ\":[[\"0\"]],\"u0zktA\":\"5 standard documents per month\",\"rKtmiD\":\"5 Users Included\",\"vaHmll\":\"A 10x better signing experience.\",\"gBefbz\":[\"Add More Users for \",[\"0\"]],\"XkF8tv\":\"All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics0>\",\"tkQ/WI\":\"Amount Raised\",\"qOMroC\":\"API Access\",\"FNv8t7\":\"Beautiful.\",\"W/TUoX\":\"Because signing should be celebrated. That’s why we care about the smallest detail in our product.\",\"astDB+\":\"Blog\",\"7zGun7\":\"Build on top.\",\"fxgcNV\":\"Can I use Documenso commercially?\",\"V+D/YP\":\"Careers\",\"CWe7wB\":\"Changelog\",\"JZbmjL\":\"Choose a template from the community app store. Or submit your own template for others to use.\",\"chL5IG\":\"Community\",\"p5+XQN\":\"Completed Documents\",\"NApCXa\":\"Completed Documents per Month\",\"z5kV0h\":\"Connections\",\"YcfUZ9\":\"Contact Us\",\"1NJjIG\":\"Create connections and automations with Zapier and more to integrate with your favorite tools.\",\"rr83qK\":\"Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp.\",\"75ojt0\":\"Customers with an Active Subscriptions.\",\"pF9qTh\":\"Customise and expand.\",\"f8fH8W\":\"Design\",\"W6qD1T\":\"Designed for every stage of your journey.\",\"K6KbY4\":\"Direct Link\",\"aLD+Td\":\"Documenso is a community effort to create an open and vibrant ecosystem around a tool, everybody is free to use and adapt. By being truly open we want to create trusted infrastructure for the future of the internet.\",\"32yG8y\":\"Documenso on X\",\"+1xAO7\":\"Document signing,<0/>finally open source.\",\"TvY/XA\":\"Documentation\",\"tSS7hj\":\"Easily embed Documenso into your product. Simply copy and paste our react widget into your application.\",\"BWMGM4\":\"Easy Sharing (Soon).\",\"LRAhFG\":\"Easy Sharing.\",\"V6EY8B\":\"Email and Discord Support\",\"C0/bri\":\"Engagement\",\"8Zy3YU\":\"Enterprise Compliance, License or Technical Needs?\",\"ZSW8id\":\"Everything you need for a great signing experience.\",\"sXswT6\":\"Fast.\",\"cT9Z9e\":\"Faster, smarter and more beautiful.\",\"k/ANik\":\"Finances\",\"I7Exsw\":\"Follow us on X\",\"f3Botn\":\"For companies looking to scale across multiple teams.\",\"y2DcZj\":\"For small teams and individuals with basic needs.\",\"2POOFK\":\"Free\",\"OdieZe\":\"From the blog\",\"IPgkVQ\":\"Full-Time\",\"aSWzT9\":\"Get paid (Soon).\",\"ZDIydz\":\"Get started\",\"c3b0B0\":\"Get Started\",\"pS8wej\":\"Get started today.\",\"7FPIvI\":\"Get the latest news from Documenso, including product updates, team announcements and more!\",\"kV0qBq\":\"GitHub: Total Merged PRs\",\"652R6j\":\"GitHub: Total Open Issues\",\"R1aJ0W\":\"GitHub: Total Stars\",\"P1ovAc\":\"Global Salary Bands\",\"IAq/yr\":\"Growth\",\"Xi7f+z\":\"How can I contribute?\",\"9VGuMA\":\"How do you handle my data?\",\"fByw/g\":\"Individual\",\"Csm+TN\":\"Integrated payments with Stripe so you don’t have to worry about getting paid.\",\"phSPy7\":\"Integrates with all your favourite tools.\",\"pfjrI2\":\"Is there more?\",\"LOyqaC\":\"It’s up to you. Either clone our repository or rely on our easy to use hosting solution.\",\"PCgMVa\":\"Join Date\",\"TgL4dH\":\"Join the Open Signing Movement\",\"wJijgU\":\"Location\",\"OIowgO\":\"Make it your own through advanced customization and adjustability.\",\"GHelWd\":\"Merged PR's\",\"vXBVQZ\":\"Merged PRs\",\"+8Nek/\":\"Monthly\",\"6YtxFj\":\"Name\",\"CtgXe4\":\"New Users\",\"OpNhRn\":\"No credit card required\",\"6C9AxJ\":\"No Credit Card required\",\"igwAqT\":\"None of these work for you? Try self-hosting!\",\"jjAtjQ\":\"Open Issues\",\"b76QYo\":\"Open Source or Hosted.\",\"OWsQIe\":\"Open Startup\",\"Un80BR\":\"OSS Friends\",\"6zNyfI\":\"Our custom templates come with smart rules that can help you save time and energy.\",\"+OmhKD\":\"Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features.\",\"eK0veR\":\"Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features.\",\"I2ufwS\":\"Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership.\",\"F9564X\":\"Part-Time\",\"qJVkX+\":\"Premium Profile Name\",\"aHCEmh\":\"Pricing\",\"rjGI/Q\":\"Privacy\",\"vERlcd\":\"Profile\",\"77/8W2\":\"React Widget (Soon).\",\"OYoVNk\":\"Receive your personal link to share with everyone you care about.\",\"GDvlUT\":\"Role\",\"bUqwb8\":\"Salary\",\"GNfoAO\":\"Save $60 or $120\",\"StoBff\":\"Search languages...\",\"dhi4w4\":\"Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us.\",\"kZBxnz\":\"Send, connect, receive and embed everywhere.\",\"eSfS30\":\"Seniority\",\"aoDa18\":\"Shop\",\"5lWFkC\":\"Sign in\",\"e+RpCP\":\"Sign up\",\"4yiZOB\":\"Signing Process\",\"RkUXMm\":\"Signup Now\",\"omz3DH\":\"Smart.\",\"AvYbUL\":\"Star on GitHub\",\"y2dGtU\":\"Stars\",\"uAQUqI\":\"Status\",\"XYLcNv\":\"Support\",\"KM6m8p\":\"Team\",\"lm5v+6\":\"Team Inbox\",\"CAL6E9\":\"Teams\",\"w4nM1s\":\"Template Store (Soon).\",\"yFoQ27\":\"That's awesome. You can take a look at the current <0>Issues0> and join our <1>Discord Community1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️\",\"GE1BlA\":\"This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share.\",\"MHrjPM\":\"Title\",\"2YvdxE\":\"Total Completed Documents\",\"8e4lIo\":\"Total Customers\",\"bPpoCb\":\"Total Funding Raised\",\"vb0Q0/\":\"Total Users\",\"mgQhDS\":\"Truly your own.\",\"4McJfQ\":\"Try our Free Plan\",\"9mkNAn\":\"Twitter Stats\",\"CHzOWB\":\"Unlimited Documents per Month\",\"BOV7DD\":\"Up to 10 recipients per document\",\"vdAd7c\":\"Using our hosted version is the easiest way to get started, you can simply subscribe and start signing your documents. We take care of the infrastructure, so you can focus on your business. Additionally, when using our hosted version you benefit from our trusted signing certificates which helps you to build trust with your customers.\",\"W2nDs0\":\"View all stats\",\"WMfAK8\":\"We are happy to assist you at <0>support@documenso.com0> or <1>in our Discord-Support-Channel1> please message either Lucas or Timur to get added to the channel if you are not already a member.\",\"ZaMyxU\":\"What is the difference between the plans?\",\"8GpyFt\":\"When it comes to sending or receiving a contract, you can count on lightning-fast speeds.\",\"HEDnID\":\"Where can I get support?\",\"sib3h3\":\"Why should I prefer Documenso over DocuSign or some other signing tool?\",\"cVPDPt\":\"Why should I use your hosting service?\",\"zkWmBh\":\"Yearly\",\"8AKApo\":\"Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you can use it for free and even modify it to fit your needs, as long as you publish your changes under the same license.\",\"rzQpex\":\"You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs.\",\"1j9aoC\":\"Your browser does not support the video tag.\"}")};
\ No newline at end of file
diff --git a/packages/lib/translations/en/marketing.po b/packages/lib/translations/en/marketing.po
index 913242f11..ade6cf9be 100644
--- a/packages/lib/translations/en/marketing.po
+++ b/packages/lib/translations/en/marketing.po
@@ -25,7 +25,7 @@ msgstr "5 standard documents per month"
msgid "5 Users Included"
msgstr "5 Users Included"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:30
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:34
msgid "A 10x better signing experience."
msgstr "A 10x better signing experience."
@@ -47,11 +47,11 @@ msgstr "Amount Raised"
msgid "API Access"
msgstr "API Access"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:63
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:67
msgid "Beautiful."
msgstr "Beautiful."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:65
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:69
msgid "Because signing should be celebrated. That’s why we care about the smallest detail in our product."
msgstr "Because signing should be celebrated. That’s why we care about the smallest detail in our product."
@@ -61,7 +61,7 @@ msgstr "Because signing should be celebrated. That’s why we care about the sma
msgid "Blog"
msgstr "Blog"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:60
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:64
msgid "Build on top."
msgstr "Build on top."
@@ -77,7 +77,7 @@ msgstr "Careers"
msgid "Changelog"
msgstr "Changelog"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:81
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:85
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Choose a template from the community app store. Or submit your own template for others to use."
@@ -93,7 +93,7 @@ msgstr "Completed Documents"
msgid "Completed Documents per Month"
msgstr "Completed Documents per Month"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:61
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:65
msgid "Connections"
msgstr "Connections"
@@ -101,7 +101,7 @@ msgstr "Connections"
msgid "Contact Us"
msgstr "Contact Us"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:63
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:67
msgid "Create connections and automations with Zapier and more to integrate with your favorite tools."
msgstr "Create connections and automations with Zapier and more to integrate with your favorite tools."
@@ -113,7 +113,7 @@ msgstr "Create your account and start using state-of-the-art document signing. O
msgid "Customers with an Active Subscriptions."
msgstr "Customers with an Active Subscriptions."
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:29
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:33
msgid "Customise and expand."
msgstr "Customise and expand."
@@ -125,7 +125,7 @@ msgstr "Design"
msgid "Designed for every stage of your journey."
msgstr "Designed for every stage of your journey."
-#: apps/marketing/src/components/(marketing)/carousel.tsx:37
+#: apps/marketing/src/components/(marketing)/carousel.tsx:40
msgid "Direct Link"
msgstr "Direct Link"
@@ -137,7 +137,7 @@ msgstr "Documenso is a community effort to create an open and vibrant ecosystem
msgid "Documenso on X"
msgstr "Documenso on X"
-#: apps/marketing/src/components/(marketing)/hero.tsx:100
+#: apps/marketing/src/components/(marketing)/hero.tsx:104
msgid "Document signing,<0/>finally open source."
msgstr "Document signing,<0/>finally open source."
@@ -147,13 +147,17 @@ msgstr "Document signing,<0/>finally open source."
msgid "Documentation"
msgstr "Documentation"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:106
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:110
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
-msgid "Easy Sharing (Soon)."
-msgstr "Easy Sharing (Soon)."
+#~ msgid "Easy Sharing (Soon)."
+#~ msgstr "Easy Sharing (Soon)."
+
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
+msgid "Easy Sharing."
+msgstr "Easy Sharing."
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:148
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:192
@@ -172,11 +176,11 @@ msgstr "Enterprise Compliance, License or Technical Needs?"
msgid "Everything you need for a great signing experience."
msgstr "Everything you need for a great signing experience."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:41
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:45
msgid "Fast."
msgstr "Fast."
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:32
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:36
msgid "Faster, smarter and more beautiful."
msgstr "Faster, smarter and more beautiful."
@@ -212,7 +216,7 @@ msgstr "From the blog"
msgid "Full-Time"
msgstr "Full-Time"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:83
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:87
msgid "Get paid (Soon)."
msgstr "Get paid (Soon)."
@@ -264,11 +268,11 @@ msgstr "How do you handle my data?"
msgid "Individual"
msgstr "Individual"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:85
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:89
msgid "Integrated payments with Stripe so you don’t have to worry about getting paid."
msgstr "Integrated payments with Stripe so you don’t have to worry about getting paid."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:31
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:35
msgid "Integrates with all your favourite tools."
msgstr "Integrates with all your favourite tools."
@@ -276,7 +280,7 @@ msgstr "Integrates with all your favourite tools."
msgid "Is there more?"
msgstr "Is there more?"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:40
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:44
msgid "It’s up to you. Either clone our repository or rely on our easy to use hosting solution."
msgstr "It’s up to you. Either clone our repository or rely on our easy to use hosting solution."
@@ -292,7 +296,7 @@ msgstr "Join the Open Signing Movement"
msgid "Location"
msgstr "Location"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:62
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:66
msgid "Make it your own through advanced customization and adjustability."
msgstr "Make it your own through advanced customization and adjustability."
@@ -323,7 +327,7 @@ msgid "No credit card required"
msgstr "No credit card required"
#: apps/marketing/src/components/(marketing)/callout.tsx:29
-#: apps/marketing/src/components/(marketing)/hero.tsx:121
+#: apps/marketing/src/components/(marketing)/hero.tsx:125
msgid "No Credit Card required"
msgstr "No Credit Card required"
@@ -336,7 +340,7 @@ msgstr "None of these work for you? Try self-hosting!"
msgid "Open Issues"
msgstr "Open Issues"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:38
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:42
msgid "Open Source or Hosted."
msgstr "Open Source or Hosted."
@@ -351,7 +355,7 @@ msgstr "Open Startup"
msgid "OSS Friends"
msgstr "OSS Friends"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:87
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:91
msgid "Our custom templates come with smart rules that can help you save time and energy."
msgstr "Our custom templates come with smart rules that can help you save time and energy."
@@ -387,15 +391,15 @@ msgstr "Pricing"
msgid "Privacy"
msgstr "Privacy"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:55
+#: apps/marketing/src/components/(marketing)/carousel.tsx:58
msgid "Profile"
msgstr "Profile"
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:104
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:108
msgid "React Widget (Soon)."
msgstr "React Widget (Soon)."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:44
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:48
msgid "Receive your personal link to share with everyone you care about."
msgstr "Receive your personal link to share with everyone you care about."
@@ -420,7 +424,7 @@ msgstr "Search languages..."
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
-#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:33
+#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:37
msgid "Send, connect, receive and embed everywhere."
msgstr "Send, connect, receive and embed everywhere."
@@ -442,7 +446,7 @@ msgstr "Sign in"
msgid "Sign up"
msgstr "Sign up"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:19
+#: apps/marketing/src/components/(marketing)/carousel.tsx:22
msgid "Signing Process"
msgstr "Signing Process"
@@ -452,11 +456,11 @@ msgstr "Signing Process"
msgid "Signup Now"
msgstr "Signup Now"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:85
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:89
msgid "Smart."
msgstr "Smart."
-#: apps/marketing/src/components/(marketing)/hero.tsx:128
+#: apps/marketing/src/components/(marketing)/hero.tsx:132
msgid "Star on GitHub"
msgstr "Star on GitHub"
@@ -482,12 +486,12 @@ msgstr "Team"
msgid "Team Inbox"
msgstr "Team Inbox"
-#: apps/marketing/src/components/(marketing)/carousel.tsx:25
+#: apps/marketing/src/components/(marketing)/carousel.tsx:28
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:162
msgid "Teams"
msgstr "Teams"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:79
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:83
msgid "Template Store (Soon)."
msgstr "Template Store (Soon)."
@@ -523,12 +527,12 @@ msgstr "Total Funding Raised"
msgid "Total Users"
msgstr "Total Users"
-#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:27
+#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:31
msgid "Truly your own."
msgstr "Truly your own."
#: apps/marketing/src/components/(marketing)/callout.tsx:27
-#: apps/marketing/src/components/(marketing)/hero.tsx:119
+#: apps/marketing/src/components/(marketing)/hero.tsx:123
msgid "Try our Free Plan"
msgstr "Try our Free Plan"
@@ -561,7 +565,7 @@ msgstr "We are happy to assist you at <0>support@documenso.com0> or <1>in our
msgid "What is the difference between the plans?"
msgstr "What is the difference between the plans?"
-#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:43
+#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:47
msgid "When it comes to sending or receiving a contract, you can count on lightning-fast speeds."
msgstr "When it comes to sending or receiving a contract, you can count on lightning-fast speeds."
@@ -589,6 +593,6 @@ msgstr "Yes! Documenso is offered under the GNU AGPL V3 open source license. Thi
msgid "You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs."
msgstr "You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs."
-#: apps/marketing/src/components/(marketing)/carousel.tsx:258
+#: apps/marketing/src/components/(marketing)/carousel.tsx:265
msgid "Your browser does not support the video tag."
msgstr "Your browser does not support the video tag."
diff --git a/packages/lib/types/document-audit-logs.ts b/packages/lib/types/document-audit-logs.ts
index 678438299..a3b62a083 100644
--- a/packages/lib/types/document-audit-logs.ts
+++ b/packages/lib/types/document-audit-logs.ts
@@ -244,6 +244,10 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
// Organised into union to allow us to extend each field if required.
field: z.union([
+ z.object({
+ type: z.literal(FieldType.INITIALS),
+ data: z.string(),
+ }),
z.object({
type: z.literal(FieldType.EMAIL),
data: z.string(),
diff --git a/packages/lib/utils/is-valid-redirect-url.ts b/packages/lib/utils/is-valid-redirect-url.ts
new file mode 100644
index 000000000..e89818ac2
--- /dev/null
+++ b/packages/lib/utils/is-valid-redirect-url.ts
@@ -0,0 +1,16 @@
+const ALLOWED_PROTOCOLS = ['http', 'https'];
+
+export const isValidRedirectUrl = (value: string) => {
+ try {
+ const url = new URL(value);
+
+ console.log({ protocol: url.protocol });
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol.slice(0, -1).toLowerCase())) {
+ return false;
+ }
+
+ return true;
+ } catch {
+ return false;
+ }
+};
diff --git a/packages/lib/utils/slugify.ts b/packages/lib/utils/slugify.ts
new file mode 100644
index 000000000..e8f03f3ce
--- /dev/null
+++ b/packages/lib/utils/slugify.ts
@@ -0,0 +1,3 @@
+export * from '@sindresorhus/slugify';
+
+export { default as slugify } from '@sindresorhus/slugify';
diff --git a/packages/prisma/generated/types.ts b/packages/prisma/generated/types.ts
deleted file mode 100644
index 21144bd84..000000000
--- a/packages/prisma/generated/types.ts
+++ /dev/null
@@ -1,481 +0,0 @@
-import type { ColumnType } from 'kysely';
-
-export type Generated = T extends ColumnType
- ? ColumnType
- : ColumnType;
-export type Timestamp = ColumnType;
-
-export const IdentityProvider = {
- DOCUMENSO: 'DOCUMENSO',
- GOOGLE: 'GOOGLE',
- OIDC: 'OIDC',
-} as const;
-export type IdentityProvider = (typeof IdentityProvider)[keyof typeof IdentityProvider];
-export const Role = {
- ADMIN: 'ADMIN',
- USER: 'USER',
-} as const;
-export type Role = (typeof Role)[keyof typeof Role];
-export const UserSecurityAuditLogType = {
- ACCOUNT_PROFILE_UPDATE: 'ACCOUNT_PROFILE_UPDATE',
- ACCOUNT_SSO_LINK: 'ACCOUNT_SSO_LINK',
- AUTH_2FA_DISABLE: 'AUTH_2FA_DISABLE',
- AUTH_2FA_ENABLE: 'AUTH_2FA_ENABLE',
- PASSKEY_CREATED: 'PASSKEY_CREATED',
- PASSKEY_DELETED: 'PASSKEY_DELETED',
- PASSKEY_UPDATED: 'PASSKEY_UPDATED',
- PASSWORD_RESET: 'PASSWORD_RESET',
- PASSWORD_UPDATE: 'PASSWORD_UPDATE',
- SIGN_OUT: 'SIGN_OUT',
- SIGN_IN: 'SIGN_IN',
- SIGN_IN_FAIL: 'SIGN_IN_FAIL',
- SIGN_IN_2FA_FAIL: 'SIGN_IN_2FA_FAIL',
- SIGN_IN_PASSKEY_FAIL: 'SIGN_IN_PASSKEY_FAIL',
-} as const;
-export type UserSecurityAuditLogType =
- (typeof UserSecurityAuditLogType)[keyof typeof UserSecurityAuditLogType];
-export const WebhookTriggerEvents = {
- DOCUMENT_CREATED: 'DOCUMENT_CREATED',
- DOCUMENT_SENT: 'DOCUMENT_SENT',
- DOCUMENT_OPENED: 'DOCUMENT_OPENED',
- DOCUMENT_SIGNED: 'DOCUMENT_SIGNED',
- DOCUMENT_COMPLETED: 'DOCUMENT_COMPLETED',
-} as const;
-export type WebhookTriggerEvents = (typeof WebhookTriggerEvents)[keyof typeof WebhookTriggerEvents];
-export const WebhookCallStatus = {
- SUCCESS: 'SUCCESS',
- FAILED: 'FAILED',
-} as const;
-export type WebhookCallStatus = (typeof WebhookCallStatus)[keyof typeof WebhookCallStatus];
-export const ApiTokenAlgorithm = {
- SHA512: 'SHA512',
-} as const;
-export type ApiTokenAlgorithm = (typeof ApiTokenAlgorithm)[keyof typeof ApiTokenAlgorithm];
-export const SubscriptionStatus = {
- ACTIVE: 'ACTIVE',
- PAST_DUE: 'PAST_DUE',
- INACTIVE: 'INACTIVE',
-} as const;
-export type SubscriptionStatus = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
-export const DocumentStatus = {
- DRAFT: 'DRAFT',
- PENDING: 'PENDING',
- COMPLETED: 'COMPLETED',
-} as const;
-export type DocumentStatus = (typeof DocumentStatus)[keyof typeof DocumentStatus];
-export const DocumentSource = {
- DOCUMENT: 'DOCUMENT',
- TEMPLATE: 'TEMPLATE',
- TEMPLATE_DIRECT_LINK: 'TEMPLATE_DIRECT_LINK',
-} as const;
-export type DocumentSource = (typeof DocumentSource)[keyof typeof DocumentSource];
-export const DocumentDataType = {
- S3_PATH: 'S3_PATH',
- BYTES: 'BYTES',
- BYTES_64: 'BYTES_64',
-} as const;
-export type DocumentDataType = (typeof DocumentDataType)[keyof typeof DocumentDataType];
-export const ReadStatus = {
- NOT_OPENED: 'NOT_OPENED',
- OPENED: 'OPENED',
-} as const;
-export type ReadStatus = (typeof ReadStatus)[keyof typeof ReadStatus];
-export const SendStatus = {
- NOT_SENT: 'NOT_SENT',
- SENT: 'SENT',
-} as const;
-export type SendStatus = (typeof SendStatus)[keyof typeof SendStatus];
-export const SigningStatus = {
- NOT_SIGNED: 'NOT_SIGNED',
- SIGNED: 'SIGNED',
-} as const;
-export type SigningStatus = (typeof SigningStatus)[keyof typeof SigningStatus];
-export const RecipientRole = {
- CC: 'CC',
- SIGNER: 'SIGNER',
- VIEWER: 'VIEWER',
- APPROVER: 'APPROVER',
-} as const;
-export type RecipientRole = (typeof RecipientRole)[keyof typeof RecipientRole];
-export const FieldType = {
- SIGNATURE: 'SIGNATURE',
- FREE_SIGNATURE: 'FREE_SIGNATURE',
- NAME: 'NAME',
- EMAIL: 'EMAIL',
- DATE: 'DATE',
- TEXT: 'TEXT',
- NUMBER: 'NUMBER',
- RADIO: 'RADIO',
- CHECKBOX: 'CHECKBOX',
- DROPDOWN: 'DROPDOWN',
-} as const;
-export type FieldType = (typeof FieldType)[keyof typeof FieldType];
-export const TeamMemberRole = {
- ADMIN: 'ADMIN',
- MANAGER: 'MANAGER',
- MEMBER: 'MEMBER',
-} as const;
-export type TeamMemberRole = (typeof TeamMemberRole)[keyof typeof TeamMemberRole];
-export const TeamMemberInviteStatus = {
- ACCEPTED: 'ACCEPTED',
- PENDING: 'PENDING',
-} as const;
-export type TeamMemberInviteStatus =
- (typeof TeamMemberInviteStatus)[keyof typeof TeamMemberInviteStatus];
-export const TemplateType = {
- PUBLIC: 'PUBLIC',
- PRIVATE: 'PRIVATE',
-} as const;
-export type TemplateType = (typeof TemplateType)[keyof typeof TemplateType];
-export type Account = {
- id: string;
- userId: number;
- type: string;
- provider: string;
- providerAccountId: string;
- refresh_token: string | null;
- access_token: string | null;
- expires_at: number | null;
- created_at: number | null;
- ext_expires_in: number | null;
- token_type: string | null;
- scope: string | null;
- id_token: string | null;
- session_state: string | null;
-};
-export type AnonymousVerificationToken = {
- id: string;
- token: string;
- expiresAt: Timestamp;
- createdAt: Generated;
-};
-export type ApiToken = {
- id: Generated;
- name: string;
- token: string;
- algorithm: Generated;
- expires: Timestamp | null;
- createdAt: Generated;
- userId: number | null;
- teamId: number | null;
-};
-export type Document = {
- id: Generated;
- userId: number;
- authOptions: unknown | null;
- formValues: unknown | null;
- title: string;
- status: Generated;
- documentDataId: string;
- createdAt: Generated;
- updatedAt: Generated;
- completedAt: Timestamp | null;
- deletedAt: Timestamp | null;
- teamId: number | null;
- templateId: number | null;
- source: DocumentSource;
-};
-export type DocumentAuditLog = {
- id: string;
- documentId: number;
- createdAt: Generated;
- type: string;
- data: unknown;
- name: string | null;
- email: string | null;
- userId: number | null;
- userAgent: string | null;
- ipAddress: string | null;
-};
-export type DocumentData = {
- id: string;
- type: DocumentDataType;
- data: string;
- initialData: string;
-};
-export type DocumentMeta = {
- id: string;
- subject: string | null;
- message: string | null;
- timezone: Generated;
- password: string | null;
- dateFormat: Generated;
- documentId: number;
- redirectUrl: string | null;
-};
-export type DocumentShareLink = {
- id: Generated;
- email: string;
- slug: string;
- documentId: number;
- createdAt: Generated;
- updatedAt: Timestamp;
-};
-export type Field = {
- id: Generated;
- secondaryId: string;
- documentId: number | null;
- templateId: number | null;
- recipientId: number;
- type: FieldType;
- page: number;
- positionX: Generated;
- positionY: Generated;
- width: Generated;
- height: Generated;
- customText: string;
- inserted: boolean;
- fieldMeta: unknown | null;
-};
-export type Passkey = {
- id: string;
- userId: number;
- name: string;
- createdAt: Generated;
- updatedAt: Generated;
- lastUsedAt: Timestamp | null;
- credentialId: Buffer;
- credentialPublicKey: Buffer;
- counter: string;
- credentialDeviceType: string;
- credentialBackedUp: boolean;
- transports: string[];
-};
-export type PasswordResetToken = {
- id: Generated;
- token: string;
- createdAt: Generated;
- expiry: Timestamp;
- userId: number;
-};
-export type Recipient = {
- id: Generated;
- documentId: number | null;
- templateId: number | null;
- email: string;
- name: Generated;
- token: string;
- documentDeletedAt: Timestamp | null;
- expired: Timestamp | null;
- signedAt: Timestamp | null;
- authOptions: unknown | null;
- role: Generated;
- readStatus: Generated;
- signingStatus: Generated;
- sendStatus: Generated;
-};
-export type Session = {
- id: string;
- sessionToken: string;
- userId: number;
- expires: Timestamp;
-};
-export type Signature = {
- id: Generated;
- created: Generated;
- recipientId: number;
- fieldId: number;
- signatureImageAsBase64: string | null;
- typedSignature: string | null;
-};
-export type SiteSettings = {
- id: string;
- enabled: Generated;
- data: unknown;
- lastModifiedByUserId: number | null;
- lastModifiedAt: Generated;
-};
-export type Subscription = {
- id: Generated;
- status: Generated;
- planId: string;
- priceId: string;
- periodEnd: Timestamp | null;
- userId: number | null;
- teamId: number | null;
- createdAt: Generated;
- updatedAt: Timestamp;
- cancelAtPeriodEnd: Generated;
-};
-export type Team = {
- id: Generated;
- name: string;
- url: string;
- createdAt: Generated;
- customerId: string | null;
- ownerUserId: number;
-};
-export type TeamEmail = {
- teamId: number;
- createdAt: Generated;
- name: string;
- email: string;
-};
-export type TeamEmailVerification = {
- teamId: number;
- name: string;
- email: string;
- token: string;
- expiresAt: Timestamp;
- createdAt: Generated;
-};
-export type TeamMember = {
- id: Generated;
- teamId: number;
- createdAt: Generated;
- role: TeamMemberRole;
- userId: number;
-};
-export type TeamMemberInvite = {
- id: Generated;
- teamId: number;
- createdAt: Generated;
- email: string;
- status: Generated;
- role: TeamMemberRole;
- token: string;
-};
-export type TeamPending = {
- id: Generated;
- name: string;
- url: string;
- createdAt: Generated;
- customerId: string;
- ownerUserId: number;
-};
-export type TeamTransferVerification = {
- teamId: number;
- userId: number;
- name: string;
- email: string;
- token: string;
- expiresAt: Timestamp;
- createdAt: Generated;
- clearPaymentMethods: Generated;
-};
-export type Template = {
- id: Generated;
- type: Generated;
- title: string;
- userId: number;
- teamId: number | null;
- authOptions: unknown | null;
- templateDocumentDataId: string;
- createdAt: Generated;
- updatedAt: Generated;
-};
-export type TemplateDirectLink = {
- id: string;
- templateId: number;
- token: string;
- createdAt: Generated;
- enabled: boolean;
- directTemplateRecipientId: number;
-};
-export type TemplateMeta = {
- id: string;
- subject: string | null;
- message: string | null;
- timezone: Generated;
- password: string | null;
- dateFormat: Generated;
- templateId: number;
- redirectUrl: string | null;
-};
-export type User = {
- id: Generated;
- name: string | null;
- customerId: string | null;
- email: string;
- emailVerified: Timestamp | null;
- password: string | null;
- source: string | null;
- signature: string | null;
- createdAt: Generated;
- updatedAt: Generated;
- lastSignedIn: Generated;
- roles: Generated;
- identityProvider: Generated;
- twoFactorSecret: string | null;
- twoFactorEnabled: Generated;
- twoFactorBackupCodes: string | null;
- url: string | null;
-};
-export type UserProfile = {
- id: number;
- bio: string | null;
-};
-export type UserSecurityAuditLog = {
- id: Generated;
- userId: number;
- createdAt: Generated;
- type: UserSecurityAuditLogType;
- userAgent: string | null;
- ipAddress: string | null;
-};
-export type VerificationToken = {
- id: Generated;
- secondaryId: string;
- identifier: string;
- token: string;
- expires: Timestamp;
- createdAt: Generated;
- userId: number;
-};
-export type Webhook = {
- id: string;
- webhookUrl: string;
- eventTriggers: WebhookTriggerEvents[];
- secret: string | null;
- enabled: Generated;
- createdAt: Generated;
- updatedAt: Generated;
- userId: number;
- teamId: number | null;
-};
-export type WebhookCall = {
- id: string;
- status: WebhookCallStatus;
- url: string;
- event: WebhookTriggerEvents;
- requestBody: unknown;
- responseCode: number;
- responseHeaders: unknown | null;
- responseBody: unknown | null;
- createdAt: Generated;
- webhookId: string;
-};
-export type DB = {
- Account: Account;
- AnonymousVerificationToken: AnonymousVerificationToken;
- ApiToken: ApiToken;
- Document: Document;
- DocumentAuditLog: DocumentAuditLog;
- DocumentData: DocumentData;
- DocumentMeta: DocumentMeta;
- DocumentShareLink: DocumentShareLink;
- Field: Field;
- Passkey: Passkey;
- PasswordResetToken: PasswordResetToken;
- Recipient: Recipient;
- Session: Session;
- Signature: Signature;
- SiteSettings: SiteSettings;
- Subscription: Subscription;
- Team: Team;
- TeamEmail: TeamEmail;
- TeamEmailVerification: TeamEmailVerification;
- TeamMember: TeamMember;
- TeamMemberInvite: TeamMemberInvite;
- TeamPending: TeamPending;
- TeamTransferVerification: TeamTransferVerification;
- Template: Template;
- TemplateDirectLink: TemplateDirectLink;
- TemplateMeta: TemplateMeta;
- User: User;
- UserProfile: UserProfile;
- UserSecurityAuditLog: UserSecurityAuditLog;
- VerificationToken: VerificationToken;
- Webhook: Webhook;
- WebhookCall: WebhookCall;
-};
diff --git a/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql b/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql
new file mode 100644
index 000000000..b4c827ae7
--- /dev/null
+++ b/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql
@@ -0,0 +1,2 @@
+-- AlterEnum
+ALTER TYPE "FieldType" ADD VALUE 'INITIALS';
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 44cf9e157..9d8860f44 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -409,6 +409,7 @@ model Recipient {
enum FieldType {
SIGNATURE
FREE_SIGNATURE
+ INITIALS
NAME
EMAIL
DATE
diff --git a/packages/prisma/seed/initial-seed.ts b/packages/prisma/seed/initial-seed.ts
index 66c944c9b..38b340a79 100644
--- a/packages/prisma/seed/initial-seed.ts
+++ b/packages/prisma/seed/initial-seed.ts
@@ -4,7 +4,7 @@ import path from 'node:path';
import { hashSync } from '@documenso/lib/server-only/auth/hash';
import { prisma } from '..';
-import { DocumentDataType, DocumentSource, Role } from '../client';
+import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
export const seedDatabase = async () => {
const examplePdf = fs
@@ -67,4 +67,64 @@ export const seedDatabase = async () => {
},
},
});
+
+ const testUsers = [
+ 'test@documenso.com',
+ 'test2@documenso.com',
+ 'test3@documenso.com',
+ 'test4@documenso.com',
+ ];
+
+ const createdUsers = [];
+
+ for (const email of testUsers) {
+ const testUser = await prisma.user.upsert({
+ where: {
+ email: email,
+ },
+ create: {
+ name: 'Test User',
+ email: email,
+ emailVerified: new Date(),
+ password: hashSync('password'),
+ roles: [Role.USER],
+ },
+ update: {},
+ });
+
+ createdUsers.push(testUser);
+ }
+
+ const team1 = await prisma.team.create({
+ data: {
+ name: 'Team 1',
+ url: 'team1',
+ ownerUserId: createdUsers[0].id,
+ },
+ });
+
+ const team2 = await prisma.team.create({
+ data: {
+ name: 'Team 2',
+ url: 'team2',
+ ownerUserId: createdUsers[1].id,
+ },
+ });
+
+ for (const team of [team1, team2]) {
+ await prisma.teamMember.createMany({
+ data: [
+ {
+ teamId: team.id,
+ userId: createdUsers[1].id,
+ role: TeamMemberRole.ADMIN,
+ },
+ {
+ teamId: team.id,
+ userId: createdUsers[2].id,
+ role: TeamMemberRole.MEMBER,
+ },
+ ],
+ });
+ }
};
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index b2e07d5cf..0ae8b35b4 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -30,6 +30,7 @@ import {
ZCreateDocumentMutationSchema,
ZDeleteDraftDocumentMutationSchema as ZDeleteDocumentMutationSchema,
ZDownloadAuditLogsMutationSchema,
+ ZDownloadCertificateMutationSchema,
ZFindDocumentAuditLogsQuerySchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
@@ -441,7 +442,14 @@ export const documentRouter = router({
id: documentId,
userId: ctx.user.id,
teamId,
- });
+ }).catch(() => null);
+
+ if (!document || (teamId && document.teamId !== teamId)) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'You do not have access to this document.',
+ });
+ }
const encrypted = encryptSecondaryData({
data: document.id.toString(),
@@ -463,7 +471,7 @@ export const documentRouter = router({
}),
downloadCertificate: authenticatedProcedure
- .input(ZDownloadAuditLogsMutationSchema)
+ .input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId } = input;
diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts
index 56af4644e..41f0cfb02 100644
--- a/packages/trpc/server/document-router/schema.ts
+++ b/packages/trpc/server/document-router/schema.ts
@@ -1,11 +1,11 @@
import { z } from 'zod';
-import { URL_REGEX } from '@documenso/lib/constants/url-regex';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
+import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
@@ -65,8 +65,9 @@ export const ZSetSettingsForDocumentMutationSchema = z.object({
redirectUrl: z
.string()
.optional()
- .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
- message: 'Please enter a valid URL',
+ .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
+ message:
+ 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
}),
});
@@ -131,8 +132,9 @@ export const ZSendDocumentMutationSchema = z.object({
redirectUrl: z
.string()
.optional()
- .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
- message: 'Please enter a valid URL',
+ .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
+ message:
+ 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
}),
});
@@ -175,6 +177,11 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
teamId: z.number().optional(),
});
+export const ZDownloadCertificateMutationSchema = z.object({
+ documentId: z.number(),
+ teamId: z.number().optional(),
+});
+
export const ZMoveDocumentsToTeamSchema = z.object({
documentId: z.number(),
teamId: z.number(),
diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts
index 2f2567d63..8513d52df 100644
--- a/packages/trpc/server/template-router/router.ts
+++ b/packages/trpc/server/template-router/router.ts
@@ -66,6 +66,7 @@ export const templateRouter = router({
directRecipientName,
directRecipientEmail,
directTemplateToken,
+ directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
} = input;
@@ -76,6 +77,7 @@ export const templateRouter = router({
directRecipientName,
directRecipientEmail,
directTemplateToken,
+ directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index 29f815f35..2d98f83d0 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -1,11 +1,11 @@
import { z } from 'zod';
-import { URL_REGEX } from '@documenso/lib/constants/url-regex';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
+import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { TemplateType } from '@documenso/prisma/client';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
@@ -20,6 +20,7 @@ export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
directRecipientName: z.string().optional(),
directRecipientEmail: z.string().email(),
directTemplateToken: z.string().min(1),
+ directTemplateExternalId: z.string().optional(),
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
templateUpdatedAt: z.date(),
});
@@ -96,8 +97,9 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
redirectUrl: z
.string()
.optional()
- .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
- message: 'Please enter a valid URL',
+ .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
+ message:
+ 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
})
.optional(),
diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts
index 58447155b..91e6edd0d 100644
--- a/packages/tsconfig/process-env.d.ts
+++ b/packages/tsconfig/process-env.d.ts
@@ -9,6 +9,9 @@ declare namespace NodeJS {
NEXT_PRIVATE_OIDC_WELL_KNOWN?: string;
NEXT_PRIVATE_OIDC_CLIENT_ID?: string;
NEXT_PRIVATE_OIDC_CLIENT_SECRET?: string;
+ NEXT_PRIVATE_OIDC_PROVIDER_LABEL?: string;
+ NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
+ NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PRIVATE_ENCRYPTION_KEY: string;
@@ -80,6 +83,9 @@ declare namespace NodeJS {
/**
* Inngest environment variables
*/
+ INNGEST_EVENT_KEY?: string;
+ INNGEST_SIGNING_KEY?: string;
+ NEXT_PRIVATE_INNGEST_APP_ID?: string;
NEXT_PRIVATE_INNGEST_EVENT_KEY?: string;
/**
diff --git a/packages/ui/components/field/field.tsx b/packages/ui/components/field/field.tsx
index d6553947c..ac67171de 100644
--- a/packages/ui/components/field/field.tsx
+++ b/packages/ui/components/field/field.tsx
@@ -84,7 +84,7 @@ export function FieldContainerPortal({
left: `${coords.x}px`,
// height: `${coords.height}px`,
// width: `${coords.width}px`,
- ...((!isCheckboxOrRadioField) && {
+ ...(!isCheckboxOrRadioField && {
height: `${coords.height}px`,
width: `${coords.width}px`,
}),
diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx
index 8fe8d66ed..148cadc3c 100644
--- a/packages/ui/primitives/document-flow/add-fields.tsx
+++ b/packages/ui/primitives/document-flow/add-fields.tsx
@@ -10,6 +10,7 @@ import {
CheckSquare,
ChevronDown,
ChevronsUpDown,
+ Contact,
Disc,
Hash,
Info,
@@ -18,6 +19,7 @@ import {
User,
} from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
+import { useHotkeys } from 'react-hotkeys-hook';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
@@ -39,6 +41,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
import { useStep } from '../stepper';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
+import { useToast } from '../use-toast';
import type { TAddFieldsFormSchema } from './add-fields.types';
import {
DocumentFlowFormContainerActions,
@@ -102,6 +105,8 @@ export const AddFieldsFormPartial = ({
isDocumentPdfLoaded,
teamId,
}: AddFieldsFormProps) => {
+ const { toast } = useToast();
+
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
@@ -135,7 +140,12 @@ export const AddFieldsFormPartial = ({
},
});
+ useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
+ useHotkeys(['ctrl+v', 'meta+v'], (evt) => onFieldPaste(evt));
+ useHotkeys(['ctrl+d', 'meta+d'], (evt) => onFieldCopy(evt, { duplicate: true }));
+
const onFormSubmit = handleSubmit(onSubmit);
+
const handleSavedFieldSettings = (fieldState: FieldMeta) => {
const initialValues = getValues();
@@ -168,6 +178,12 @@ export const AddFieldsFormPartial = ({
const [selectedField, setSelectedField] = useState(null);
const [selectedSigner, setSelectedSigner] = useState(null);
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
+ const [lastActiveField, setLastActiveField] = useState(
+ null,
+ );
+ const [fieldClipboard, setFieldClipboard] = useState(
+ null,
+ );
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
const selectedSignerStyles = useSignerColors(
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
@@ -280,7 +296,7 @@ export const AddFieldsFormPartial = ({
pageX -= fieldPageWidth / 2;
pageY -= fieldPageHeight / 2;
- append({
+ const field = {
formId: nanoid(12),
type: selectedField,
pageNumber,
@@ -290,7 +306,9 @@ export const AddFieldsFormPartial = ({
pageHeight: fieldPageHeight,
signerEmail: selectedSigner.email,
fieldMeta: undefined,
- });
+ };
+
+ append(field);
setIsFieldWithinBounds(false);
setSelectedField(null);
@@ -351,6 +369,57 @@ export const AddFieldsFormPartial = ({
[getFieldPosition, localFields, update],
);
+ const onFieldCopy = useCallback(
+ (event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
+ const { duplicate = false } = options ?? {};
+
+ if (lastActiveField) {
+ event?.preventDefault();
+
+ if (!duplicate) {
+ setFieldClipboard(lastActiveField);
+
+ toast({
+ title: 'Copied field',
+ description: 'Copied field to clipboard',
+ });
+
+ return;
+ }
+
+ const newField: TAddFieldsFormSchema['fields'][0] = {
+ ...structuredClone(lastActiveField),
+ formId: nanoid(12),
+ signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
+ pageX: lastActiveField.pageX + 3,
+ pageY: lastActiveField.pageY + 3,
+ };
+
+ append(newField);
+ }
+ },
+ [append, lastActiveField, selectedSigner?.email, toast],
+ );
+
+ const onFieldPaste = useCallback(
+ (event: KeyboardEvent) => {
+ if (fieldClipboard) {
+ event.preventDefault();
+
+ const copiedField = structuredClone(fieldClipboard);
+
+ append({
+ ...copiedField,
+ formId: nanoid(12),
+ signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
+ pageX: copiedField.pageX + 3,
+ pageY: copiedField.pageY + 3,
+ });
+ }
+ },
+ [append, fieldClipboard, selectedSigner?.email],
+ );
+
useEffect(() => {
if (selectedField) {
window.addEventListener('mousemove', onMouseMove);
@@ -457,11 +526,13 @@ export const AddFieldsFormPartial = ({
{selectedField && (