Compare commits

...

477 Commits

Author SHA1 Message Date
824f4a070a feat: demo 2024-03-27 20:31:08 +08:00
2c8d11c37d fix: demo 2024-03-26 22:42:38 +08:00
006b732edb fix: update document flow fetch logic (#1039)
## Description

**Fixes issues with mismatching state between document steps.**

For example, editing a recipient and proceeding to the next step may not
display the updated recipient. And going back will display the old
recipient instead of the updated values.

**This PR also improves mutation and query speeds by adding logic to
bypass query invalidation.**

```ts
export const trpc = createTRPCReact<AppRouter>({
  unstable_overrides: {
    useMutation: {
      async onSuccess(opts) {
        await opts.originalFn();

        // This forces mutations to wait for all the queries on the page to reload, and in
        // this case one of the queries is `searchDocument` for the command overlay, which
        // on average takes ~500ms. This means that every single mutation must wait for this.
        await opts.queryClient.invalidateQueries(); 
      },
    },
  },
});
```

I've added workarounds to allow us to bypass things such as batching and
invalidating queries. But I think we should instead remove this and
update all the mutations where a query is required for a more optimised
system.

## Example benchmarks

Using stg-app vs this preview there's an average 50% speed increase
across mutations.

**Set signer step:**
Average old speed: ~1100ms
Average new speed: ~550ms

**Set recipient step:**
Average old speed: ~1200ms
Average new speed: ~600ms

**Set fields step:**
Average old speed: ~1200ms
Average new speed: ~600ms

## Related Issue

This will resolve #470

## Changes Made

- Added ability to skip batch queries
- Added a state to store the required document data.
- Refetch the data between steps if/when required
- Optimise mutations and queries

## Checklist

- [X] I have tested these changes locally and they work as expected.
- [X] I have followed the project's coding style guidelines.

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-03-26 21:12:41 +08:00
5210fe2963 feat: add passkeys (#989)
## Description

Add support to login with passkeys.

Passkeys can be added via the user security settings page.

Note: Currently left out adding the type of authentication method for
the 'user security audit logs' because we're using the `signIn`
next-auth event which doesn't appear to provide the context. Will look
into it at another time.

## Changes Made

- Add passkeys to login
- Add passkeys feature flag
- Add page to manage passkeys
- Add audit logs relating to passkeys
- Updated prisma schema to support passkeys & anonymous verification
tokens

## Testing Performed

To be done.

MacOS:
- Safari  
- Chrome  
- Firefox 

Windows:
- Chrome [Untested] 
- Firefox [Untested]

Linux:
- Chrome [Untested]
- Firefox [Untested]

iOS:
- Safari 

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [X] I have tested these changes locally and they work as expected.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced Passkey authentication, including creation, sign-in, and
management of passkeys.
- Added a Passkeys section in Security Settings for managing user
passkeys.
- Implemented UI updates for Passkey authentication, including a new
dialog for creating passkeys and a data table for managing them.
- Enhanced security settings with server-side feature flags to
conditionally display new security features.
- **Bug Fixes**
	- Improved UI consistency in the Settings Security Activity Page.
- Updated button styling in the 2FA Recovery Codes component for better
visibility.
- **Refactor**
- Streamlined authentication options to include WebAuthn credentials
provider.
- **Chores**
- Updated database schema to support passkeys and related functionality.
	- Added new audit log types for passkey-related activities.
- Enhanced server-only authentication utilities for passkey registration
and management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
3eddfcc805 chore: add test for multiple recipient (#1045) 2024-03-25 13:57:32 +07:00
43400c07de feat: remove 2FA password requirement (#1053) 2024-03-25 11:34:50 +08:00
715c14a6ae chore: set default PR template (#1055)
## Details

Currently there's no way to have a proper PR template selector. Since we
rarely use the E2E template, I've opted to move the generic PR template
to be the default template.
2024-03-25 12:57:55 +11:00
606966b357 feat: add sticky behavior to pricing options container (#1042)
Fixes #894
2024-03-25 12:55:33 +11:00
24852f3c68 feat: modify periods menu horizontal alignment on mobile 2024-03-24 19:07:26 -05:00
48ee977617 Merge branch 'main' of https://github.com/JuanSebastianM/documenso into feat/894-pricing-section-ux-improvement 2024-03-24 18:35:08 -05:00
fc34f1c045 chore: set default PR template 2024-03-24 17:01:21 +08:00
1725af71b6 chore: add test to update username (#1049) 2024-03-23 13:41:12 +08:00
007687bdee fix: monthly count instead of cummulative (#1047) 2024-03-22 09:19:53 +01:00
f5a1d9a625 fix: monthly count instead of cummulative 2024-03-22 08:08:37 +00:00
72fd1eead2 fix: use correct date format (#1046)
### Before
![CleanShot 2024-03-21 at 17 23
09@2x](https://github.com/documenso/documenso/assets/55143799/d1cd22ca-399b-4ba2-bb82-b4dc869605c8)

### After
![CleanShot 2024-03-21 at 17 23
17@2x](https://github.com/documenso/documenso/assets/55143799/cdb814ea-01be-4bcb-9bad-df41030f320a)
2024-03-22 14:25:12 +08:00
5289ae2fbc Merge branch 'main' into test/sign-redirect-url 2024-03-21 16:36:48 +00:00
c4c6e67249 chore: prod playwright config 2024-03-21 16:36:10 +00:00
5377d27c6a chore: test for redirect url 2024-03-21 16:28:42 +00:00
1cd7dd236b chore: test signing a document 2024-03-21 16:15:29 +00:00
67beb8225c chore: minor update to open page (#1043) 2024-03-21 13:17:47 +01:00
94198e7584 chore: text 2024-03-21 13:16:17 +01:00
facafe0997 feat: place card titles in the box 2024-03-21 01:44:32 +00:00
8c1686f113 feat: add total signed documents 2024-03-21 01:25:23 +00:00
a8752098f6 fix: invalid datetime on graph 2024-03-21 00:48:49 +00:00
3e15b5d745 feat: add sticky behavior to pricing options container 2024-03-20 15:05:17 -05:00
0dfdf36bda feat: restructure open page (#1040) 2024-03-20 12:48:44 +01:00
574cd176c2 chore: update copy to have more swag 2024-03-20 12:34:03 +01:00
48858cfdd0 chore: restructure open page 2024-03-20 10:31:19 +00:00
2facc0e331 feat: add completed documents per month graph 2024-03-20 10:17:31 +00:00
e7071f1f5a fix: username overflow issue (#1036)
Before:-
![Screenshot 2024-03-19
000255](https://github.com/documenso/documenso/assets/81948346/169bf207-3e0f-419c-bf72-ff25347c6814)

Now:-

![Screenshot 2024-03-18
235807](https://github.com/documenso/documenso/assets/81948346/17cd30d3-d6ef-4e31-b174-142c06491c4a)
2024-03-19 14:04:19 +11:00
b95f7176e2 fix: username overflow issue 2024-03-18 18:25:04 +00:00
6d754acfcd fix: disable edit signer inputs (#1035)
## Description

Update the add signer form to disable the signers when required.

I assume the actual issue is that `{...field}` was spreading a disabled
prop which was overriding our one.

## Changes Made

- Use fieldset to disable inputs
- Manually disable select since fieldset doesn't work for that select
for some reason
2024-03-18 19:59:39 +08:00
796456929e feat: improve lint-staged performance (#1006)
Right now, the eslint command runs separately for each staged file. This
PR aims to change that by running just one eslint command for all the
files that have been changed.
2024-03-18 15:18:20 +11:00
de9c9f4aab chore: tidying 2024-03-18 02:44:39 +00:00
b972056c8f Merge branch 'main' into improve-lint 2024-03-18 13:31:43 +11:00
69871e7d39 fix: update codespaces on create script (#1034)
## Description

Updates the `on-create.sh` script to use our `dx` command resolving
issues where it couldn't find the docker compose file due to changes
that have happened since publishing the Documenso docker image.

## Related Issue

Resolves #1026

## Test Details

N/A

## Checklist

- [x] I have written the new test and ensured it works as intended.
- [x] I have added necessary documentation to explain the purpose of the
test.
- [x] I have followed the project's testing guidelines and coding style.
- [x] I have addressed any review feedback from previous submissions, if
applicable.

## Additional Notes

N/A
2024-03-18 13:29:47 +11:00
9cfd769356 chore: use rust based cms signing (#1030) 2024-03-18 12:33:10 +11:00
bd20c5499f fix: update codespaces on create script 2024-03-18 01:28:26 +00:00
3c6cc7fd46 Merge branch 'main' into chore/add-rust-signer 2024-03-18 12:24:59 +11:00
4ac800fa78 feat: added custom dark mode styling for swagger ui (#1022)
**Description:**

Updated the OpenAPI doc dark mode styling here
https://app.documenso.com/api/v1/openapi

**Before:**

<img width="1473" alt="Screenshot 2024-03-13 at 09 48 36"
src="https://github.com/documenso/documenso/assets/23498248/a4fa1fef-699f-40e9-a06d-e513fc786399">

**After:**

<img width="1471" alt="Screenshot 2024-03-13 at 12 40 08"
src="https://github.com/documenso/documenso/assets/23498248/39c606b5-ab8b-4031-9821-a57c8bb80b7d">
2024-03-16 23:37:03 +11:00
3598bd0139 fix: use tailwind for menu switcher ring 2024-03-16 12:18:12 +00:00
d62838f4a0 fix: pagination discrepancy (#1024)
- This PR fixes the pagination discrepancy in the `DataTablePagination`
component.
ref #1021
2024-03-16 22:32:18 +11:00
8de8139b85 chore: send email to document owner (#1031)
**Description:**

This PR sends an email to the document owner once the signing has been
completed
2024-03-16 22:29:28 +11:00
a7594c9b3c fix: update sending logic 2024-03-16 11:06:33 +00:00
f012826b6b chore: send document owner email
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-15 22:26:31 +05:30
38752f95f3 Merge branch 'fix/pagination' of https://github.com/Gautam-Hegde/documenso into fix/pagination 2024-03-15 22:11:06 +05:30
4379b43ad9 chore: tidy code 2024-03-15 22:09:58 +05:30
8859b2779f chore: use rust based cms signing 2024-03-15 22:29:15 +11:00
e29bfbf5e0 chore: updated focus custom css
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-15 09:19:47 +05:30
17c6a4bd55 chore: updated focus state of menu switcher
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-15 09:18:51 +05:30
d6668ad204 Merge branch 'main' of https://github.com/documenso/documenso into feat/swagger-styling 2024-03-15 09:17:01 +05:30
91e1fe5e8f Merge branch 'main' into fix/pagination 2024-03-14 18:41:09 +05:30
fd4d5468cf fix: use gif for readme 2024-03-14 23:52:49 +11:00
d5c4885c67 fix: update signup form to handle password managers better 2024-03-14 12:39:58 +00:00
564f0dd945 fix: avoid opengraph image limit (#1027) 2024-03-14 23:27:19 +11:00
524a7918d5 fix: toss the signature 2024-03-14 10:41:59 +00:00
0db2e6643d fix: final final v2 2024-03-14 10:39:48 +00:00
f5967e28c3 fix: without protection? 2024-03-14 10:02:09 +00:00
4926b6de50 fix: boring sign/verify approach 2024-03-14 09:40:26 +00:00
d6c8a3d32c fix: what happens if we use a dynamic import? 2024-03-14 09:20:01 +00:00
a9bb559568 fix: avoid opengraph image limit 2024-03-14 10:56:46 +02:00
8d1da3df72 Merge branch 'main' of https://github.com/documenso/documenso into feat/swagger-styling 2024-03-14 09:53:48 +05:30
00c71fd66c chore: fixed focus ring
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-14 01:02:48 +05:30
df8d394c28 chore: updated to resolvedTheme
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-14 00:47:54 +05:30
bc3c9424c4 docs: add article about public api (#1005) 2024-03-13 21:23:44 +11:00
6643c4b9fc update author description 2024-03-13 12:00:42 +02:00
ec7b69f1a4 implement feedback 2024-03-13 11:59:12 +02:00
0488442652 fix: pagination discrepancy 2024-03-13 13:45:10 +05:30
cc483016d8 chore: updated styling
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-13 13:24:09 +05:30
025af6e9f4 chore: added eol
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-13 12:41:08 +05:30
e5497efe7c chore: updated dark mode styling
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-13 12:38:56 +05:30
27e7e51789 Merge branch 'main' of https://github.com/documenso/documenso into feat/swagger-styling 2024-03-13 09:56:16 +05:30
52afae331e chore: updated to send email to doc owners
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-13 09:50:37 +05:30
27a69819f9 feat: added custom styling for swagger ui
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-13 09:49:31 +05:30
a178e1d86f feat: add website cta (#1007)
![hover](https://github.com/documenso/documenso/assets/25515812/9132ef8c-f956-4370-8e20-cc01bc196664)

![notover](https://github.com/documenso/documenso/assets/25515812/a12e72f0-84eb-43bd-bb95-828fef8e8819)
2024-03-12 14:17:38 +11:00
1fd29f7e89 feat: require confirmation for user account deletion (#1009)
### This PR adds the necessary user friction in the Delete Dialog,
ensuring that users are intentionally deleting their accounts.

- User must disable 2FA to delete the account.


![2fa](https://github.com/documenso/documenso/assets/85569489/634fd9dd-2aea-4dd8-a231-ade82b71fc7d)

- Explicit user confirmation


![!2FA](https://github.com/documenso/documenso/assets/85569489/11a074b6-7ec7-4568-ba1a-ee884766047b)


fixes #998
2024-03-12 14:15:53 +11:00
d3f4e20f1c fix: update styling and e2e test 2024-03-12 02:57:22 +00:00
3ebeb347c5 chore: updated url regex (#1017)
**Description:**

- Fixes #1016 

**Scenarios Tested:**

- https://info.adikris.in
- https://www.info.adikris.in
- http://www.info.adikris.in
- https://adikris.in
- http://adikris.in
- https://adikris.com.au
2024-03-12 13:40:45 +11:00
f6c2b6c1c5 fix: minor updates 2024-03-12 01:52:16 +00:00
efb90ca5fb chore: use email confirmation 2024-03-11 23:17:11 +05:30
9ac346443d Merge branch 'main' into feat/add-website-cta 2024-03-11 13:06:46 +02:00
f2aa0cd714 Merge branch 'main' into feat/typeInDeletion 2024-03-11 15:20:19 +05:30
bbcb90d8a5 chore: updated url regex
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-03-11 15:00:58 +05:30
c2cf25b138 fix: templates incorrectly linking when in a team 2024-03-11 01:46:19 +00:00
63c23301a9 Merge branch 'main' into feat/typeInDeletion 2024-03-11 12:19:10 +11:00
1a23744d2a fix: table layout shift while changing tabs (#921)
fixes: #875 



https://github.com/documenso/documenso/assets/28510494/083fd87a-ef62-40e6-9696-9c04b4411502
2024-03-11 12:16:19 +11:00
7f31ab1ce3 fix: add scrollbar gutter property 2024-03-11 00:52:56 +00:00
7ac34099ee fix: ensure password input is cleared when 2fa enable dialog is closed (#1014)
Issue found by guatam in Documenso Discord.

https://discord.com/channels/1132216843537485854/1133420505030987857/1215716399822147664

Bug Loom:

https://www.loom.com/share/b7defde91dfb437580c13c7f166f59ff?sid=cbca746f-174a-44ad-997c-cf3a2eef8380

Fix Loom:

https://www.loom.com/share/8748ac47459247a49ddf7d5379e2a0a0?sid=02977122-c150-44e8-9a19-4c8356c298d7
2024-03-11 11:26:35 +11:00
c744482b84 fix: add conditional to useEffects 2024-03-11 10:55:46 +11:00
b71cb66dd3 fix: show close icon on notification in mobile (#996)
As per the requirement on the mobile, the close icon will always be
visible

On the desktop, close icon will be visible on hover.

fixes #965
2024-03-10 22:12:29 +11:00
afe99e5ec9 fix: revert reset changes, reset on open state change instead 2024-03-10 09:36:54 +00:00
6f958b9320 fix: update the dialog cancel to reset 2024-03-10 09:28:08 +00:00
f646fa29d7 fix: ensure password input is cleared when 2fa enable dialog is closed 2024-03-10 07:01:18 +00:00
373e806bd9 fix: eslint config file parseOptions.project path is updated (#1008)
I found out that the problem of slow down is the use of
parseOption.project with multiple tsconfig files which increases usage
of memory consumption quite a lot.

Here is the reference I found to resolve this issue:

[reference
link](https://github.com/typescript-eslint/typescript-eslint/issues/1192)
[Doc
link](https://typescript-eslint.io/getting-started/typed-linting/monorepos/)

Based on this reference I created the solution and tested out locally it
does reduce the eslint time.

#1002
2024-03-10 15:32:59 +11:00
c3c00dfe05 fix: update docker documentation and include cert env vars (#1013)
## Description

Updates the docker documentation and compose files to include key file
related configuration for self hosters. This was missed as the key file
environment variables weren't documented in the `.env.example` file
which was used for creating the docker documentation.

## Related Issue

#1012

## Changes Made

- Updated the docker configuration to include key file environment
variables
- Updated the .env.example to include key file env vars
- Updated the compose images to use volumes for key files

## Testing Performed

- I have used the containers with the updated values and confirmed that
they work as expected

## Checklist

- [x] I have tested these changes locally and they work as expected.
- [x] I have added/updated tests that prove the effectiveness of these
changes.
- [x] I have updated the documentation to reflect these changes, if
applicable.
- [x] I have followed the project's coding style guidelines.
- [x] I have addressed the code review feedback from the previous
submission, if applicable.

## Additional Notes

N/A
2024-03-10 14:11:47 +11:00
9e1b2e5cc3 fix: update sharp dependency 2024-03-10 13:48:25 +11:00
608e622f69 fix: update testing compose config 2024-03-10 13:48:09 +11:00
415f79f821 fix: update docker docs and compose files 2024-03-10 11:13:05 +11:00
62b4a13d4d feat: upgrade packages 2024-03-09 00:32:08 +05:30
19714fb807 feat: update packages 2024-03-09 00:29:42 +05:30
7631c6e90e feat: add prettier to lint 2024-03-09 00:17:04 +05:30
d462ca0b46 feat: remove prettier plugin 2024-03-09 00:16:16 +05:30
b433225762 Update command.tsx 2024-03-08 22:12:05 +05:30
ad92b1ac23 feat: typeIn confirmation 2024-03-08 21:56:17 +05:30
a4806f933e Merge branch 'main' of https://github.com/Gautam-Hegde/documenso 2024-03-08 21:33:19 +05:30
3b5f8d149a fix: eslint config file parseOptions.project path is updated 2024-03-08 21:14:37 +05:30
0d41c6babf Merge branch 'main' into test/sign-redirect-url 2024-03-08 15:03:21 +00:00
9b5346efef chore: add test for multiple recipient 2024-03-08 14:54:18 +00:00
e8b209eb82 fix: fixed cta component 2024-03-08 15:46:44 +02:00
4476cf8fd1 Merge branch 'main' into fix/layout-shift-on-table 2024-03-09 00:41:20 +11:00
0fdb7f7a8d fix: changed to card component 2024-03-08 15:30:08 +02:00
05aa52b44a fix: fixed the recipients viewing issue on touch screens (#773)
## Description

This pull request addresses the issue where users on touchscreen devices
couldn't properly see the Recipients list. I have implemented a two-easy
solution to address this problem, offering a seamless user experience
across device types:

1. Popover for Touchscreen: For touchscreen devices, I have added a
popover that displays the Recipients list when users tap on the avatars.

2. Tooltip for Larger Screens: On larger screens (desktops and laptops),
I have added a tooltip that appears when users hover over the avatars.

## Changes
1. Added `Popover` for smaller devices and keep the `Tooltip` for larger
devices.
2. Renamed the component `stack-avatars-wtih-tooltip` to
`stack-avatars-wtih-ui` because now it contains both Tooltip and
Popover.
3. Used the `useWindowSize` hook to conditionally render the `Tooltip`
and `Popover`
4. To avoid repeating the same code, I've created a new component named
`stack-avatars-components.tsx` to show the recipient's details. This
component uses both Popover and Tooltip.


## PR Preview



https://github.com/documenso/documenso/assets/87828904/2dc9b056-b4bd-43dd-b427-a0e803dee55a


## Issue

Closes #756
2024-03-08 23:59:11 +11:00
40343d1c72 fix: add use client directive 2024-03-08 12:34:49 +00:00
08201240d2 Merge branch 'main' into update-documents-avatar 2024-03-08 23:26:28 +11:00
32b0b1bcda fix: revert api change and use mouseenter/mouseleave 2024-03-08 12:21:32 +00:00
61ca34eee1 removed unused cn 2024-03-08 13:46:22 +02:00
41843691e8 feat: add website cta 2024-03-08 13:44:25 +02:00
c463d5a0ed fix: add double quote 2024-03-08 16:47:38 +05:30
8afe669978 feat: improve lint staged performance 2024-03-08 16:26:47 +05:30
fd4ea3aca5 fix: update docker docs 2024-03-08 20:00:24 +11:00
ee2cb0eedf docs: add article about public api 2024-03-08 10:20:58 +02:00
1b32c5a17f fix: fix blog post date (#1003)
Fixing the blog date for
https://documenso.com/blog/removing-server-actions
(I assume it was meant to be March 7th)


![image](https://github.com/documenso/documenso/assets/64188227/a7b96168-b094-46c0-877a-da26c9d140d4)
2024-03-08 09:27:30 +02:00
6376112f9d fix: overflow issue with user name input (#991)
Before:-

![Screenshot 2024-03-06
203158](https://github.com/documenso/documenso/assets/81948346/17050582-454b-49af-8124-294d0a0be5bc)

After:-

![Screenshot 2024-03-06
202332](https://github.com/documenso/documenso/assets/81948346/7c346699-3bff-4847-95ef-fd7fdc8a89af)
2024-03-08 15:24:17 +11:00
ddfd4b9e1b fix: update styling 2024-03-08 03:59:15 +00:00
5bec549868 feat: improved ui of document dropzone for max quota state (#997)
**Description:**


[Dropzone.webm](https://github.com/documenso/documenso/assets/23498248/df2d3a54-0e39-4d2d-b792-bf4cd4a1e19d)
2024-03-08 14:38:27 +11:00
2f728f401b chore: add e2e test for deleting a user (#1001) 2024-03-08 14:32:06 +11:00
bc60278bac fix: remove useless ternaries 2024-03-08 03:30:57 +00:00
e9664d6369 chore: tidy code 2024-03-08 03:23:27 +00:00
3b3346e6af fix: remove data-testid attributes 2024-03-08 13:47:59 +11:00
ee35b4a24b fix: update test to use getByRole 2024-03-08 02:46:25 +00:00
47b06fa290 Merge branch 'main' into test/delete-user 2024-03-08 13:30:24 +11:00
b9dccdb359 chore: updated code of conduct link (#999)
**Description:**

Updated broken link to code of conduct in the Readme
2024-03-08 12:54:23 +11:00
5e00280486 fix: add seed script to dx setup (#1000) 2024-03-08 12:53:08 +11:00
f0fd5506fc fix: skip seeding when running migrate dev
When prisma:migrate-dev needs to reset the database it will run the seed script to repopulate data. Now that we've added the seed script to our root setup command we will want to avoid this behaviour since we will end up double seeding the database which currently can cause issues.
2024-03-08 12:49:55 +11:00
ff3b49656c chore: remove unused function 2024-03-08 00:07:11 +00:00
e47ca1d6b6 chore: add e2e test for deleting a user 2024-03-08 00:04:27 +00:00
59cdf3203e fix: add seed script to dx setup 2024-03-07 20:09:29 +00:00
92f44cd304 chore: remove trailing slash
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-08 00:36:18 +05:30
3ce5b9e0a0 chore: updated code_of_conduct link
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-08 00:33:15 +05:30
e7f8f4e188 Merge branch 'main' of https://github.com/documenso/documenso into feat/doc-limit-improvement 2024-03-07 21:08:04 +05:30
6c9303012c chore: updated animation
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-07 21:06:16 +05:30
6fc3803ad2 Merge branch 'main' of https://github.com/Gautam-Hegde/documenso 2024-03-07 20:54:55 +05:30
f7e7c6dedf fix: overflow issue 2024-03-07 20:08:11 +05:30
0c426983bb chore: updated initial animation state
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-07 19:28:35 +05:30
9ae51a0072 feat: improved ui of document dropzone for max quota state
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-07 19:04:58 +05:30
d694f4a17b fix: show close icon on notification inmobile 2024-03-07 18:48:23 +05:30
80f767b321 chore: update docker section in readme 2024-03-07 23:36:34 +11:00
4ec8eeeac1 docs: add article on removing server actions (#994) 2024-03-07 23:11:32 +11:00
8e813ab2ac fix: use docker manifests for multiarch 2024-03-07 22:16:37 +11:00
51f60926ce fix: re-add buildx and qemu 2024-03-07 10:27:14 +00:00
2ccc2f22de chore: back to docker buildx 2024-03-07 10:24:17 +00:00
f6eddaa9f6 fix: remove duplicate neon pooler (#990)
## Description

Fixes the issue with Vercel preview deployments failing.

It appears that the old `PGHOST` environment variable injected by neon
was:

`ep-snowy-snowflake-a2vc5pa2.eu-central-1.aws.neon.tech`

It is now:

`ep-snowy-snowflake-a2vc5pa2.eu-central-1-pooler.aws.neon.tech`

Notice the `-pooler` being attached automatically to the `PGHOST`.

## References

> The following changes were made to the [Neon Vercel
Integration](https://vercel.com/integrations/neon):
>
>To ensure that users accessing a Neon database from a serverless
environment have enough connections, the DATABASE_URL and PGHOST
environment variables added to a Vercel project by the Neon integration
are now set to a pooled Neon connection string by default. Pooled
connections support up to 10,000 simultaneous connections. Previously,
these variables were set to an unpooled connection string supporting
fewer concurrent connections.

https://neon.tech/docs/changelog

https://neon.tech/docs/guides/vercel#manage-vercel-environment-variables
2024-03-07 18:17:28 +08:00
35b2405921 chore: use warpbuild for docker publishing 2024-03-07 10:02:33 +00:00
7b77084dee fix: deleted files that shouldn't have been committed 2024-03-07 11:34:19 +02:00
a34334c4dc docs: add article on removing server actions 2024-03-07 11:32:01 +02:00
0c8b24ba75 chore: remove bcrypt (#993)
Remove `bcrypt` in favor of `@node-rs/bcrypt` which includes precompiled
binaries for many platforms reducing the number of tasks to run post
`npm install`.

Resolves #986
2024-03-07 19:58:53 +11:00
8dad3607cf fix: add @node-rs/bcrypt to server component externals 2024-03-07 18:48:06 +11:00
cffb7907b5 chore: remove bcrypt 2024-03-07 18:30:22 +11:00
b9b57f16c0 fix: use matrix build 2024-03-07 05:05:35 +00:00
03d2a2a278 fix: update buildx platform arg 2024-03-07 04:35:25 +00:00
82efc5f589 fix: use docker buildx and push 2024-03-07 04:07:55 +00:00
8a9588ca65 fix: fetch tags for publish action 2024-03-07 03:49:03 +00:00
f723a34464 chore: remove launch week quote 2024-03-07 14:12:10 +11:00
dd1d657057 feat: tidy docker setup (#988)
Tidy the docker setup and include a Github Action
for publishing docker containers to DockerHub and
Github Container Registry.

Also add a small README file with docker hosting instructions.
2024-03-07 14:11:03 +11:00
10ef5b6e51 fix: improvements from testing 2024-03-07 02:57:02 +00:00
b5b74a788c fix: overflow issue with user name input 2024-03-06 14:52:59 +00:00
9525b9bd63 fix: resolve issue with testing compose file 2024-03-06 23:48:28 +11:00
d382e03085 feat: add typefully card to open page (#979)
**Description:**

The new card for typefully looks like the below screenshot

<img width="960" alt="Screenshot 2024-03-01 at 12 53 00"
src="https://github.com/documenso/documenso/assets/23498248/925d3362-f883-48b2-8870-83b8115bac7d">
2024-03-06 13:30:35 +05:30
b8fa45380b Merge branch 'main' into feat/typefully 2024-03-06 13:28:52 +05:30
2cce6dc2e5 feat: tidy docker setup
Tidy the docker setup and include a Github Action
for publishing docker containers to DockerHub and
Github Container Registry.

Also add a small README file with docker hosting instructions.
2024-03-06 15:46:51 +11:00
927cb1a934 fix: incorrect download filename logic 2024-03-05 23:05:32 +00:00
579a2f96e5 chore: rename community => early adopter 2024-03-05 13:04:46 +00:00
8a82952b34 Merge branch 'main' into feat/typefully 2024-03-05 16:42:29 +05:30
70494fa5bb feat: add offline development support (#987)
## Description

Add support to develop without network access since TRPC by default will
prevent network requests when offline.

https://tanstack.com/query/v4/docs/framework/react/guides/network-mode#network-mode-always

## Changes Made

- Add dynamic logic to toggle offline development
- Removed teams feature flag
2024-03-05 22:06:48 +11:00
41ccefe212 chore: remove twt logo asset
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-05 15:43:33 +05:30
e83a4becee chore: update twitter logo
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-05 15:41:34 +05:30
a03ce728f3 chore: remove ssr
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-05 15:34:42 +05:30
d29caaf823 Merge branch 'feat/typefully' of https://github.com/documenso/documenso into feat/typefully 2024-03-05 14:59:18 +05:30
7e9b96f059 Merge branch 'main' of https://github.com/documenso/documenso into feat/typefully 2024-03-05 14:58:27 +05:30
691255da3f chore: updated styling of tooltips and margins
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-05 14:57:29 +05:30
f6f9c301da feat(ci): cache github workflow actions (#804) 2024-03-05 10:04:06 +11:00
a17d4a2a39 fix: handle signature annotations 2024-03-03 11:36:28 +11:00
73aae6f1e3 feat: improve admin panel 2024-03-03 01:55:33 +11:00
328d16483c chore: update profile claim dialog and modal (#983)
**Description:**

**Settings Page:**

<img width="668" alt="Screenshot 2024-03-01 at 19 12 40"
src="https://github.com/documenso/documenso/assets/23498248/08e48432-39a6-4ef0-bc53-931fc3c81545">

**Claim Modal:** 

<img width="588" alt="Screenshot 2024-03-01 at 19 14 17"
src="https://github.com/documenso/documenso/assets/23498248/69bc2d02-97c6-4a29-88a4-55ed8898ccf5">
2024-03-02 12:45:22 +11:00
6dd2abfe51 fix: username min length + fixed condition (#982)
fixed #981 

`url.length <= 6` >>> `url.length < 6`

also removed debug message from the form component
2024-03-02 12:43:44 +11:00
452545dab1 chore: updated button text
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-01 19:12:09 +05:30
437410c73a chore: updated text color
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-01 19:11:47 +05:30
36a95f6153 chore: updated text color
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-01 19:10:15 +05:30
0f03ad4a6b chore: updated wordings for claimed ursers
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-01 19:05:16 +05:30
8674ad4c88 docs: add node version minimum requirement (#975)
## PR fixes which issue ? 

This PR fixes #974 

## Description: 

- Have added minimum node version required to setup the Documenso
project locally which will ensure to save time of new contributors
setting up their project. 💚

## Reference for the minimum requirement decision: 


https://nextjs.org/docs/app/building-your-application/upgrading/version-14#v14-summary

---------

Co-authored-by: Adithya Krishna <aadithya794@gmail.com>
Co-authored-by: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
2024-03-01 15:12:08 +02:00
98e00739c8 Merge branch 'main' into feat/typefully 2024-03-01 15:05:14 +02:00
00c36782ff fix: why didn't prettier catch this 2024-03-01 22:59:52 +11:00
665ccd7628 update username min characters 2024-03-01 11:30:42 +00:00
e5fe3d897d remove fixed true condition
from auth signup router
2024-03-01 11:27:24 +00:00
bfb1c65f98 remove debug logs
console.log on signup form
2024-03-01 11:25:21 +00:00
3b8b87a90b Chore/blog (#980)
day 5
2024-03-01 11:14:26 +01:00
819e58dd61 chore: image 2024-03-01 11:11:52 +01:00
8c435d48b7 chore: day5 2024-03-01 10:22:10 +01:00
0c8a89a2ea feat: add typefully card
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-03-01 12:53:51 +05:30
85885b5ad5 fix: api documentation errors on ssr 2024-03-01 14:54:41 +11:00
5cc4204788 fix: add early return to webhook trigger 2024-03-01 14:12:06 +11:00
e226f012a9 chore: ctas (#978) 2024-02-29 14:25:53 +01:00
4ad722ba00 chore: ctas 2024-02-29 14:25:21 +01:00
1927cc4067 chore: typos and links (#977) 2024-02-29 14:15:18 +01:00
c2ec092404 chore: typos and links 2024-02-29 14:14:35 +01:00
ebe23335f8 fix: return the recipient as an array to match other formats from zapier (#971)
Return the recipient as an array to match the other formats for Zapier.
Otherwise, Zaps with the "DOCUMENT_OPENED" hooks won't work.

All the other webhooks return the "Recipient" field as an array.
2024-02-29 08:37:01 +02:00
e5be219b99 feat: claim profile (#972)
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-29 15:29:47 +11:00
aa87a86a5f fix: update e2e tests 2024-02-29 15:19:38 +11:00
1cc7c95a33 fix: update e2e test 2024-02-29 14:26:37 +11:00
9f576eb47c fix: update signup mutation schema 2024-02-29 14:13:37 +11:00
bd82ad7a0b fix: revert settings-header changes 2024-02-29 14:12:00 +11:00
cc1b888174 chore: tidy changes 2024-02-29 14:09:17 +11:00
5576cdc2b0 Merge branch 'main' into feat/public-profile-1 2024-02-29 14:08:19 +11:00
ecc9dc63ea feat: the rest of the owl 2024-02-29 13:22:21 +11:00
11fe380338 chore: typo (#970) 2024-02-28 13:38:47 +01:00
f376c7b951 chore: typo 2024-02-28 13:38:10 +01:00
90477dfd00 chore: cta (#969) 2024-02-28 12:23:52 +01:00
dd81f946b4 chore: cta 2024-02-28 12:23:21 +01:00
f4e1df7f3a Chore/blog (#968) 2024-02-28 12:14:46 +01:00
3510d8b6b0 chore: text 2024-02-28 12:14:19 +01:00
1590fa9457 chore: text 2024-02-28 12:13:48 +01:00
ce09c32bf5 Chore/blog (#967)
day 3 lgtm
2024-02-28 11:59:56 +01:00
077da72b68 feat: implement webhooks (#913) 2024-02-28 18:31:48 +11:00
c8869b3088 chore: fixed UI for webhooks team 2024-02-28 09:18:05 +02:00
8bd1647a2d chore: fixed UI 2024-02-28 09:14:10 +02:00
00a7389af3 chore: implemented feedback 2024-02-28 08:13:28 +02:00
ae00290b6f fix: add reference to swagger docs 2024-02-28 04:34:33 +00:00
e3e2cfbcfd fix: refactor and implement design 2024-02-28 14:43:09 +11:00
8ab77e0e55 chore: day 3 2024-02-27 19:30:01 +01:00
93e34a5abd Merge branch 'main' into feat/webhook-implementation 2024-02-27 16:37:29 +02:00
f0a511e238 chore: refactor code 2024-02-27 15:22:02 +02:00
837ff531a9 chore: format 2024-02-27 11:55:10 +01:00
b02263eea1 Chore/blog (#964)
day 2 blogpost
2024-02-27 11:49:16 +01:00
f5a0c1733f chore: tags 2024-02-27 11:47:52 +01:00
18368b5801 chore: day 2 blogpost 2024-02-27 11:47:22 +01:00
b225cc8139 chore: updated styles
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-27 20:40:43 +11:00
b498f8edb7 feat: update ui
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-27 20:40:42 +11:00
b1261510d2 feat: update signin signup pages
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-27 20:39:20 +11:00
65d762dd4b feat: update signin signup ui
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-27 20:39:19 +11:00
deea6b1535 feat: update marketing site
Signed-off-by: Adithya Krishna <adithya@documenso.com>
2024-02-27 20:36:40 +11:00
af30443f5a Merge branch 'main' into feat/webhook-implementation 2024-02-27 18:17:54 +11:00
a4b1f7c983 feat: support team webhooks 2024-02-27 16:56:32 +11:00
7dd2bbd8ab feat: update webhook handling and triggering 2024-02-27 15:16:14 +11:00
488464e3e7 chore: add team to webhook model 2024-02-27 13:38:12 +11:00
1ec549b869 chore: add webhook-call model 2024-02-27 13:37:24 +11:00
c2daa964c0 chore: use cuids for webhooks 2024-02-27 12:13:56 +11:00
53d18eb3c8 chore: typo (#963) 2024-02-26 17:13:19 +01:00
21016216b6 chore: typo 2024-02-26 17:13:01 +01:00
b5b0aeef6d chore: typo (#962)
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---

## Description

<!--- Describe the changes introduced by this pull request. -->
<!--- Explain what problem it solves or what feature/fix it adds. -->

## Related Issue

<!--- If this pull request is related to a specific issue, reference it
here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->

## Changes Made

<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes.
-->

- Change 1
- Change 2
- ...

## Testing Performed

<!--- Describe the testing that you have performed to validate these
changes. -->
<!--- Include information about test cases, testing environments, and
results. -->

- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.

## Additional Notes

<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential
concerns, or anything else relevant. -->
2024-02-26 17:11:53 +01:00
8faf3afbbe chore: typo 2024-02-26 17:11:09 +01:00
0044b1f617 Update README.md (#961) 2024-02-26 14:22:00 +01:00
75f8305023 Update README.md 2024-02-26 14:20:44 +01:00
36e32639c9 chore: blog day 1 (#960)
deploy day 1
2024-02-26 13:12:10 +01:00
25164abc14 chore: capitals 2024-02-26 13:11:44 +01:00
1b8b81b16c chore: capitals 2024-02-26 13:11:08 +01:00
4bdd41c7c5 chore: blog day 1 2024-02-26 13:09:15 +01:00
a31057d0d1 fix: add updated badge 2024-02-26 22:35:10 +11:00
70165c4469 chore: ui updates 2024-02-26 22:24:23 +11:00
8dceeffff7 chore: pricing update (#952)
new pricing, ready to be merged when the launchweek day 1 article drops
2024-02-26 12:09:28 +01:00
15c22d3897 feat: updated the triggerWebhook function 2024-02-26 12:52:30 +02:00
a54d09ab19 Merge branch 'main' into chore/pricing-update 2024-02-26 09:44:16 +01:00
15ebe6dbaf fix: add cascade delete to recipient fields 2024-02-26 03:51:28 +00:00
5805d8a903 Merge branch 'main' into feat/webhook-implementation 2024-02-26 12:47:21 +11:00
f7c6b53258 feat: public api start (#674)
This PR adds the public-facing API.

![API high-level
workings](https://github.com/documenso/documenso/assets/25515812/9f866889-164f-401f-bf4e-d3e69b6382f1)

The public API will allow users to interact with Documenso
programmatically. At the moment, there are 5 available endpoints:
1. `GET /documents`
    - get all documents of the user making the API call
- it accepts 2 URL query parameters - `page` and `perPage`, but they are
not mandatory
2. `GET /documents/:id`
    - get a specific document of the user making the API call
- it requires 1 URL parameter, which represents the ID of the document
that needs to be retrieved
3. `DELETE /documents/:id`
    - delete a specific document of the user making the API call
- it requires 1 URL parameter, which represents the ID of the document
that needs to be deleted
4. `POST /documents`
- making a POST request to this endpoint will return an S3 pre-signed
URL where you can upload the document
    - it requires you to pass the file name and the file content type
5. `PATCH /documents/:id/send-for-signing`
    - send a document for signing
    - it allows you to pass the following:
        - signer email *(required)*
        - signer name *(optional)*
        - field type (signature, email, name, etc.) *(required)*
        - page number where to insert the field *(required)*
        - page X *(required)*
        - page Y *(required)*
        - page width *(required)*
        - page height *(required)*
        - email subject *(optional)*
        - email body *(optional)*

The users are authenticated through API tokens. The users can create one
or more tokens in their account settings, and each token will be
available for 1 year _(duration open to suggestions/changes)_.

Each time the user makes a request, the app checks if the token exists
and if it's valid. The app will return `401` *(unauthorized)* and the
appropriate error message if either is false. If both are true AND the
user uses the correct HTTP verb and passes the required URL parameters
and body, the API call will be successful.

Code overview:
- `@documenso/packages/lib/trpc/api-contract` - this folder contains the
`contract` and `schema` files. The `contract.ts` file describes the
structure of the API, the format of the requests and responses, and how
to authenticate your API calls, among others. The `schema.ts` file
contains the Zod schemas used in the API contract.
- `@documenso/packages/trpc/server/api-token-router` - this folder
contains the `router.rs` and `schema.ts` files. Here are the tRPC
procedures used on the frontend. That is, the user's settings page,
where the user can list/create/delete API tokens.
- `@documenso/packages/trpc/tsconfig.json` - I added `"strict": true`
because that's what's suggested in the `ts-rest` documentation.
*([source](https://ts-rest.com/docs/quickstart#create-a-contract))*
- `[...ts-rest].tsx` - This file contains the implementation of the API.
You can see the logic behind each route.
- the rest of the code represents the code for the API tokens page in
the user's settings.
2024-02-26 12:41:16 +11:00
4778270d3c fix: use template dialog to pass teamId 2024-02-26 12:20:50 +11:00
3c51a1bc3d Merge branch 'main' into feat/public-api 2024-02-26 12:15:33 +11:00
fcfb741363 feat: edit recipients when creating document from template (#953)
https://github.com/documenso/documenso/assets/55143799/85a840e3-4fb4-4c02-ba63-b9626f4cea58
2024-02-26 12:00:23 +11:00
a54159a9ec fix: add missing teamId references 2024-02-26 11:59:32 +11:00
5bd0dde4a5 fix: e2e tests 2024-02-26 10:50:46 +11:00
6b8e11b535 fix: template router update 2024-02-26 10:31:24 +11:00
c7dac2f4de fix: update delete endpoint and add limits 2024-02-26 10:01:13 +11:00
4cb0d6999d fix: build errors from merge 2024-02-26 09:13:46 +11:00
2a74ce06ef Merge branch 'main' into feat/public-api 2024-02-26 00:21:25 +11:00
6ea6dda99d feat: add transparent background to fields (#899) 2024-02-26 00:10:35 +11:00
187a988f9c Merge branch 'main' into transparent-fields 2024-02-26 00:02:56 +11:00
5bef2fba91 fix: follow figma design 2024-02-25 13:02:09 +00:00
df3ba11655 feat: account deletion (#846)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
	- Added two-factor authentication form handling in profile settings.
- Introduced account deletion functionality, including Stripe customer
data removal.
- Updated UI components and styles for improved user interaction, such
as enhanced visual feedback for destructive actions.
- **Refactor**
- Improved code quality by updating import statements for type-only
imports.
- **Chores**
- Restructured the `forwardPorts` array in dev container configuration
for clarity.
- Removed a trailing comma in the `vscode` extensions list to adhere to
JSON format rules.
- **Documentation**
- Added comments to clarify the handling of undefined values in
two-factor authentication verification.
- **Database**
- Implemented a SQL migration script for creating a default deleted user
in the system.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-02-25 22:49:55 +11:00
9cf72e1442 chore: tidy code and extract alert-dialog 2024-02-25 11:12:18 +00:00
7226d5ac53 Merge branch 'main' into feat/account-deletion 2024-02-24 23:18:14 +11:00
dbd63498e0 feat: custom text field (#945)
https://github.com/documenso/documenso/assets/55143799/c0f296ce-1e91-4b49-9c19-1bdffbd3163f
2024-02-24 22:55:16 +11:00
4ca154d88a fix: update e2e test? 2024-02-24 11:54:13 +00:00
d34c862688 fix: switch custom text to input rather than textarea 2024-02-24 11:25:12 +00:00
a5e51e63a1 Merge branch 'main' into custom-text-field 2024-02-24 22:23:17 +11:00
14c77d7c92 fix: update e2e test 2024-02-24 11:19:07 +00:00
a5a0fd9187 fix: update e2e test 2024-02-24 10:55:48 +00:00
77193b93c4 fix: resolve build error and update dialog width 2024-02-24 10:36:27 +00:00
a10957d909 Merge branch 'main' into feat/improve-create-document-from-template 2024-02-24 21:26:11 +11:00
688e32dfc2 feat: add roles to templates recipients (#935) 2024-02-24 21:24:26 +11:00
b1b3b84d82 Merge branch 'main' into feat/template-recipient-roles 2024-02-24 21:01:45 +11:00
b09acaf94e feat: custom banner (#956) 2024-02-24 20:40:41 +11:00
fab4992e13 feat: add zapier support 2024-02-24 11:18:58 +02:00
d103c68a73 fix: remove console logs 2024-02-24 08:08:14 +00:00
a06b40af3c fix: revert hardcoded env 2024-02-24 15:11:12 +11:00
0900836040 fix: missing marketing env variables 2024-02-24 14:56:11 +11:00
9ea56bddd1 chore: pricing update 2024-02-23 17:02:20 +01:00
0b83671c78 fix: hardcode signup until env issue is fixed (#958)
hotfix for broken signup links
2024-02-23 16:50:43 +01:00
41cbf3ba3c fix: hardcode signup until env issue is fixed 2024-02-23 16:49:17 +01:00
8165a090d1 feat: migrate to site-settings 2024-02-23 10:47:01 +00:00
c436559787 feat: create a banner with custom text by admin 2024-02-22 20:20:49 +00:00
15e191f62d fix: e2e tests (#955) 2024-02-22 23:58:51 +11:00
f973dfd09c feat: add document page view (#926)
## Description

This feature allows us to have a dedicated page to view details for a
document.

The current `/documents/[id]` page has been moved to
`/documents/[id]/edit`.

The new `/documents/[id]` page will now display general information on
the document itself.

This will allow us to provide more details relating to a document in the
future, such as version history.

## Examples

These may be outdated as deployment failed

Login via example@documenso.com

Draft document no recipients:

https://stg-7hpkvyc5i-documenso.vercel.app/documents/446

Draft document with recipients:

https://stg-7hpkvyc5i-documenso.vercel.app/documents/440

Pending document:

https://stg-7hpkvyc5i-documenso.vercel.app/documents/447

Completed document:

https://stg-7hpkvyc5i-documenso.vercel.app/documents/448

Pending teams document:

https://stg-7hpkvyc5i-documenso.vercel.app/t/hello/documents/449/

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [X] I have followed the project's coding style guidelines.

## Additional Notes

When a user "sends" a document (DRAFT -> PENDING), should we redirect
them to the document/id page, or keep it how it currently is and have it
redirect to `/documents` page?
2024-02-22 23:25:22 +11:00
a58fee2da6 fix: e2e tests 2024-02-22 22:58:51 +11:00
1232f54a23 Merge branch 'main' into feat/separate-document-page 2024-02-22 22:54:15 +11:00
306e5ff31f fix: add timeouts to longer transactions 2024-02-22 11:05:49 +00:00
34825aaf3a feat: add separate document audit page 2024-02-22 19:14:01 +11:00
dd29845934 fix: put max height in teams section in profile dropdown (#947)
fixes: #942
2024-02-22 16:49:54 +11:00
a48bda0d27 feat: add page numbers to documents (#946) 2024-02-22 16:36:34 +11:00
48321f2f62 Merge branch 'main' into feat/separate-document-page 2024-02-22 16:13:33 +11:00
2abcdd7533 feat: team api tokens 2024-02-22 13:39:34 +11:00
df132a51ab feat: ability to download all the 2FA recovery codes (#944)
fixes #938 



https://github.com/documenso/documenso/assets/81948346/8bf807ae-41b0-417b-9f55-d96584e71acb
2024-02-21 13:39:08 +11:00
8287722f59 fix: update view dialog to use new download api 2024-02-21 02:29:19 +00:00
aba6b58c14 fix: simplify download api 2024-02-21 02:19:35 +00:00
b6c9213b66 fix: disable static generation of marketing site pages
Disables the generation of the blog and content pages using
generateStaticPaths to deal with a regression with routing
introduced with next-runtime-env.
2024-02-21 00:58:57 +00:00
22e3a79a72 Merge branch 'main' into feat/public-api 2024-02-21 11:29:36 +11:00
6ee896048e feat: dialog to enter custom recipients for creating document from template 2024-02-20 19:11:12 +00:00
4d286e01d1 feat: allow recipients when creating document from template 2024-02-20 17:20:03 +00:00
3106c325d7 Merge branch 'main' into fix/layout-shift-on-table 2024-02-20 21:52:35 +05:30
b9e5905469 feat: create from template 2024-02-20 19:46:18 +11:00
39c6cbf66a feat: ability to download 2FA recovery codes 2024-02-19 11:25:15 +00:00
c6dbaaea21 Merge branch 'main' into feat/webhook-implementation 2024-02-19 10:02:06 +02:00
ac6da9ab45 feat: add feature flag 2024-02-19 14:31:26 +11:00
791a22cb5f refactor: add comments 2024-02-19 13:22:48 +11:00
11299d3f92 feat: add custom text to singleplayer 2024-02-17 11:24:47 +00:00
9301b8ef4d fix: multiple text fields sharing the same text value 2024-02-17 10:50:19 +00:00
ad6fad1182 fix: custom text with multiline throwing an error
took me hours
2024-02-17 10:43:22 +00:00
5687503dfc feat: sign document with a custom text 2024-02-17 08:26:30 +00:00
0186f2dfed feat: ability to download 2FA recovery codes 2024-02-17 13:19:03 +05:30
f98567ea87 feat: request usee to disable 2fa before deleting account 2024-02-17 07:34:21 +00:00
2815b1a809 feat: add enterprise billing (#939)
## Description

Add support for enterprise billing plans.

Enterprise billing plans by default get access to everything early
adopters do:
- Unlimited teams
- Unlimited documents

They will also get additional features in the future.

## Notes

Pending webhook updates to support enterprise onboarding.
Rolled back env changes `NEXT_PUBLIC_PROJECT` since it doesn't seem to
work.
2024-02-17 12:42:00 +11:00
5d6cdbef89 feat: ability to download all the 2FA recovery codes 2024-02-16 20:46:27 +00:00
26d4bbf010 chore: ui updates 2024-02-16 13:58:03 +02:00
960914aeb5 fix: undo operation on signature pad (#868)
fixes: #864
2024-02-16 22:57:14 +11:00
d83769b410 chore: use unsafe effect 2024-02-16 11:56:02 +00:00
cd240ae8a4 chore: loading spinner 2024-02-16 13:55:47 +02:00
a1459b41fd Merge branch 'main' into feat/webhook-implementation 2024-02-16 13:04:38 +02:00
a0cf2a2c75 fix: improved document-dropzone ui for small vertical screens (#857)
improved document-dropzone ui for small vertical screens (screens less
than 800px vertically)
Although it can still become congested on really small vertical screens,
but possibility is really low.

fixes: #840
2024-02-16 22:03:24 +11:00
a30b73ce86 fix: update css 2024-02-16 11:02:04 +00:00
46d163d9d6 fix: highlighting issue in recipient selection (#937)
fixes: #920 

<img width="391" alt="image"
src="https://github.com/documenso/documenso/assets/75713174/08b2f5ab-4a6f-423a-a2fa-8f7b04789bb8">
2024-02-16 21:50:53 +11:00
681a89cfe1 chore: minor lint fixes (#934) 2024-02-16 21:48:45 +11:00
4d6e780abe chore: merge main 2024-02-16 12:12:54 +02:00
7f3f6f5312 feat: hide secret field 2024-02-16 11:44:03 +02:00
019db27b1d feat: trigger webhook functionality 2024-02-16 11:04:11 +02:00
e5f4edc120 chore: create security.txt (#878)
Adding a security.txt file enables security researchers to quickly and
easily see where they can submit security issues and know that they are
being taken serious. From the proposal website:

> "When security risks in web services are discovered by independent
security researchers who understand the severity of the risk, they often
lack the channels to disclose them properly. As a result, security
issues may be left unreported. security.txt defines a standard to help
organizations define the process for security researchers to disclose
security vulnerabilities securely.”

See also https://securitytxt.org
2024-02-16 12:34:41 +11:00
25291b64eb fix: highlighting issue in recipient selection 2024-02-15 22:25:23 +05:30
fddd860d15 chore: code refactor to avoid repetitions 2024-02-15 11:33:43 +00:00
fe2093fe7c feat: add next-runtime-env (#869)
This PR adds the package
[next-runtime-env](https://github.com/expatfile/next-runtime-env/) to
populate the public environment variables at runtime.
2024-02-15 22:10:21 +11:00
7fbe9b519c fix: styling 2024-02-15 20:42:17 +11:00
bd3c64658a feat: add document version history UI 2024-02-15 20:39:26 +11:00
769eaa0ed9 feat: add roles to templates recipients 2024-02-15 07:01:41 +00:00
49cddfab38 chore: lint with oxc 2024-02-15 06:11:50 +00:00
3e12a05ab8 chore: more grammar 2024-02-14 17:19:48 +01:00
a76504c0a4 Merge branch 'main' into chore-security-text 2024-02-14 17:16:44 +01:00
abab0c0a22 chore: grammer and format 2024-02-14 17:14:43 +01:00
c680cfc24f chore: update pr based on review 2024-02-14 14:55:46 +00:00
cab875f68a fix: update create delete user sql script 2024-02-14 14:55:46 +00:00
6daaa3a6d4 Merge branch 'main' into feat/account-deletion 2024-02-14 14:54:26 +00:00
61958989b4 feat: more webhook functionality 2024-02-14 14:38:58 +02:00
4c5b910a59 chore: add examples 2024-02-14 13:15:35 +11:00
524a2f4ea9 Merge branch 'main' into feat/separate-document-page 2024-02-13 11:15:31 +11:00
82b87739d0 fix: update document links 2024-02-12 19:00:47 +11:00
071475769c feat: add document page view 2024-02-12 17:30:23 +11:00
1a82740d0f feat: support recipient roles 2024-02-12 15:16:09 +11:00
51608ed390 fix: lint issue 2024-02-12 02:02:43 +00:00
8ebef831ac Merge branch 'main' into feat/add-runtime-env 2024-02-12 12:30:35 +11:00
20e2976731 fix: build issues 2024-02-12 01:29:22 +00:00
0209127136 feat: delete webhook functionality 2024-02-09 16:28:18 +02:00
ddb9dd11d7 feat: added backend stuff 2024-02-09 16:07:33 +02:00
b3ba77dfed feat: allow user to choose expiry date 2024-02-09 11:35:09 +02:00
e91bb78f2d Merge branch 'main' into feat/public-api 2024-02-09 16:00:40 +11:00
748bf6de6b fix: add dropped constants from merge 2024-02-08 22:12:04 +11:00
d13cf743bf Merge branch 'main' into feat/add-runtime-env 2024-02-08 22:06:59 +11:00
58f4b72939 added fixed width for status col 2024-02-08 13:31:38 +05:30
98df273ebc feat: add field and recipient endpoints 2024-02-08 16:58:44 +11:00
b3514bd0c7 add new webhook dialog 2024-02-07 16:04:12 +02:00
7ef771533a Merge branch 'main' into transparent-fields 2024-02-07 13:34:46 +05:30
edeeaa5651 feat: implement webhooks 2024-02-06 16:12:31 +02:00
30752815e7 feat: soft-delete transfered documents 2024-02-05 13:06:36 +00:00
bc989075ba Merge branch 'main' into feat/account-deletion 2024-02-05 13:01:59 +00:00
4c09867b55 feat: transfer completed and pending documents to deleted email 2024-02-05 12:47:22 +00:00
4d93ed60c5 feat: add deleted account migration 2024-02-05 12:12:02 +00:00
c970abc871 added onchange handler 2024-02-02 20:46:54 +05:30
d5b3df1648 fixed variable declaration 2024-02-02 19:32:39 +05:30
142c1c003e changed useEffect variables 2024-02-02 18:16:54 +05:30
a06c628653 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 17:56:58 +05:30
7ca3697303 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 14:49:06 +05:30
d598677dc5 feat: add transparent background to fields 2024-02-02 08:36:54 +00:00
8ac2209493 Merge branch 'main' into chore-security-text 2024-02-02 16:16:25 +11:00
9c4ec34a3c fix: add precommit step for .well-known 2024-02-02 04:00:28 +00:00
1f142e334a Merge branch 'main' into chore-security-text 2024-01-31 20:31:34 +01:00
08f82b23dc fix: update env entries to evaluate at runtime 2024-01-31 22:32:42 +11:00
747a7b0aea chore: security contacts and descr 2024-01-30 16:15:32 +01:00
375df71f5c Merge branch 'main' into chore-security-text 2024-01-29 16:43:57 +01:00
7b6d6fb1b9 Merge branch 'main' into update-documents-avatar 2024-01-29 19:52:29 +08:00
c0bb5205e1 chore: merged main 2024-01-29 10:14:56 +02:00
014c09bd91 fix: account deletion error for users without two factor authentication 2024-01-28 18:43:20 +00:00
927a656c57 Create security.txt
See also https://securitytxt.org
2024-01-28 01:00:07 +01:00
751fb5275c Merge branch 'main' into feat/add-runtime-env 2024-01-26 14:04:54 +02:00
2f18518961 chore: merged main 2024-01-25 10:53:05 +02:00
d451a7acce feat: add next-runtime-env 2024-01-25 10:48:20 +02:00
d8aecc4092 fixed undo operation on signature pad 2024-01-25 13:21:55 +05:30
ab8c8e2a57 Merge branch 'main' of https://github.com/Gautam-Hegde/documenso 2024-01-24 12:08:21 +05:30
e5c2263e92 fix: imporoved document-dropzone ui for small vertical screens 2024-01-23 18:37:02 +05:30
6e22eff5a1 feat: command grp border 2024-01-23 00:02:04 +05:30
5a28eaa4ff feat: add recipient creation 2024-01-22 17:38:02 +11:00
98667dac15 chore: code tidy 2024-01-22 12:03:14 +05:30
9e433af112 feat: require 2fa code before account is deleted 2024-01-21 15:38:32 +00:00
7762b1db65 feat: add loading to button 2024-01-21 09:47:50 +00:00
a3e560899a feat: delete user from db and unsubscribe from stripe 2024-01-20 23:30:56 +00:00
f652ca9b73 feat: account deletion confirmation dialog 2024-01-20 19:07:47 +00:00
b6aface982 chore: update api description 2024-01-19 16:59:48 +02:00
b28a7f9702 chore: add openapi 2024-01-19 16:55:16 +02:00
3b82ba57f3 chore: implemented feedback plus some restructuring 2024-01-17 12:44:25 +02:00
1d4e78e579 Merge branch 'main' into update-documents-avatar 2024-01-09 20:33:31 +08:00
6065140715 feat: cache layers 2024-01-06 15:29:19 +05:30
34a59d2db3 fix: cache 2024-01-06 15:18:33 +05:30
ba37633ecd fix: revert 2024-01-06 15:18:09 +05:30
46e83d65bb feat: cache docker 2024-01-06 15:14:28 +05:30
142c93aa63 chore: revert force build error 2024-01-06 14:45:44 +05:30
3eb1a17d3c chore: force build error 2024-01-06 14:42:49 +05:30
6d1ad179d4 feat: add clean cache workflow 2024-01-06 13:30:21 +05:30
8eed13e275 fix: remove additional workflow 2024-01-05 02:22:42 +05:30
346078dbbe fix: e2e 2024-01-05 02:15:27 +05:30
c8337d7dcc fix: key 2024-01-05 02:13:42 +05:30
75630ef19d fix: npm action 2024-01-05 01:58:00 +05:30
634807328e fix: command 2024-01-05 01:38:14 +05:30
2bbbe1098a fix: action 2024-01-05 01:32:47 +05:30
d24b9de254 fix: skip install 2024-01-05 01:19:22 +05:30
c86f79dd7b feat: add workflow call actions 2024-01-05 01:11:28 +05:30
0c12e34c38 fix: remove call 2024-01-05 01:08:32 +05:30
26b604dbd0 fix: add workflow call 2024-01-05 00:21:05 +05:30
308f55f3d4 fix: key 2024-01-05 00:09:41 +05:30
e5b7bf81fa fix: add action to codeql 2024-01-05 00:06:16 +05:30
b35f050409 fix: add shell 2024-01-05 00:03:20 +05:30
ce6f523230 fix: key 2024-01-04 23:56:32 +05:30
9e57de512a feat: use actions 2024-01-04 23:46:09 +05:30
9b5d64cc1a feat: add playwright action 2024-01-04 23:44:27 +05:30
fc372d0aa9 feat: add node install action 2024-01-04 23:41:48 +05:30
e470020b16 feat: add cache build action 2024-01-04 23:41:24 +05:30
0a9006430f fix: command 2024-01-04 23:40:35 +05:30
8e754405f8 Merge branch 'main' into update-documents-avatar 2024-01-04 20:45:39 +06:00
a1215df91a refactor: extract api implementation to package
Extracts the API implementation to a package so we can
potentially reuse it across different applications in the
event that we move off using a Next.js API route.

Additionally tidies up the tokens page and form to be more simplified.
2023-12-31 13:58:15 +11:00
3fb711cb42 Merge branch 'update-documents-avatar' of https://github.com/ashrafchowdury/documenso into update-documents-avatar 2023-12-29 19:34:46 +08:00
8a8a5510cb Merge branch 'main' into update-documents-avatar 2023-12-29 19:13:02 +06:00
ce67de9a1c refactor: changed component name for better readability 2023-12-29 19:29:13 +08:00
cf5841a895 Merge branch 'main' of https://github.com/ashrafchowdury/documenso into update-documents-avatar 2023-12-29 18:48:43 +08:00
2bc5d15af2 Merge branch 'main' into update-documents-avatar 2023-12-28 10:43:51 +06:00
d283cc2d26 chore: implemented feedback 2023-12-21 16:02:02 +02:00
6a56905fea chore: merged main 2023-12-21 10:14:07 +02:00
a22ada5f41 chore: add delete cascade 2023-12-20 14:44:43 +02:00
fb46b09e4f chore: small changes 2023-12-20 12:47:46 +02:00
2a448fe06d fix: fixed the recipients viewing issue on touch screens 2023-12-20 17:08:09 +08:00
17486b961d chore: refactor delete dialog 2023-12-19 15:51:43 +02:00
da03fc1fd0 chore: finishing touches 2023-12-18 12:24:42 +02:00
19736ce60b chore: implemented feedback 2023-12-14 11:05:39 +02:00
e79d385534 Merge branch 'main' into feat/public-api 2023-12-11 14:44:29 +02:00
8ecd8a7d10 chore: implemented feedback + a small refactoring 2023-12-11 14:33:30 +02:00
66c0db91da chore: cleanup and feedback implementation 2023-12-08 13:28:34 +00:00
54401b94ae chore: split api contract
moved the schemas from the api contract to a separate file
2023-12-08 09:58:23 +00:00
11ae6d3c16 chore: small changes 2023-12-06 16:53:34 +00:00
6c5526dd49 chore: update routes
trying to add the route for creating documents
2023-12-06 15:27:30 +00:00
936e75fd30 chore: merged main 2023-12-06 13:18:59 +00:00
6be4b7ae90 feat: add authorization for api calls 2023-11-30 14:39:31 +02:00
76800674ee feat: improve messaging 2023-11-29 14:57:27 +02:00
d43d40fd6b feat: improvements to the newly created token message 2023-11-29 14:43:26 +02:00
e1732de81d feat: show newly created token 2023-11-28 15:49:46 +02:00
6a5fc7a5fb feat: confirm to delete dialog 2023-11-28 12:37:01 +02:00
13997d3dca feat: add delete and copy token on token page 2023-11-27 16:29:24 +02:00
2deaad5c34 feat: token page 2023-11-27 12:50:21 +02:00
fbee6eedc1 feat: api token functions 2023-11-24 16:13:09 +02:00
80fe7ccdf5 feat: api token page in the settings 2023-11-24 13:59:33 +02:00
2ccede72ea chore: update the contract to add deleteDocument route 2023-11-23 15:23:47 +02:00
309b56168a feat: create the model for the api token 2023-11-23 15:21:13 +02:00
5c8a77ee8f chore: merged main 2023-11-23 12:05:28 +02:00
b3008fb272 feat: add route for retrieving a single document by id 2023-11-23 10:02:22 +02:00
6d6c93539f feat: update contract 2023-11-22 15:51:04 +02:00
4a6b3edc05 feat: get documents api route with pagination 2023-11-22 15:44:49 +02:00
24d9906557 feat: public api start 2023-11-22 15:03:15 +02:00
498 changed files with 24591 additions and 60585 deletions

View File

@ -10,7 +10,13 @@
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
],
"customizations": {
"vscode": {
"extensions": [
@ -25,8 +31,8 @@
"GitHub.copilot",
"GitHub.vscode-pull-request-github",
"Prisma.prisma",
"VisualStudioExptTeam.vscodeintellicode",
"VisualStudioExptTeam.vscodeintellicode"
]
}
}
}
}

View File

@ -1,13 +1,10 @@
#!/usr/bin/env bash
# Start the database and mailserver
docker compose -f ./docker/compose-without-app.yml up -d
# Install dependencies
npm install
# Copy the env file
cp .env.example .env
# Run the migrations
npm run prisma:migrate-dev
# Run the dev setup
npm run dx

View File

@ -22,10 +22,33 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# [[E2E Tests]]
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# [[SIGNING]]
# The transport to use for document signing. Available options: local (default) | gcloud-hsm
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
# OPTIONAL: The passphrase to use for the local file-based signing transport.
NEXT_PRIVATE_SIGNING_PASSPHRASE=
# OPTIONAL: The local file path to the .p12 file to use for the local signing transport.
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=
# OPTIONAL: The base64-encoded contents of the .p12 file to use for the local signing transport.
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS=
# OPTIONAL: The path to the Google Cloud HSM key to use for the gcloud-hsm signing transport.
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH=
# OPTIONAL: The path to the Google Cloud HSM public certificate file to use for the gcloud-hsm signing transport.
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH=
# OPTIONAL: The base64-encoded contents of the Google Cloud HSM public certificate file to use for the gcloud-hsm signing transport.
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS=
# OPTIONAL: The path to the Google Cloud Credentials file to use for the gcloud-hsm signing transport.
NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS=
# [[SIGNING]]
# OPTIONAL: Defines the signing transport to use. Available options: local (default)
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
# OPTIONAL: Defines the passphrase for the signing certificate.
NEXT_PRIVATE_SIGNING_PASSPHRASE=
# OPTIONAL: Defines the file contents for the signing certificate as a base64 encoded string.
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS=
# OPTIONAL: Defines the file path for the signing certificate. defaults to ./example/cert.p12
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=
# [[STORAGE]]
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
@ -90,6 +113,11 @@ NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
# OPTIONAL: Leave blank to allow users to signup through /signup page.
NEXT_PUBLIC_DISABLE_SIGNUP=
# [[E2E Tests]]
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# This is only required for the marketing site
# [[REDIS]]
NEXT_PRIVATE_REDIS_URL=

24
.github/actions/cache-build/action.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Cache production build binaries
description: 'Cache or restore if necessary'
inputs:
node_version:
required: false
default: v18.x
runs:
using: 'composite'
steps:
- name: Cache production build
uses: actions/cache@v3
id: production-build-cache
with:
path: |
${{ github.workspace }}/apps/web/.next
${{ github.workspace }}/apps/marketing/.next
**/.turbo/**
**/dist/**
key: prod-build-${{ github.run_id }}
restore-keys: prod-build-
- run: npm run build
shell: bash

39
.github/actions/node-install/action.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: 'Setup node and cache node_modules'
inputs:
node_version:
required: false
default: v18.x
runs:
using: 'composite'
steps:
- name: Set up Node ${{ inputs.node_version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Cache npm
uses: actions/cache@v3
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: modules-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
run: |
npm ci --no-audit
npm run prisma:generate
env:
HUSKY: '0'

View File

@ -0,0 +1,19 @@
name: Install playwright binaries
description: 'Install playwright, cache and restore if necessary'
runs:
using: 'composite'
steps:
- name: Cache playwright
id: cache-playwright
uses: actions/cache@v3
with:
path: |
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: playwright-${{ hashFiles('**/package-lock.json') }}
restore-keys: playwright-
- name: Install playwright
if: steps.cache-playwright.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
shell: bash

View File

@ -1,6 +1,7 @@
name: 'Continuous Integration'
on:
workflow_call:
push:
branches: ['main']
pull_request:
@ -10,9 +11,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
HUSKY: 0
jobs:
build_app:
name: Build App
@ -23,20 +21,12 @@ jobs:
with:
fetch-depth: 2
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- uses: ./.github/actions/node-install
- name: Copy env
run: cp .env.example .env
- name: Build
run: npm run build
- uses: ./.github/actions/cache-build
build_docker:
name: Build Docker Image
@ -47,5 +37,31 @@ jobs:
with:
fetch-depth: 2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build Docker Image
run: ./docker/build.sh
uses: docker/build-push-action@v5
with:
push: false
context: .
file: ./docker/Dockerfile
tags: documenso-${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

29
.github/workflows/clean-cache.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge

View File

@ -25,19 +25,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install Dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- name: Build Documenso
run: npm run build
- uses: ./.github/actions/node-install
- uses: ./.github/actions/cache-build
- name: Initialize CodeQL
uses: github/codeql-action/init@v2

View File

@ -6,29 +6,21 @@ on:
branches: ['main']
jobs:
e2e_tests:
name: "E2E Tests"
name: 'E2E Tests'
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- uses: ./.github/actions/node-install
- name: Start Services
run: npm run dx:up
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Generate Prisma Client
run: npm run prisma:generate -w @documenso/prisma
- uses: ./.github/actions/playwright-install
- name: Create the database
run: npm run prisma:migrate-dev
@ -36,6 +28,8 @@ jobs:
- name: Seed the database
run: npm run prisma:seed
- uses: ./.github/actions/cache-build
- name: Run Playwright tests
run: npm run ci
@ -43,7 +37,7 @@ jobs:
if: always()
with:
name: test-results
path: "packages/app-tests/**/test-results/*"
path: 'packages/app-tests/**/test-results/*'
retention-days: 30
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}

133
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,133 @@
name: Publish Docker
on:
push:
branches: ['release']
jobs:
build_and_publish_platform_containers:
name: Build and publish platform containers
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- warp-ubuntu-latest-x64-4x
- warp-ubuntu-latest-arm64-4x
steps:
- uses: actions/checkout@v4
with:
fetch-tags: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}
- name: Build the docker image
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
run: |
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
GIT_SHA="$(git rev-parse HEAD)"
docker build \
-f ./docker/Dockerfile \
--progress=plain \
-t "documenso/documenso-$BUILD_PLATFORM:latest" \
-t "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA" \
-t "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION" \
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:latest" \
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$GIT_SHA" \
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$APP_VERSION" \
.
- name: Push the docker image to DockerHub
run: docker push --all-tags "documenso/documenso-$BUILD_PLATFORM"
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
- name: Push the docker image to GitHub Container Registry
run: docker push --all-tags "ghcr.io/documenso/documenso-$BUILD_PLATFORM"
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
create_and_publish_manifest:
name: Create and publish manifest
runs-on: ubuntu-latest
needs: build_and_publish_platform_containers
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-tags: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}
- name: Create and push DockerHub manifest
run: |
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
GIT_SHA="$(git rev-parse HEAD)"
docker manifest create \
documenso/documenso:latest \
--amend documenso/documenso-amd64:latest \
--amend documenso/documenso-arm64:latest \
docker manifest create \
documenso/documenso:$GIT_SHA \
--amend documenso/documenso-amd64:$GIT_SHA \
--amend documenso/documenso-arm64:$GIT_SHA \
docker manifest create \
documenso/documenso:$APP_VERSION \
--amend documenso/documenso-amd64:$APP_VERSION \
--amend documenso/documenso-arm64:$APP_VERSION \
docker manifest push documenso/documenso:latest
docker manifest push documenso/documenso:$GIT_SHA
docker manifest push documenso/documenso:$APP_VERSION
- name: Create and push Github Container Registry manifest
run: |
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
GIT_SHA="$(git rev-parse HEAD)"
docker manifest create \
ghcr.io/documenso/documenso:latest \
--amend ghcr.io/documenso/documenso-amd64:latest \
--amend ghcr.io/documenso/documenso-arm64:latest \
docker manifest create \
ghcr.io/documenso/documenso:$GIT_SHA \
--amend ghcr.io/documenso/documenso-amd64:$GIT_SHA \
--amend ghcr.io/documenso/documenso-arm64:$GIT_SHA \
docker manifest create \
ghcr.io/documenso/documenso:$APP_VERSION \
--amend ghcr.io/documenso/documenso-amd64:$APP_VERSION \
--amend ghcr.io/documenso/documenso-arm64:$APP_VERSION \
docker manifest push ghcr.io/documenso/documenso:latest
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION

View File

@ -1,4 +1,16 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
echo "Copying pdf.js"
npm run copy:pdfjs --workspace apps/**
echo "Copying .well-known/ contents"
node "$MONOREPO_ROOT/scripts/copy-wellknown.cjs"
git add "$MONOREPO_ROOT/apps/web/public/"
git add "$MONOREPO_ROOT/apps/marketing/public/"
npx lint-staged

View File

@ -1,7 +1,9 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll": "explicit"
},
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"javascript.preferences.importModuleSpecifier": "non-relative",

7
.well-known/security.txt Normal file
View File

@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

View File

@ -1,5 +1,3 @@
>We are nominated for a Product Hunt Gold Kitty 😺✨ and appreciate any support: https://documen.so/kitty
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<p align="center" style="margin-top: 20px">
@ -29,20 +27,11 @@
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
</a>
<a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
<a href="CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
</p>
<div>
<img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
<img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/040cfbae-3438-4ca3-87f2-ce52c793dcaf">
<img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/72d445be-41e5-4936-bdba-87ef8e70fa09">
<img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/d7b86c0f-a755-4476-a022-a608db2c4633">
<img style="display: block; height: 120px; width: 24%"
src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
<div align="center">
<img src="https://github.com/documenso/documenso/assets/13398220/d96ed533-6f34-4a97-be9b-442bdb189c69" style="width: 80%;" />
</div>
## About this project
@ -107,7 +96,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
To run Documenso locally, you will need
- Node.js
- Node.js (v18 or above)
- Postgres SQL Database
- Docker (optional)
@ -209,7 +198,16 @@ If you're a visual learner and prefer to watch a video walkthrough of setting up
## Docker
🚧 Docker containers and images are current in progress. We are actively working on bringing a simple Docker build and publish pipeline for Documenso.
We provide a Docker container for Documenso, which is published on both DockerHub and GitHub Container Registry.
- DockerHub: [https://hub.docker.com/r/documenso/documenso](https://hub.docker.com/r/documenso/documenso)
- GitHub Container Registry: [https://ghcr.io/documenso/documenso](https://ghcr.io/documenso/documenso)
You can pull the Docker image from either of these registries and run it with your preferred container hosting provider.
Please note that you will need to provide environment variables for connecting to the database, mailserver, and so forth.
For detailed instructions on how to configure and run the Docker container, please refer to the [Docker README](./docker/README.md) in the `docker` directory.
## Self Hosting

View File

@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-01-25
Tags:
tags:
- Vision
- Mission
- Open Source

View File

@ -24,6 +24,8 @@ tags:
</figcaption>
</figure>
> 🔔 UPDATE: We launched <a href="https://documen.so/day1" target="_blank">teams</a> and the early adopters plan will be replaced by the new teams pricing as soon as all availible early adopters seats are filled.
## Community-Driven Development
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.

View File

@ -0,0 +1,64 @@
---
title: Launch Week II - Day 1 - Teams
description: Teams for Documenso are here. And they come free for early adopters!
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-02-26
tags:
- Launch Week
- Teams
- Early Adopter Perks
---
<video
id="vid"
width="100%"
src="https://github.com/documenso/design/assets/1309312/12a85ec7-20bb-4813-9714-e4da42c9cfba"
autoPlay
loop
muted
></video>
> TLDR; Docucmenso now supports teams that share documents, templates and a team mail address. Early Adopter get UNLIMITED<sup>1</sup> Users.
## Kicking off Launch Week II - "Connected"
The day has come! Roughly 5 months after kicked off our first launch week with open sourcing our design and Malfunction Mania, Launch Week #2 is here 🎉 This Launch Week's theme is "connected", since this is all about connecting humans, machines and documents.
Working with documents and getting that signature is a team sport. This is why we are kicking it off today with a very long-awaited feature: Documenso now supports teams!
## Introducing Teams for Documenso
You can now create teams next to your personal account: Simply invite your colleagues, and you can include everyone you like in working with your documents. With teams, you can:
- Send unlimited signature requests with unlimited recipients
- Create, view, edit and sign documents owned by the team
- Define a dedicated team email, to receive signing requests into a team inbox for the owner to sign
- Manage team roles: Member (Create+Edit), Managers (+Manage Team Members), Owner (+Transfer Team +Delete Team + Sign Documents sent to team email)
## Pricing
Together with Teams, we are announcing the new teams pricing:
- $10 per seat per month
- 5 seats minimum
- You can add seats dynamically as needed
This pricing will take effect, as soon as the early adopter seats run out. Want to check out teams: [https://documen.so/teams](https://documen.so/teams).
## Early Adopter Perks
There is one more point on pricing I have been looking forward to for a long time:
All early adopter plans now include **UNLIMITED teams and users**<sup>1</sup> . We appreciate your support so far very much, and I'm happy to announce this first of more early adopter perks to come. We have roughly 48 early adopter plans left, so if you plan to onboard your team, now is a great time to [grab your early adopter seat.](https://documen.so/claim-early-adopters-plan)
We are eager to hear from all teams users how you like this addition and what we can add to make it even better. Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here, and we would love to hear from you :)
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 1! Teams just dropped. Check it out https://documen.so/day1 🚀"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
Best from Hamburg\
Timur
\
[1] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com and ask for clarification if it's more than 100.

View File

@ -0,0 +1,76 @@
---
title: Launch Week II - Day 2 - Templates
description: Templates help you prepare regular documents faster. And you can share them with your team!
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-02-27
tags:
- Launch Week
- Templates
---
<video
id="vid"
width="100%"
src="https://github.com/documenso/design/assets/1309312/c9504db1-26b7-4033-88ed-a95cabd02e92"
autoPlay
loop
muted
></video>
> TLDR; You can now reuse documents via templates. More field types coming soon as well.
## Introducing Templates
It's day 2 of Launch Week, everybody 🙌 After introducing [Teams](https://documenso.com/blog/launch-week-2-day-1) yesterday, today we are looking at making Documenso faster for daily use:
We are launching templates for Documenso! Templates are an easy way to reuse documents you send out often with just a few clicks. With templates, you can:
<figure>
<MdxNextImage
src="/blog/quickfill.png"
width="1260"
height="630"
alt="Template recipients quick fill view"
/>
<figcaption className="text-center">
Quickly fill out recipients, when creating from a template
</figcaption>
</figure>
- Save often-uploaded documents for reuse
- Pre-define fields, so you just have to send the document
- Quickly fill out recipients and roles for new documents
- Share templates with your team to make working together even easier
<figure>
<MdxNextImage
src="/blog/template.png"
width="1260"
height="630"
alt="Create from template view"
/>
<figcaption className="text-center">
POV: You are a diligent german and create custom receipts with Documenso
</figcaption>
</figure>
## Pricing
Templates are **included in all Documenso Plans!** That includes our free tier: The limit of 5 documents per month still applies, but you are free to reach it with less friction using templates. Sharing templates with other users is only possible with the teams plan. If you want to share templates with people not in your team, we might have something coming up later this week 👀
## What's Next for Templates
We have a lot of great stuff coming up for templates as well:
- More Field Types are in the pipeline
- Sharing Templates Externally 👀
Check out templates [here](https://documen.so/templates) and let us know what you think and what we can improve. Which field types are you missing? Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 2! They just launched templates, and I'm pumped 🎉🚀🚀🚀. Check it out https://documen.so/day2"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
Best from Hamburg\
Timur

View File

@ -0,0 +1,53 @@
---
title: Launch Week II - Day 3 - API
description: Documenso's mission is to create a plattform developers all around the world can build upon. Today we are releasing the first version of our public API, included in all plans!
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-02-28
tags:
- Launch Week
- API
---
<video
id="vid"
width="100%"
src="https://github.com/documenso/design/assets/1309312/cb74d6cb-a127-4cac-a166-ad6b56c6140d"
autoPlay
loop
muted
></video>
> TLDR; The public API is now availible for all plans.
## Introducing the public Documenso API
Launch. Week. Day. 3 🎉 Documenso's mission is to create a platform that developers all around the world can build upon. Today we are releasing the first version of our public API, and we are pumped. Since this is the first version, we focused on the basics. With the new API you can:
- Get Documents (Individual or all Accessible)
- Upload Documents
- Delete Documents
- Create Documents from Templates
- Trigger Sending Documents for Singing
You can check out the detailed API documentation here:
> API DOCUMENTATION: [https://app.documenso.com/api/v1/openapi](https://app.documenso.com/api/v1/openapi)
## Pricing
We are building Documenso to be an open and extendable platform; therefore the API is included in all current plans. The API is authenticated via auth tokens, which every user can create at no extra cost, as can teams. Existing limits still apply (i.e., the number of included documents for the free plan). While we don't have all the details yet, we don't intend to price the API usage in itself (rather the accounts using it) since we want you to build on Documenso without being smothered by API costs.
> Try the API here for free: [https://documen.so/api](https://documen.so/api)
## What's next for the API
You tell us. This is by far the most requested feature, so we would like to hear from you. What should we add? How can we integrate even better?
Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 3! The public API is here 👀 Check it out https://documen.so/day3"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
Best from Hamburg\
Timur

View File

@ -0,0 +1,63 @@
---
title: Launch Week II - Day 4 - Webhooks and Zapier
description: If you want to integrate Documenso without fiddling with the API, we got you as well. You can now integrate Documenso via Zapier, included in all plans!
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-02-29
tags:
- Launch Week
- Zapier
- Webhooks
---
<video
id="vid"
width="100%"
src="https://github.com/documenso/design/assets/1309312/3b60789d-8d27-4c66-ae5c-179e33c2e3e6"
autoPlay
loop
muted
></video>
> TLDR; Zapier Integration is now available for all plans.
## Introducing Zapier for Documenso
Day 4 🥳 Yesterday we introduced our [public API](https://documen.so/day3) for developers to build on Documenso. If you are not a developer or simple want a quicker integration this is for you: Documenso now support Zapier Integrations! Just connect your Documenso account via a simple login flow with Zapier and you will have access to Zapier's universe of integrations 💫 The integration currently supports:
- Document Created ([https://documen.so/zapier-created](https://documen.so/zapier-created))
- Document Sent ([Chttps://documen.so/zapier-sent](https://documen.so/zapier-sent))
- Document Opened ([https://documen.so/zapier-opened](https://documen.so/zapier-opened))
- Document Signed ([https://documen.so/zapier-signed](https://documen.so/zapier-signed))
- Document Completed ([https://documen.so/zapier-completed](https://documen.so/zapier-completed))
> ⚡️ You can create your own Zaps here: https://zapier.com/apps/documenso/integrations
Each event comes with extensive meta-data for you to use in Zapier. Missing something? Reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord). We're always here and would love to hear from you :)
## Also Introducing for Documenso: Webhooks
To build the Zapier Integration, we needed a good webhooks concept, so we added that as well. Together with your Zaps, you can also now create customer webhooks in Documenso. You can try webhooks here for free: [https://documen.so/webhooks](https://documen.so/webhooks)
<figure>
<MdxNextImage
src="/blog/hooks.png"
width="1260"
height="630"
alt="Webhooks UI"
/>
<figcaption className="text-center">
Create unlimited custom webhooks with each plan.
</figcaption>
</figure>
## Pricing
Just like the API, we consider the Zapier integration and webhooks part of the open Documenso platform. Zapier is **available for all Documenso plans**, including free! [Login now](https://documen.so/login) to check it out.
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 4! Documenso now supports @Zapier 🤯 https://documen.so/day4"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
Best from Hamburg\
Timur

View File

@ -0,0 +1,61 @@
---
title: Launch Week II - Day 5 - Documenso Profiles
description: Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles will launch as soon as they are shiny.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-03-01
tags:
- Launch Week
- Profiles
---
<video
id="vid"
width="100%"
src="https://github.com/documenso/design/assets/1309312/89643ae2-2aa9-484c-a522-a0e35097c469"
autoPlay
loop
muted
></video>
> TLDR; Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles launch as soon as they are shiny.
## Introducing Documenso Profile Links
Day 5 - The Finale 🔥
Signing documents has always been between humans, and signing something together should be as frictionless as possible. It should also be async, so you don't force your counterpart to jog to their device to send something when you are ready. Today we are announcing the new Documenso Profiles:
<figure>
<MdxNextImage
src="/blog/profile.png"
width="1260"
height="813"
alt="Timur's Documenso Profile"
/>
<figcaption className="text-center">
Async > Sync: Add public templates to your Documenso Link and let people sign whenever they are ready.
</figcaption>
</figure>
Documenso profiles work with your existing templates. You can just add them to your public profile to let everyone with your link sign them. With profiles, we want to bring back the human aspect of signing.
By making profiles public, you can always access what your counterparty offers and make them more visible in the process. Long-term, we plan to add more to profiles to help you ensure the person you are dealing with is who they claim to be. Documenso wants to be the trust layer of the internet, and we want to start at the very fundamental level: The individual transaction.
Profiles are our first step towards bringing more trust into everything, simply by making the use of signing more frictionless. As there is more and more content of questionable origin out there, we want to support you in making it clear what you send out and what not.
## Pricing and Claiming
Documenso profile username can be claimed starting today. Documenso profiles will launch as soon as we are happy with the details ✨
- Long usernames (6 characters or more) come free with every account, e.g. **documenso.com/u/timurercan**
- Short usernames (5 characters or fewer) or less require any paid account ([Early Adopter](https://documen.so/claim-early-adopters-plan), [Teams](https://documen.so/teams) or Enterprise): **e.g., documenso.com/u/timur**
You can claim your username here: [https://documen.so/claim](https://documen.so/claim)
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 5! You can now claim your username for the upcoming profile links 😮 https://documen.so/day5"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
Best from Hamburg\
Timur

View File

@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-01-10
Tags:
tags:
- GitHub
- Backlog
- Roadmap

View File

@ -0,0 +1,292 @@
---
title: 'Building the Documenso Public API - The Why and How'
description: 'This article talks about the need for the public API and the process of building it. It also discusses the requirements we had to meet and the constraints we had to work within.'
authorName: 'Catalin'
authorImage: '/blog/blog-author-catalin.webp'
authorRole: 'I like to code and write'
date: 2024-03-08
tags:
- Development
- API
---
This article covers the process of building the public API for Documenso. It starts by explaining why the API was needed for a digital document signing company in the first place. Then, it'll dive into the steps we took to build it. Lastly, it'll present the requirements we had to meet and the constraints we had to work within.
## Why the public API
We decided to build the public API to open a new way of interacting with Documenso. While the web app does the job well, there are use cases where it's not enough. In those cases, the users might want to interact with the platform programmatically. Usually, that's for integrating Documenso with other applications.
With the new public API that's now possible. You can integrate Documenso's functionalities within other applications to automate tasks, create custom solutions, and build custom workflows, to name just a few.
The API provides 12 endpoints at the time of writing this article:
- (GET) `/api/v1/documents` - retrieve all the documents
- (POST) `/api/v1/documents` - upload a new document and getting a presigned URL
- (GET) `/api/v1/documents/{id}` - fetch a specific document
- (DELETE) `/api/v1/documents/{id}` - delete a specific document
- (POST) `/api/v1/templates/{templateId}/create-document` - create a new document from an existing template
- (POST) `/api/v1/documents/{id}/send` - send a document for signing
- (POST) `/api/v1/documents/{id}/recipients` - create a document recipient
- (PATCH) `/api/v1/documents/{id}/recipients/{recipientId}` - update the details of a document recipient
- (DELETE) `/api/v1/documents/{id}/recipients/{recipientId}` - delete a specific recipient from a document
- (POST) `/api/v1/documents/{id}/fields` - create a field for a document
- (PATCH) `/api/v1/documents/{id}/fields` - update the details of a document field
- (DELETE) `/api/v1/documents/{id}/fields` - delete a field from a document
> Check out the [API documentation](https://app.documenso.com/api/v1/openapi).
Moreover, it also enables us to enhance the platform by bringing other integrations to Documenso, such as Zapier.
In conclusion, the new public API extends Documenso's capabilities, provides more flexibility for users, and opens up a broader world of possibilities.
## Picking the right approach & tech
Once we decided to build the API, we had to choose the approach and technologies to use. There were 2 options:
1. Build an additional application
2. Launch the API in the existing codebase
### 1. Build an additional application
That would mean creating a new codebase and building the API from scratch. Having a separate app for the API would result in benefits such as:
- lower latency responses
- supporting larger field uploads
- separation between the apps (Documenso and the API)
- customizability and flexibility
- easier testing and debugging
This approach has significant benefits. However, one major drawback is that it requires additional resources.
We'd have to spend a lot of time just on the core stuff, such as building and configuring the basic server. After that, we'd spend time implementing the endpoints and authorization, among other things. When the building is done, there will be another application to deploy and manage. All of this would stretch our already limited resources.
So, we asked ourselves if there is another way of doing it without sacrificing the API quality and the developer experience.
### 2. Launch the API in the existing codebase
The other option was to launch the API in the existing codebase. Rather than writing everything from scratch, we could use most of our existing code.
Since we're using tRPC for our internal API (backend), we looked for solutions that work well with tRPC. We narrowed down the choices to:
- [trpc-openapi](https://github.com/jlalmes/trpc-openapi)
- [ts-rest](https://ts-rest.com/)
Both technologies allow you to build public APIs. The `trpc-openapi` technology allows you to easily turn tRPC procedures into REST endpoints. It's more like a plugin for tRPC.
On the other hand, `ts-rest` is more of a standalone solution. `ts-rest` enables you to create a contract for the API, which can be used both on the client and server. You can consume and implement the contract in your application, thus providing end-to-end type safety and RPC-like client.
> You can see a [comparison between trpc-openapi and ts-rest](https://catalins.tech/public-api-trpc/) here.
So, the main difference between the 2 is that `trpc-openapi` is like a plugin that extends tRPC's capabilities, whereas `ts-rest` provides the tools for building a standalone API.
### Our choice
After analyzing and comparing the 2 options, we decided to go with `ts-rest` because of its benefits. Here's a paragraph from the `ts-rest` documentation that hits the nail on the head:
> tRPC has many plugins to solve this issue by mapping the API implementation to a REST-like API, however, these approaches are often a bit clunky and reduce the safety of the system overall, ts-rest does this heavy lifting in the client and server implementations rather than requiring a second layer of abstraction and API endpoint(s) to be defined.
## API Requirements
We defined the following requirements for the API:
- The API should use path-based versioning (e.g. `/v1`)
- The system should use bearer tokens for API authentication
- The API token should be a random string of 32 to 40 characters
- The system should hash the token and store the hashed value
- The system should only display the API token when it's created
- The API should have self-generated documentation like Swagger
- Users should be able to create an API key
- Users should be able to choose a token name
- Users should be able to choose an expiration date for the token
- User should be able to choose between 7 days, 1 month, 3 months, 6 months, 12 months, never
- System should display all the user's tokens in the settings page
- System should display the token name, creation date, expiration date and a delete button
- Users should be able to delete an API key
- Users should be able to retrieve all the documents from their account
- Users should be able to upload a new document
- Users should receive an S3 pre-signed URL after a successful upload
- Users should be able to retrieve a specific document from their account by its id
- Users should be able to delete a specific document from their account by its id
- Users should be able to create a new document from an existing document template
- Users should be able to send a document for signing to 1 or more recipients
- Users should be able to create a recipient for a document
- Users should be able to update the details of a recipient
- Users should be able to delete a recipient from a document
- Users should be able to create a field (e.g. signature, email, name, date) for a document
- Users should be able to update a field for a document
- Users should be able to delete a field from a document
## Constraints
We also faced the following constraints while developing the API:
**1. Resources**
Limited resources were one of the main constraints. We're a new startup with a relatively small team. Building and maintaining an additional application would strain our limited resources.
**2. Technology stack**
Another constraint was the technology stack. Our tech stack includes TypeScript, Prisma, and tRPC, among others. We also use Vercel for hosting.
As a result, we wanted to use technologies we are comfortable with. This allowed us to leverage our existing knowledge and ensured consistency across our applications.
Using familiar technologies also meant we could develop the API faster, as we didn't have to spend time learning new technologies. We could also leverage existing code and tools used in our main application.
It's worth mentioning that this is not a permanent decision. We're open to moving the API to another codebase/tech stack when it makes sense (e.g. API is heavily used and needs better performance).
**3. File uploads**
Due to our current architecture, we support file uploads with a maximum size of 50 MB. To circumvent this, we created an additional step for uploading documents.
Users make a POST request to the `/api/v1/documents` endpoint and the API responds with an S3 pre-signed URL. The users then make a 2nd request to the pre-signed URL with their document.
## How we built the API
![API package diagram](api-package.webp)
Our codebase is a monorepo, so we created a new API package in the `packages` directory. It contains both the API implementation and its documentation. The main 2 blocks of the implementation consist of the API contract and the code for the API endpoints.
![API implementation diagram](api-implementation.webp)
In a few words, the API contract defines the API structure, the format of the requests and responses, how to authenticate API calls, the available endpoints and their associated HTTP verbs. You can explore the [API contract](https://github.com/documenso/documenso/blob/main/packages/api/v1/contract.ts) on GitHub.
Then, there's the implementation part, which is the actual code for each endpoint defined in the API contract. The implementation is where the API contract is brought to life and made functional.
Let's take the endpoint `/api/v1/documents` as an example.
```ts
export const ApiContractV1 = c.router(
{
getDocuments: {
method: 'GET',
path: '/api/v1/documents',
query: ZGetDocumentsQuerySchema,
responses: {
200: ZSuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
summary: 'Get all documents',
},
...
}
);
```
The API contract specifies the following things for `getDocuments`:
- the allowed HTTP request method is GET, so trying to make a POST request, for example, results in an error
- the path is `/api/v1/documents`
- the query parameters the user can pass with the request
- in this case - `page` and `perPage`
- the allowed responses and their schema
- `200` returns an object containing an array of all documents and a field `totalPages`, which is self-explanatory
- `401` returns an object with a message such as "Unauthorized"
- `404` returns an object with a message such as "Not found"
The implementation of this endpoint needs to match the contract completely; otherwise, `ts-rest` will complain, and your API might not work as intended.
The `getDocuments` function from the `implementation.ts` file runs when the user hits the endpoint.
```ts
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
getDocuments: authenticatedMiddleware(async (args, user, team) => {
const page = Number(args.query.page) || 1;
const perPage = Number(args.query.perPage) || 10;
const { data: documents, totalPages } = await findDocuments({
page,
perPage,
userId: user.id,
teamId: team?.id,
});
return {
status: 200,
body: {
documents,
totalPages,
},
};
}),
...
});
```
There is a middleware, too, `authenticatedMiddleware`, that handles the authentication for API requests. It ensures that the API token exists and the token used has the appropriate privileges for the resource it accesses.
That's how the other endpoints work as well. The code differs, but the principles are the same. You can explore the [API implementation](https://github.com/documenso/documenso/blob/main/packages/api/v1/implementation.ts) and the [middleware code](https://github.com/documenso/documenso/blob/main/packages/api/v1/middleware/authenticated.ts) on GitHub.
### Documentation
For the documentation, we decided to use Swagger UI, which automatically generates the documentation from the OpenAPI specification.
The OpenAPI specification describes an API containing the available endpoints and their HTTP request methods, authentication methods, and so on. Its purpose is to help both machines and humans understand the API without having to look at the code.
The Documenso OpenAPI specification is live [here](https://documenso.com/api/v1/openapi.json).
Thankfully, `ts-rest` makes it seamless to generate the OpenAPI specification.
```ts
import { generateOpenApi } from '@ts-rest/open-api';
import { ApiContractV1 } from './contract';
export const OpenAPIV1 = generateOpenApi(
ApiContractV1,
{
info: {
title: 'Documenso API',
version: '1.0.0',
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
},
},
{
setOperationId: true,
},
);
```
Then, the Swagger UI takes the OpenAPI specification as a prop and generates the documentation. The code below shows the component responsible for generating the documentation.
```ts
'use client';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
export const OpenApiDocsPage = () => {
return <SwaggerUI spec={OpenAPIV1} displayOperationId={true} />;
};
export default OpenApiDocsPage;
```
Lastly, we create an API endpoint to display the Swagger documentation. The code below dynamically imports the `OpenApiDocsPage` component and displays it.
```ts
'use client';
import dynamic from 'next/dynamic';
const Docs = dynamic(async () => import('@documenso/api/v1/api-documentation'), {
ssr: false,
});
export default function OpenApiDocsPage() {
return <Docs />;
}
```
You can access and play around with the documentation at [documenso.com/api/v1/openapi](https://documenso.com/api/v1/openapi). You should see a page like the one shown in the screenshot below.
![The documentation for the Documenso API](docs.webp)
> This article shows how to [generate Swagger documentation for a Next.js API](https://catalins.tech/generate-swagger-documentation-next-js-api/).
So, that's how we went about building the first iteration of the public API after taking into consideration all the constraints and the current needs. The [GitHub pull request for the API](https://github.com/documenso/documenso/pull/674) is publicly available on GitHub. You can go through it at your own pace.
## Conclusion
The current architecture and approach work well for our current stage and needs. However, as we continue to grow and evolve, our architecture and approach will likely need to adapt. We monitor API usage and performance regularly and collect feedback from users. This enables us to find areas for improvement, understand our users' needs, and make informed decisions about the next steps.

View File

@ -0,0 +1,56 @@
---
title: 'Embracing the Future and Moving Back Again: From Server Actions to tRPC'
description: 'This article talks about the need for the public API and the process of building it. It also talks about the requirements, constraints, challenges and lessons learnt from building it.'
authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder'
date: 2024-03-07
tags:
- Development
---
On October 26, 2022, during the Next.js Conf, Vercel unveiled Next.js 13, introducing a bunch of new features and a whole new paradigm shift for creating Next.js apps.
React Server Components (RSCs) have been known for some time but haven't yet been implemented in a React meta-framework. With Next.js now adding them, the world of server-side rendering React applications was about to change.
## The promise of React Server Components
Described by Vercel as a tool for writing UIs rendered or optionally cached on the server, RSC promised direct integration with databases and server-specific capabilities, a departure from the typical React and SSR models.
This feature initially appeared as a game-changer, promising simpler data fetching and routing, thanks to async server components and additional nested layout support.
After experimenting with it for about six months on other projects, I came to the conclusion that while it was a little rough at the edges, it was indeed a game changer and something that should be adopted in Next.js projects moving forward.
## A new new paradigm: Server Actions
Vercel didn't stop at adding Server Components. A short while after the initial Next.js 13 release, they introduced "Server Actions." Server Actions allow for the calling of server-side functions without API routes, reducing the amount of ceremony required for adding a new mutation or event to your application.
## Betting on new technology
Following the release of both Server Actions and Server Components, we at Documenso embarked on a rewrite of the application. We had accumulated a bit of technical debt from the initial MVP, which was best shed as we also improved the design with help from our new designer.
Having had much success with Server Components on other projects, I opted to go all in on both Server Components and Server Actions for the new version of the application. I was excited to see how much simpler and more efficient the application would be with these new technologies.
Following our relaunch, we felt quite happy with our choices of server actions, which even let us cocktail certain client—and server-side logic into a single component without needing a bunch of effort on our end.
## Navigating challenges with Server Actions
While initially successful, we soon encountered issues with Server Actions, particularly when monitoring the application. Unfortunately, server actions make it much harder to monitor network requests and identify failed server actions since they use the route that they are currently on.
Despite this issue, things were manageable; however, a critical issue arose when the usage of server actions caused bundle corruption, resulting in a top-level await insertion that would cause builds and tests to fail.
This last issue was a deal breaker since it highlighted how much magic we were relying on with server actions. I like to avoid adding more magic where possible since, with bundlers and meta-frameworks, we are already handing off a lot of heavy lifting and getting a lot of magic in return.
## Going all in on tRPC
My quick and final solution to the issues we were facing was to fully embrace [tRPC](https://trpc.io/), which we already used in other app areas. The migration took less than a day and resolved all our issues while adding a lot more visibility to failing actions since they now had a dedicated API route.
For those unfamiliar with tRPC, it is a framework for building end-to-end typesafe APIs with TypeScript and Next.js. It's an awesome library that lets you call your defined API safely within the client, causing build time errors when things inevitably drift.
I've been a big fan of tRPC for some time now, so the decision to drop server actions and instead use more of tRPC was easy for me.
## Lessons learned and the future
While I believe server actions are a great idea, and I'm sure they will be improved in the future, I've relearned the lesson that it's best to avoid magic where possible.
This doesn't mean we won't consider moving certain things back to server actions in the future. But for now, we will wait and watch how others use them and how they evolve.

View File

@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-02-06
Tags:
tags:
- Founders
- Mission
- Open Source

View File

@ -12,6 +12,7 @@ export const BlogPost = defineDocumentType(() => ({
authorName: { type: 'string', required: true },
authorImage: { type: 'string', required: false },
authorRole: { type: 'string', required: true },
cta: { type: 'boolean', required: false, default: true },
},
computedFields: {
href: { type: 'string', resolve: (post) => `/${post._raw.flattenedPath}` },

View File

@ -21,6 +21,7 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
const config = {
experimental: {
outputFileTracingRoot: path.join(__dirname, '../../'),
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign'],
serverActions: {
bodySizeLimit: '50mb',
},

View File

@ -36,7 +36,7 @@
"react-hook-form": "^7.43.9",
"react-icons": "^4.11.0",
"recharts": "^2.7.2",
"sharp": "0.33.1",
"sharp": "^0.33.1",
"typescript": "5.2.2",
"zod": "^3.22.4"
},

View File

@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because one or more lines are too long

View File

@ -5,14 +5,13 @@ import { allDocuments } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
export const generateStaticParams = () =>
allDocuments.map((post) => ({ post: post._raw.flattenedPath }));
export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { content: string } }) => {
const document = allDocuments.find((post) => post._raw.flattenedPath === params.content);
const document = allDocuments.find((doc) => doc._raw.flattenedPath === params.content);
if (!document) {
notFound();
return { title: 'Not Found' };
}
return { title: document.title };

View File

@ -1,25 +1,21 @@
import { ImageResponse } from 'next/og';
import { allBlogPosts } from 'contentlayer/generated';
import { NextResponse } from 'next/server';
export const runtime = 'edge';
export const contentType = 'image/png';
export const IMAGE_SIZE = {
const IMAGE_SIZE = {
width: 1200,
height: 630,
};
type BlogPostOpenGraphImageProps = {
params: { post: string };
};
export async function GET(_request: Request) {
const url = new URL(_request.url);
export default async function BlogPostOpenGraphImage({ params }: BlogPostOpenGraphImageProps) {
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
const title = url.searchParams.get('title');
const author = url.searchParams.get('author');
if (!blogPost) {
return null;
if (!title || !author) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// The long urls are needed for a compiler optimisation on the Next.js side, lifting this up
@ -49,10 +45,10 @@ export default async function BlogPostOpenGraphImage({ params }: BlogPostOpenGra
<img src={logoImage} alt="logo" tw="h-8" />
<h1 tw="mt-8 text-6xl text-center flex items-center justify-center w-full max-w-[800px] font-bold text-center mx-auto">
{blogPost.title}
{title}
</h1>
<p tw="font-normal">Written by {blogPost.authorName}</p>
<p tw="font-normal">Written by {author}</p>
</div>
),
{

View File

@ -7,21 +7,36 @@ import { ChevronLeft } from 'lucide-react';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
export const generateStaticParams = () =>
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
import { CallToAction } from '~/components/(marketing)/call-to-action';
export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { post: string } }) => {
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
if (!blogPost) {
notFound();
return {
title: 'Not Found',
};
}
// Use the url constructor to ensure that things are escaped as they should be
const searchParams = new URLSearchParams({
title: blogPost.title,
author: blogPost.authorName,
});
return {
title: {
absolute: `${blogPost.title} - Documenso Blog`,
},
description: blogPost.description,
openGraph: {
images: [`${blogPost.href}/opengraph?${searchParams.toString()}`],
},
twitter: {
images: [`${blogPost.href}/opengraph?${searchParams.toString()}`],
},
};
};
@ -41,53 +56,57 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
const MDXContent = useMDXComponent(post.body.code);
return (
<article className="prose dark:prose-invert mx-auto py-8">
<div className="mb-6 text-center">
<time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
{new Date(post.date).toLocaleDateString()}
</time>
<div>
<article className="prose dark:prose-invert mx-auto py-8">
<div className="mb-6 text-center">
<time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
{new Date(post.date).toLocaleDateString()}
</time>
<h1 className="text-3xl font-bold">{post.title}</h1>
<h1 className="text-3xl font-bold">{post.title}</h1>
<div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4">
<div className="bg-foreground h-10 w-10 rounded-full">
{post.authorImage && (
<img
src={post.authorImage}
alt={`Image of ${post.authorName}`}
className="bg-foreground/10 h-10 w-10 rounded-full"
/>
)}
</div>
<div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4">
<div className="bg-foreground h-10 w-10 rounded-full">
{post.authorImage && (
<img
src={post.authorImage}
alt={`Image of ${post.authorName}`}
className="bg-foreground/10 h-10 w-10 rounded-full"
/>
)}
</div>
<div className="text-sm leading-6">
<p className="text-foreground text-left font-semibold">{post.authorName}</p>
<p className="text-muted-foreground">{post.authorRole}</p>
<div className="text-sm leading-6">
<p className="text-foreground text-left font-semibold">{post.authorName}</p>
<p className="text-muted-foreground">{post.authorRole}</p>
</div>
</div>
</div>
</div>
<MDXContent components={mdxComponents} />
<MDXContent components={mdxComponents} />
{post.tags.length > 0 && (
<ul className="not-prose flex list-none flex-row space-x-2 px-0">
{post.tags.map((tag, i) => (
<li
key={`tag-${i}`}
className="bg-muted hover:bg-muted/60 text-foreground relative z-10 whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-medium"
>
{tag}
</li>
))}
</ul>
)}
{post.tags.length > 0 && (
<ul className="not-prose flex list-none flex-row space-x-2 px-0">
{post.tags.map((tag, i) => (
<li
key={`tag-${i}`}
className="bg-muted hover:bg-muted/60 text-foreground relative z-10 whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-medium"
>
{tag}
</li>
))}
</ul>
)}
<hr />
<hr />
<Link href="/blog" className="text-muted-foreground flex items-center hover:opacity-60">
<ChevronLeft className="mr-2 h-6 w-6" />
Back to all posts
</Link>
</article>
<Link href="/blog" className="text-muted-foreground flex items-center hover:opacity-60">
<ChevronLeft className="mr-2 h-6 w-6" />
Back to all posts
</Link>
</article>
{post.cta && <CallToAction className="mt-8" utmSource={`blog_${params.post}`} />}
</div>
);
}

View File

@ -5,6 +5,7 @@ import { allBlogPosts } from 'contentlayer/generated';
export const metadata: Metadata = {
title: 'Blog',
};
export default function BlogPage() {
const blogPosts = allBlogPosts.sort((a, b) => {
const dateA = new Date(a.date);

View File

@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { ArrowRight } from 'lucide-react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
@ -12,6 +13,8 @@ import { Button } from '@documenso/ui/primitives/button';
import { PasswordReveal } from '~/components/(marketing)/password-reveal';
export const dynamic = 'force-dynamic';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
@ -175,11 +178,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
This is a temporary password. Please change it as soon as possible.
</p>
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`}
target="_blank"
className="mt-4 block"
>
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signin`} target="_blank" className="mt-4 block">
<Button size="lg" className="text-base">
Let's get started!
<ArrowRight className="ml-2 h-5 w-5" />

View File

@ -2,8 +2,12 @@
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Footer } from '~/components/(marketing)/footer';
@ -17,6 +21,10 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
const [scrollY, setScrollY] = useState(0);
const pathname = usePathname();
const { getFlag } = useFeatureFlags();
const showProfilesAnnouncementBar = getFlag('marketing_profiles_announcement_bar');
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
@ -30,7 +38,8 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
return (
<div
className={cn('relative flex min-h-[100vh] max-w-[100vw] flex-col pt-20 md:pt-28', {
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
'overflow-y-auto overflow-x-hidden':
pathname && !['/singleplayer', '/pricing'].includes(pathname),
})}
>
<div
@ -38,6 +47,31 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
'bg-background/50 backdrop-blur-md': scrollY > 5,
})}
>
{showProfilesAnnouncementBar && (
<div className="relative inline-flex w-full items-center justify-center overflow-hidden px-4 py-2.5">
<div className="absolute inset-0 -z-[1]">
<Image
src={launchWeekTwoImage}
className="h-full w-full object-cover"
alt="Launch Week 2"
/>
</div>
<div className="text-background text-center text-sm text-white">
Claim your documenso public profile username now!{' '}
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
<a
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=marketing-announcement-bar`}
className="bg-background text-foreground rounded-md px-2.5 py-1 text-xs font-medium duration-300"
>
Claim Now
</a>
</div>
</div>
</div>
)}
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
</div>

View File

@ -1,11 +1,10 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils';
export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
data: T;
@ -34,13 +33,13 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
.reverse();
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>
</div>
<div className={className} {...props}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={chartHeight}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />

View File

@ -1,11 +1,10 @@
'use client';
import { HTMLAttributes, useEffect, useState } from 'react';
import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts';
import { cn } from '@documenso/ui/lib/utils';
import { CAP_TABLE } from './data';
const COLORS = ['#7fd843', '#a2e771', '#c6f2a4'];
@ -48,10 +47,12 @@ export const CapTable = ({ className, ...props }: CapTableProps) => {
setIsSSR(false);
}, []);
return (
<div className={cn('flex flex-col', className)} {...props}>
<h3 className="px-4 text-lg font-semibold">Cap Table</h3>
<div className={className} {...props}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Cap Table</h3>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border shadow-sm hover:shadow">
{!isSSR && (
<PieChart width={400} height={400}>
<Pie

View File

@ -1,11 +1,10 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils';
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
@ -18,10 +17,12 @@ export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps)
}));
return (
<div className={cn('flex flex-col', className)} {...props}>
<h3 className="px-4 text-lg font-semibold">Total Funding Raised</h3>
<div className={className} {...props}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Total Funding Raised</h3>
</div>
<div className="border-border mt-2.5 flex flex-1 flex-col items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData} margin={{ top: 40, right: 40, bottom: 20, left: 40 }}>
<XAxis dataKey="date" />

View File

@ -0,0 +1,56 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
export type MonthlyCompletedDocumentsChartProps = {
className?: string;
data: GetUserMonthlyGrowthResult;
};
export const MonthlyCompletedDocumentsChart = ({
className,
data,
}: MonthlyCompletedDocumentsChartProps) => {
const formattedData = [...data].reverse().map(({ month, count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
count: Number(count),
};
});
return (
<div className={className}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Completed Documents per Month</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Completed Documents']}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Completed Documents"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -4,7 +4,6 @@ import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
import { cn } from '@documenso/ui/lib/utils';
export type MonthlyNewUsersChartProps = {
className?: string;
@ -14,24 +13,27 @@ export type MonthlyNewUsersChartProps = {
export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartProps) => {
const formattedData = [...data].reverse().map(({ month, count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'),
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
count: Number(count),
};
});
return (
<div className={cn('flex flex-col', className)}>
<div className="flex items-center px-4">
<h3 className="text-lg font-semibold">New Users</h3>
</div>
<div className={className}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">New Users</h3>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value) => [Number(value).toLocaleString('en-US'), 'New Users']}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>

View File

@ -4,7 +4,6 @@ import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
import { cn } from '@documenso/ui/lib/utils';
export type MonthlyTotalUsersChartProps = {
className?: string;
@ -14,24 +13,27 @@ export type MonthlyTotalUsersChartProps = {
export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersChartProps) => {
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'),
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
count: Number(count),
};
});
return (
<div className={cn('flex flex-col', className)}>
<div className="flex items-center px-4">
<h3 className="text-lg font-semibold">Total Users</h3>
</div>
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Total Users</h3>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Total Users']}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>

View File

@ -2,19 +2,24 @@ import type { Metadata } from 'next';
import { z } from 'zod';
import { getCompletedDocumentsMonthly } from '@documenso/lib/server-only/user/get-monthly-completed-document';
import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-monthly-growth';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
import { MetricCard } from '~/app/(marketing)/open/metric-card';
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
import { CallToAction } from '~/components/(marketing)/call-to-action';
import { BarMetric } from './bar-metrics';
import { CapTable } from './cap-table';
import { FundingRaised } from './funding-raised';
import { MetricCard } from './metric-card';
import { MonthlyCompletedDocumentsChart } from './monthly-completed-documents-chart';
import { MonthlyNewUsersChart } from './monthly-new-users-chart';
import { MonthlyTotalUsersChart } from './monthly-total-users-chart';
import { SalaryBands } from './salary-bands';
import { TeamMembers } from './team-members';
import { OpenPageTooltip } from './tooltip';
import { TotalSignedDocumentsChart } from './total-signed-documents-chart';
import { Typefully } from './typefully';
export const metadata: Metadata = {
title: 'Open Startup',
@ -129,118 +134,149 @@ export default async function OpenPage() {
{ total_count: mergedPullRequests },
STARGAZERS_DATA,
EARLY_ADOPTERS_DATA,
MONTHLY_USERS,
MONTHLY_COMPLETED_DOCUMENTS,
] = await Promise.all([
fetchGithubStats(),
fetchOpenIssues(),
fetchMergedPullRequests(),
fetchStargazers(),
fetchEarlyAdopters(),
getUserMonthlyGrowth(),
getCompletedDocumentsMonthly(),
]);
const MONTHLY_USERS = await getUserMonthlyGrowth();
return (
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
<div>
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
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:{' '}
<a className="font-bold" href="https://documenso.com/blog/pre-seed" target="_blank">
Announcing Open Metrics
</a>
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
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:{' '}
<a
className="font-bold"
href="https://documenso.com/blog/pre-seed"
target="_blank"
rel="noreferrer"
>
Announcing Open Metrics
</a>
</p>
</div>
<div className="my-12 grid grid-cols-12 gap-8">
<div className="col-span-12 grid grid-cols-4 gap-4">
<MetricCard
className="col-span-2 lg:col-span-1"
title="Stargazers"
value={stargazersCount.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Forks"
value={forksCount.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Open Issues"
value={openIssues.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Merged PR's"
value={mergedPullRequests.toLocaleString('en-US')}
/>
</div>
<TeamMembers className="col-span-12" />
<SalaryBands className="col-span-12" />
</div>
<h2 className="px-4 text-2xl font-semibold">Finances</h2>
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
<CapTable className="col-span-12 lg:col-span-6" />
</div>
<h2 className="px-4 text-2xl font-semibold">Community</h2>
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="stars"
title="GitHub: Total Stars"
label="Stars"
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="mergedPRs"
title="GitHub: Total Merged PRs"
label="Merged PRs"
chartHeight={400}
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="forks"
title="GitHub: Total Forks"
label="Forks"
chartHeight={400}
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="openIssues"
title="GitHub: Total Open Issues"
label="Open Issues"
chartHeight={400}
className="col-span-12 lg:col-span-6"
/>
<Typefully className="col-span-12 lg:col-span-6" />
</div>
<h2 className="px-4 text-2xl font-semibold">Growth</h2>
<div className="mb-12 mt-4 grid grid-cols-12 gap-8">
<BarMetric<EarlyAdoptersType>
data={EARLY_ADOPTERS_DATA}
metricKey="earlyAdopters"
title="Early Adopters"
label="Early Adopters"
className="col-span-12 lg:col-span-6"
extraInfo={<OpenPageTooltip />}
/>
<MonthlyTotalUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
<MonthlyNewUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
<MonthlyCompletedDocumentsChart
data={MONTHLY_COMPLETED_DOCUMENTS}
className="col-span-12 lg:col-span-6"
/>
<TotalSignedDocumentsChart
data={MONTHLY_COMPLETED_DOCUMENTS}
className="col-span-12 lg:col-span-6"
/>
</div>
</div>
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Is there more?</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
This page is evolving as we learn what makes a great signing company. We'll update it when
we have more to share.
</p>
</div>
<div className="mt-12 grid grid-cols-12 gap-8">
<div className="col-span-12 grid grid-cols-4 gap-4">
<MetricCard
className="col-span-2 lg:col-span-1"
title="Stargazers"
value={stargazersCount.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Forks"
value={forksCount.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Open Issues"
value={openIssues.toLocaleString('en-US')}
/>
<MetricCard
className="col-span-2 lg:col-span-1"
title="Merged PR's"
value={mergedPullRequests.toLocaleString('en-US')}
/>
</div>
<TeamMembers className="col-span-12" />
<SalaryBands className="col-span-12" />
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
<CapTable className="col-span-12 lg:col-span-6" />
<BarMetric<EarlyAdoptersType>
data={EARLY_ADOPTERS_DATA}
metricKey="earlyAdopters"
title="Early Adopters"
label="Early Adopters"
className="col-span-12 lg:col-span-6"
extraInfo={<OpenPageTooltip />}
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="stars"
title="Github: Total Stars"
label="Stars"
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="mergedPRs"
title="Github: Total Merged PRs"
label="Merged PRs"
chartHeight={300}
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="forks"
title="Github: Total Forks"
label="Forks"
chartHeight={300}
className="col-span-12 lg:col-span-6"
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA}
metricKey="openIssues"
title="Github: Total Open Issues"
label="Open Issues"
chartHeight={300}
className="col-span-12 lg:col-span-6"
/>
<MonthlyTotalUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
<MonthlyNewUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Where's the rest?</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center text-lg leading-normal">
We're still working on getting all our metrics together. We'll update this page as soon
as we have more to share.
</p>
</div>
</div>
<CallToAction className="mt-12" utmSource="open-page" />
</div>
);
}

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import {

View File

@ -0,0 +1,56 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
export type TotalSignedDocumentsChartProps = {
className?: string;
data: GetUserMonthlyGrowthResult;
};
export const TotalSignedDocumentsChart = ({ className, data }: TotalSignedDocumentsChartProps) => {
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
count: Number(count),
};
});
return (
<div className={className}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Total Completed Documents</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value) => [
Number(value).toLocaleString('en-US'),
'Total Completed Documents',
]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Total Completed Documents"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -0,0 +1,40 @@
'use client';
import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { FaXTwitter } from 'react-icons/fa6';
import { Button } from '@documenso/ui/primitives/button';
export type TypefullyProps = HTMLAttributes<HTMLDivElement>;
export const Typefully = ({ className, ...props }: TypefullyProps) => {
return (
<div className={className} {...props}>
<div className="border-border flex flex-col justify-center rounded-2xl border p-6 pl-2 shadow-sm hover:shadow">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">Twitter Stats</h3>
</div>
<div className="my-12 flex flex-col items-center gap-y-4 text-center">
<FaXTwitter className="h-12 w-12" />
<Link href="https://typefully.com/documenso/stats" target="_blank">
<h1>Documenso on X</h1>
</Link>
<Button className="rounded-full" size="sm" asChild>
<Link href="https://typefully.com/documenso/stats" target="_blank">
View all stats
</Link>
</Button>
<Button className="rounded-full bg-white" size="sm" asChild>
<Link href="https://twitter.com/documenso" target="_blank">
Follow us on X
</Link>
</Button>
</div>
</div>
</div>
);
};

View File

@ -15,6 +15,8 @@ export const metadata: Metadata = {
title: 'Pricing',
};
export const dynamic = 'force-dynamic';
export type PricingPageProps = {
searchParams?: {
planId?: string;
@ -53,7 +55,7 @@ export default function PricingPage() {
<div className="mt-4 flex justify-center">
<Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild>
<Link href="https://github.com/documenso/documenso" target="_blank">
<Link href="https://github.com/documenso/documenso" target="_blank" rel="noreferrer">
Get Started
</Link>
</Button>
@ -166,6 +168,7 @@ export default function PricingPage() {
<Link
className="text-documenso-700 font-bold"
target="_blank"
rel="noreferrer"
href="mailto:support@documenso.com"
>
support@documenso.com
@ -175,6 +178,7 @@ export default function PricingPage() {
className="text-documenso-700 font-bold"
href="https://documen.so/discord"
target="_blank"
rel="noreferrer"
>
in our Discord-Support-Channel
</a>{' '}

View File

@ -27,7 +27,7 @@ export default async function SinglePlayerModeSuccessPage({
return notFound();
}
const signatures = await getRecipientSignatures({ recipientId: document.Recipient.id });
const signatures = await getRecipientSignatures({ recipientId: document.Recipient[0].id });
return <SinglePlayerModeSuccess document={document} signatures={signatures} />;
}

View File

@ -6,6 +6,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import type { Field, Recipient } from '@documenso/prisma/client';
@ -190,7 +191,7 @@ export const SinglePlayerClient = () => {
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=singleplayer`}
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
@ -202,7 +203,7 @@ export const SinglePlayerClient = () => {
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
community plan
early adopter plan
</Link>{' '}
for exclusive features, including the ability to collaborate with multiple signers.
</p>
@ -255,6 +256,7 @@ export const SinglePlayerClient = () => {
fields={fields}
onSubmit={onSignSubmit}
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
requireCustomText={Boolean(fields.find((field) => field.type === 'TEXT'))}
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
/>
</Stepper>

View File

@ -7,6 +7,7 @@ export const metadata: Metadata = {
};
export const revalidate = 0;
export const dynamic = 'force-dynamic';
// !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during

View File

@ -2,7 +2,10 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { PublicEnvScript } from 'next-runtime-env';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -17,32 +20,35 @@ import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = {
title: {
template: '%s - Documenso',
default: 'Documenso',
},
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
keywords:
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
authors: { name: 'Documenso, Inc.' },
robots: 'index, follow',
openGraph: {
title: 'Documenso - The Open Source DocuSign Alternative',
export function generateMetadata() {
return {
title: {
template: '%s - Documenso',
default: 'Documenso',
},
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
type: 'website',
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`],
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
},
};
keywords:
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
authors: { name: 'Documenso, Inc.' },
robots: 'index, follow',
metadataBase: new URL(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3000'),
openGraph: {
title: 'Documenso - The Open Source DocuSign Alternative',
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
type: 'website',
images: ['/opengraph-image.jpg'],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: ['/opengraph-image.jpg'],
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
},
};
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags();
@ -58,6 +64,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<PublicEnvScript />
</head>
<Suspense>

View File

@ -0,0 +1,31 @@
import Link from 'next/link';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
type CallToActionProps = {
className?: string;
utmSource?: string;
};
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
return (
<Card spotlight className={className}>
<CardContent className="flex flex-col items-center justify-center p-12">
<h2 className="text-center text-2xl font-bold">Join the Open Signing Movement</h2>
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
Create your account and start using state-of-the-art document signing. Open and beautiful
signing is within your grasp.
</p>
<Button className="mt-8 rounded-full no-underline" size="lg" asChild>
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=${utmSource}`} target="_blank">
Get started
</Link>
</Button>
</CardContent>
</Card>
);
};

View File

@ -40,9 +40,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Early Adopters Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
Claim Early Adopter Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
$30/mo
</span>
</Button>
@ -53,7 +53,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<LuGithub className="mr-2 h-5 w-5" />
Star on Github
Star on GitHub
{starCount && starCount > 0 && (
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
{starCount.toLocaleString('en-US')}

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';

View File

@ -9,6 +9,7 @@ import Link from 'next/link';
import LogoImage from '@documenso/assets/logo.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { HamburgerMenu } from './mobile-hamburger';
import { MobileNavigation } from './mobile-navigation';
@ -68,12 +69,18 @@ export const Header = ({ className, ...props }: HeaderProps) => {
</Link>
<Link
href="https://app.documenso.com/signin"
href="https://app.documenso.com/signin?utm_source=marketing-header"
target="_blank"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Sign in
</Link>
<Button className="rounded-full" size="sm" asChild>
<Link href="https://app.documenso.com/signup?utm_source=marketing-header" target="_blank">
Sign up
</Link>
</Button>
</div>
<HamburgerMenu

View File

@ -3,7 +3,8 @@
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import type { Variants } from 'framer-motion';
import { motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { LuGithub } from 'react-icons/lu';
import { match } from 'ts-pattern';
@ -113,16 +114,16 @@ export const Hero = ({ className, ...props }: HeroProps) => {
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Early Adopters Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
Claim Early Adopter Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
$30/mo
</span>
</Button>
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<LuGithub className="mr-2 h-5 w-5" />
Star on Github
Star on GitHub
</Button>
</Link>
</motion.div>
@ -224,7 +225,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
<span className="bg-primary text-black">
(in a non-legally binding, but heartfelt way)
</span>{' '}
and lock in the early supporter plan for forever, including everything we build this
and lock in the early adopter plan for forever, including everything we build this
year.
</p>

View File

@ -47,9 +47,13 @@ export const MENU_NAVIGATION_LINKS = [
text: 'Privacy',
},
{
href: 'https://app.documenso.com/signin',
href: 'https://app.documenso.com/signin?utm_source=marketing-header',
text: 'Sign in',
},
{
href: 'https://app.documenso.com/signup?utm_source=marketing-header',
text: 'Sign up',
},
];
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {

View File

@ -8,6 +8,7 @@ import Link from 'next/link';
import { AnimatePresence, motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -22,7 +23,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
return (
<div className={cn('', className)} {...props}>
<div className="flex items-center justify-center gap-x-6">
<div className="bg-background sticky top-32 flex items-center justify-end gap-x-6 shadow-[-1px_-5px_2px_6px_hsl(var(--background))] md:top-[7.5rem] lg:static lg:justify-center">
<AnimatePresence>
<motion.button
key="MONTHLY"
@ -39,7 +40,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
{period === 'MONTHLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
className="bg-foreground lg:bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
@ -62,7 +63,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
{period === 'YEARLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
className="bg-foreground lg:bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
@ -83,7 +84,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<Button className="rounded-full text-base" asChild>
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-free-plan`}
target="_blank"
className="mt-6"
>
@ -101,7 +102,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</div>
<div
data-plan="community"
data-plan="early-adopter"
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
>
<p className="text-foreground text-4xl font-medium">Early Adopters</p>
@ -117,29 +118,31 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</p>
<Button className="mt-6 rounded-full text-base" asChild>
<Link href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}>Signup Now</Link>
<Link
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-early-adopter`}
target="_blank"
>
Signup Now
</Link>
</Button>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4 font-medium">
{' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank">
The Early Adopter Deal:
<p className="text-foreground py-4">
<a
href="https://documen.so/early-adopters-pricing-page"
target="_blank"
rel="noreferrer"
>
Limited Time Offer: <span className="text-documenso-700">Read More</span>
</a>
</p>
<p className="text-foreground py-4">Join the movement</p>
<p className="text-foreground py-4">Simple signing solution</p>
<p className="text-foreground py-4">Unlimited Teams</p>
<p className="text-foreground py-4">Unlimited Users</p>
<p className="text-foreground py-4">Unlimited Documents per month</p>
<p className="text-foreground py-4">Includes all upcoming features</p>
<p className="text-foreground py-4">Email, Discord and Slack assistance</p>
<p className="text-foreground py-4">
<strong>
{' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank">
Includes all upcoming features
</a>
</strong>
</p>
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
</div>
<div className="flex-1" />
</div>
<div

View File

@ -6,6 +6,7 @@ import Link from 'next/link';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { Signature } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
@ -54,7 +55,7 @@ export const SinglePlayerModeSuccess = ({
<SigningCard3D
className="mt-8"
name={document.Recipient.name || document.Recipient.email}
name={document.Recipient[0].name || document.Recipient[0].email}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
@ -64,7 +65,7 @@ export const SinglePlayerModeSuccess = ({
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
<DocumentShareButton
documentId={document.id}
token={document.Recipient.token}
token={document.Recipient[0].token}
className="flex-1 bg-transparent backdrop-blur-sm"
/>
@ -85,7 +86,7 @@ export const SinglePlayerModeSuccess = ({
<p className="text-muted-foreground/60 mt-16 text-center text-sm">
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=singleplayer`}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>

View File

@ -7,6 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { env } from 'next-runtime-env';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
@ -144,7 +145,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
setTimeout(resolve, 1000);
});
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
const planId = env('NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID');
if (!planId) {
throw new Error('No plan ID found.');
}
const claimPlanInput = signatureDataUrl
? {
@ -194,7 +199,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)}
>
<h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
<h3 className="text-xl font-semibold">Sign up to Early Adopter Plan</h3>
<p className="text-muted-foreground mt-2 text-xs">
with Timur Ercan & Lucas Smith from Documenso
</p>
@ -203,7 +208,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<AnimatePresence>
<motion.div key="email">
<label htmlFor="email" className="text-foreground text-lg font-semibold lg:text-xl">
<label htmlFor="email" className="text-foreground font-medium ">
Whats your email?
</label>
@ -215,7 +220,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Input
id="email"
type="email"
placeholder=""
placeholder="your@example.com"
className="bg-background w-full pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
@ -260,11 +265,8 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
transform: 'translateX(25%)',
}}
>
<label
htmlFor="name"
className="text-foreground text-lg font-semibold lg:text-xl"
>
and your name?
<label htmlFor="name" className="text-foreground font-medium ">
And your name?
</label>
<Controller

View File

@ -1,13 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiRequest, NextApiResponse } from 'next';
import { randomUUID } from 'crypto';
import { TEarlyAdopterCheckoutMetadataSchema } from '@documenso/ee/server-only/stripe/webhook/early-adopter-checkout-metadata';
import type { TEarlyAdopterCheckoutMetadataSchema } from '@documenso/ee/server-only/stripe/webhook/early-adopter-checkout-metadata';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { TClaimPlanResponseSchema, ZClaimPlanRequestSchema } from '~/api/claim-plan/types';
import type { TClaimPlanResponseSchema } from '~/api/claim-plan/types';
import { ZClaimPlanRequestSchema } from '~/api/claim-plan/types';
export default async function handler(
req: NextApiRequest,
@ -40,7 +42,7 @@ export default async function handler(
if (user) {
return res.status(200).json({
redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`,
redirectUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/signin`,
});
}
@ -77,8 +79,8 @@ export default async function handler(
mode: 'subscription',
metadata,
allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}`,
success_url: `${NEXT_PUBLIC_MARKETING_URL()}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${NEXT_PUBLIC_MARKETING_URL()}`,
});
if (!checkout.url) {

View File

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { withAxiom } = require('next-axiom');
const fs = require('fs');
const path = require('path');
const { version } = require('./package.json');
@ -22,6 +23,7 @@ const config = {
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
experimental: {
outputFileTracingRoot: path.join(__dirname, '../../'),
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign'],
serverActions: {
bodySizeLimit: '50mb',
},
@ -90,4 +92,4 @@ const config = {
},
};
module.exports = config;
module.exports = withAxiom(config);

View File

@ -14,15 +14,18 @@
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/tailwind-config": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"@hookform/resolvers": "^3.1.0",
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3",
"@tanstack/react-query": "^4.29.5",
"cookie-es": "^1.0.0",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
@ -30,6 +33,7 @@
"micro": "^10.0.1",
"next": "14.0.3",
"next-auth": "4.24.5",
"next-axiom": "^1.1.1",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"perfect-freehand": "^1.2.0",
@ -42,20 +46,23 @@
"react-hotkeys-hook": "^4.4.1",
"react-icons": "^4.11.0",
"react-rnd": "^10.4.1",
"sharp": "0.33.1",
"remeda": "^1.27.1",
"sharp": "^0.33.1",
"ts-pattern": "^5.0.5",
"typescript": "5.2.2",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@documenso/tailwind-config": "*",
"@simplewebauthn/types": "^9.0.1",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/ua-parser-js": "^0.7.39"
"@types/ua-parser-js": "^0.7.39",
"typescript": "5.2.2"
},
"overrides": {
"next-auth": {

View File

@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

View File

@ -0,0 +1,69 @@
'use client';
import Link from 'next/link';
import { type Document, DocumentStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminActionsProps = {
className?: string;
document: Document;
};
export const AdminActions = ({ className, document }: AdminActionsProps) => {
const { toast } = useToast();
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
trpc.admin.resealDocument.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'Document resealed',
});
},
onError: () => {
toast({
title: 'Error',
description: 'Failed to reseal document',
variant: 'destructive',
});
},
});
return (
<div className={cn('flex gap-x-4', className)}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
loading={isResealDocumentLoading}
disabled={document.status !== DocumentStatus.COMPLETED}
onClick={() => resealDocument({ id: document.id })}
>
Reseal document
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[40ch]">
Attempts sealing the document again, useful for after a code change has occurred to
resolve an erroneous document.
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button variant="outline" asChild>
<Link href={`/admin/users/${document.userId}`}>Go to owner</Link>
</Button>
</div>
);
};

View File

@ -0,0 +1,86 @@
import { DateTime } from 'luxon';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@documenso/ui/primitives/accordion';
import { Badge } from '@documenso/ui/primitives/badge';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { AdminActions } from './admin-actions';
import { RecipientItem } from './recipient-item';
type AdminDocumentDetailsPageProps = {
params: {
id: string;
};
};
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
const document = await getEntireDocument({ id: Number(params.id) });
return (
<div>
<div className="flex items-start justify-between">
<div className="flex items-center gap-x-4">
<h1 className="text-2xl font-semibold">{document.title}</h1>
<DocumentStatus status={document.status} />
</div>
{document.deletedAt && (
<Badge size="large" variant="destructive">
Deleted
</Badge>
)}
</div>
<div className="text-muted-foreground mt-4 text-sm">
<div>
Created on: <LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
</div>
<div>
Last updated at: <LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
</div>
</div>
<hr className="my-4" />
<h2 className="text-lg font-semibold">Admin Actions</h2>
<AdminActions className="mt-2" document={document} />
<hr className="my-4" />
<h2 className="text-lg font-semibold">Recipients</h2>
<div className="mt-4">
<Accordion type="multiple" className="space-y-4">
{document.Recipient.map((recipient) => (
<AccordionItem
key={recipient.id}
value={recipient.id.toString()}
className="rounded-lg border"
>
<AccordionTrigger className="px-4">
<div className="flex items-center gap-x-4">
<h4 className="font-semibold">{recipient.name}</h4>
<Badge size="small" variant="neutral">
{recipient.email}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className="border-t px-4 pt-4">
<RecipientItem recipient={recipient} />
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1,182 @@
'use client';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
type Field,
type Recipient,
type Signature,
SigningStatus,
} from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { DataTable } from '@documenso/ui/primitives/data-table';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ZAdminUpdateRecipientFormSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormSchema>;
export type RecipientItemProps = {
recipient: Recipient & {
Field: Array<
Field & {
Signature: Signature | null;
}
>;
};
};
export const RecipientItem = ({ recipient }: RecipientItemProps) => {
const { toast } = useToast();
const router = useRouter();
const form = useForm<TAdminUpdateRecipientFormSchema>({
defaultValues: {
name: recipient.name,
email: recipient.email,
},
});
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
try {
await updateRecipient({
id: recipient.id,
name,
email,
});
toast({
title: 'Recipient updated',
description: 'The recipient has been updated successfully',
});
router.refresh();
} catch (error) {
toast({
title: 'Failed to update recipient',
description: error.message,
variant: 'destructive',
});
}
};
return (
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onUpdateRecipientFormSubmit)}>
<fieldset
className="flex h-full max-w-xl flex-col gap-y-4"
disabled={
form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED
}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Button type="submit" loading={form.formState.isSubmitting}>
Update Recipient
</Button>
</div>
</fieldset>
</form>
</Form>
<hr className="my-4" />
<h2 className="mb-4 text-lg font-semibold">Fields</h2>
<DataTable
data={recipient.Field}
columns={[
{
header: 'ID',
accessorKey: 'id',
cell: ({ row }) => <div>{row.original.id}</div>,
},
{
header: 'Type',
accessorKey: 'type',
cell: ({ row }) => <div>{row.original.type}</div>,
},
{
header: 'Inserted',
accessorKey: 'inserted',
cell: ({ row }) => <div>{row.original.inserted ? 'True' : 'False'}</div>,
},
{
header: 'Value',
accessorKey: 'customText',
cell: ({ row }) => <div>{row.original.customText}</div>,
},
{
header: 'Signature',
accessorKey: 'signature',
cell: ({ row }) => (
<div>
{row.original.Signature?.typedSignature && (
<span>{row.original.Signature.typedSignature}</span>
)}
{row.original.Signature?.signatureImageAsBase64 && (
<img
src={row.original.Signature.signatureImageAsBase64}
alt="Signature"
className="h-12 w-full dark:invert"
/>
)}
</div>
),
},
]}
/>
</div>
);
};

View File

@ -1,125 +0,0 @@
'use client';
import { useTransition } from 'react';
import Link from 'next/link';
import { Loader } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Document, User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
export type DocumentsDataTableProps = {
results: FindResultSet<
Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
}
>;
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
return (
<div className="relative">
<DataTable
columns={[
{
header: 'Created',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Title',
accessorKey: 'title',
cell: ({ row }) => {
return (
<div className="block max-w-[5rem] truncate font-medium md:max-w-[10rem]">
{row.original.title}
</div>
);
},
},
{
header: 'Owner',
accessorKey: 'owner',
cell: ({ row }) => {
const avatarFallbackText = row.original.User.name
? extractInitials(row.original.User.name)
: row.original.User.email.slice(0, 1).toUpperCase();
return (
<Tooltip delayDuration={200}>
<TooltipTrigger>
<Link href={`/admin/users/${row.original.User.id}`}>
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
<TooltipContent className="flex max-w-xs items-center gap-2">
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
<div className="text-muted-foreground flex flex-col text-sm">
<span>{row.original.User.name}</span>
<span>{row.original.User.email}</span>
</div>
</TooltipContent>
</Tooltip>
);
},
},
{
header: 'Last updated',
accessorKey: 'updatedAt',
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
},
]}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};

View File

@ -0,0 +1,150 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Loader } from 'lucide-react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
// export type AdminDocumentResultsProps = {};
export const AdminDocumentResults = () => {
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const [term, setTerm] = useState(() => searchParams?.get?.('term') ?? '');
const debouncedTerm = useDebouncedValue(term, 500);
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
trpc.admin.findDocuments.useQuery(
{
term: debouncedTerm,
page: page || 1,
perPage: perPage || 20,
},
{
keepPreviousData: true,
},
);
const onPaginationChange = (newPage: number, newPerPage: number) => {
updateSearchParams({
page: newPage,
perPage: newPerPage,
});
};
return (
<div>
<Input
type="search"
placeholder="Search by document title"
value={term}
onChange={(e) => setTerm(e.target.value)}
/>
<div className="relative mt-4">
<DataTable
columns={[
{
header: 'Created',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Title',
accessorKey: 'title',
cell: ({ row }) => {
return (
<Link
href={`/admin/documents/${row.original.id}`}
className="block max-w-[5rem] truncate font-medium hover:underline md:max-w-[10rem]"
>
{row.original.title}
</Link>
);
},
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
},
{
header: 'Owner',
accessorKey: 'owner',
cell: ({ row }) => {
const avatarFallbackText = row.original.User.name
? extractInitials(row.original.User.name)
: row.original.User.email.slice(0, 1).toUpperCase();
return (
<Tooltip delayDuration={200}>
<TooltipTrigger>
<Link href={`/admin/users/${row.original.User.id}`}>
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
</Link>
</TooltipTrigger>
<TooltipContent className="flex max-w-xs items-center gap-2">
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
</AvatarFallback>
</Avatar>
<div className="text-muted-foreground flex flex-col text-sm">
<span>{row.original.User.name}</span>
<span>{row.original.User.email}</span>
</div>
</TooltipContent>
</Tooltip>
);
},
},
{
header: 'Last updated',
accessorKey: 'updatedAt',
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
},
]}
data={findDocumentsData?.data ?? []}
perPage={findDocumentsData?.perPage ?? 20}
currentPage={findDocumentsData?.currentPage ?? 1}
totalPages={findDocumentsData?.totalPages ?? 1}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isFindDocumentsLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
</div>
);
};

View File

@ -1,28 +1,12 @@
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { DocumentsDataTable } from './data-table';
export type DocumentsPageProps = {
searchParams?: {
page?: string;
perPage?: string;
};
};
export default async function Documents({ searchParams = {} }: DocumentsPageProps) {
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const results = await findDocuments({
page,
perPage,
});
import { AdminDocumentResults } from './document-results';
export default function AdminDocumentsPage() {
return (
<div>
<h2 className="text-4xl font-semibold">Manage documents</h2>
<div className="mt-8">
<DocumentsDataTable results={results} />
<AdminDocumentResults />
</div>
</div>
);

View File

@ -1,11 +1,11 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react';
import { BarChart3, FileStack, Settings, User2, Wallet2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -78,6 +78,20 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
Subscriptions
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/banner') && 'bg-secondary',
)}
asChild
>
<Link href="/admin/site-settings">
<Settings className="mr-2 h-5 w-5" />
Site Settings
</Link>
</Button>
</div>
);
};

View File

@ -0,0 +1,200 @@
'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import type { TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
import {
SITE_SETTINGS_BANNER_ID,
ZSiteSettingsBannerSchema,
} from '@documenso/lib/server-only/site-settings/schemas/banner';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { ColorPicker } from '@documenso/ui/primitives/color-picker';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Switch } from '@documenso/ui/primitives/switch';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ZBannerFormSchema = ZSiteSettingsBannerSchema;
type TBannerFormSchema = z.infer<typeof ZBannerFormSchema>;
export type BannerFormProps = {
banner?: TSiteSettingsBannerSchema;
};
export function BannerForm({ banner }: BannerFormProps) {
const router = useRouter();
const { toast } = useToast();
const form = useForm<TBannerFormSchema>({
resolver: zodResolver(ZBannerFormSchema),
defaultValues: {
id: SITE_SETTINGS_BANNER_ID,
enabled: banner?.enabled ?? false,
data: {
content: banner?.data?.content ?? '',
bgColor: banner?.data?.bgColor ?? '#000000',
textColor: banner?.data?.textColor ?? '#FFFFFF',
},
},
});
const enabled = form.watch('enabled');
const { mutateAsync: updateSiteSetting, isLoading: isUpdateSiteSettingLoading } =
trpcReact.admin.updateSiteSetting.useMutation();
const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
try {
await updateSiteSetting({
id,
enabled,
data,
});
toast({
title: 'Banner Updated',
description: 'Your banner has been updated successfully.',
duration: 5000,
});
router.refresh();
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: 'An error occurred',
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: 'An unknown error occurred',
variant: 'destructive',
description:
'We encountered an unknown error while attempting to update the banner. Please try again later.',
});
}
}
};
return (
<div>
<h2 className="font-semibold">Site Banner</h2>
<p className="text-muted-foreground mt-2 text-sm">
The site banner is a message that is shown at the top of the site. It can be used to display
important information to your users.
</p>
<Form {...form}>
<form
className="mt-4 flex flex-col rounded-md"
onSubmit={form.handleSubmit(onBannerUpdate)}
>
<div className="mt-4 flex flex-col gap-4 md:flex-row">
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Enabled</FormLabel>
<FormControl>
<div>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
</FormItem>
)}
/>
<fieldset
className="flex flex-col gap-4 md:flex-row"
disabled={!enabled}
aria-disabled={!enabled}
>
<FormField
control={form.control}
name="data.bgColor"
render={({ field }) => (
<FormItem>
<FormLabel>Background Color</FormLabel>
<FormControl>
<div>
<ColorPicker {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="data.textColor"
render={({ field }) => (
<FormItem>
<FormLabel>Text Color</FormLabel>
<FormControl>
<div>
<ColorPicker {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
</div>
<fieldset disabled={!enabled} aria-disabled={!enabled}>
<FormField
control={form.control}
name="data.content"
render={({ field }) => (
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea className="h-32 resize-none" {...field} />
</FormControl>
<FormDescription>
The content to show in the banner, HTML is allowed
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
<Button
type="submit"
loading={isUpdateSiteSettingLoading}
className="mt-4 justify-end self-end"
>
Update Banner
</Button>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { BannerForm } from './banner-form';
// import { BannerForm } from './banner-form';
export default async function AdminBannerPage() {
const banner = await getSiteSettings().then((settings) =>
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
);
return (
<div>
<SettingsHeader title="Site Settings" subtitle="Manage your site settings here" />
<div className="mt-8">
<BannerForm banner={banner} />
</div>
</div>
);
}

View File

@ -0,0 +1,131 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteUserDialogProps = {
className?: string;
user: User;
};
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
const router = useRouter();
const { toast } = useToast();
const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isLoading: isDeletingUser } =
trpc.admin.deleteUser.useMutation();
const onDeleteAccount = async () => {
try {
await deleteUser({
id: user.id,
email,
});
toast({
title: 'Account deleted',
description: 'The account has been deleted successfully.',
duration: 5000,
});
router.push('/admin/users');
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: 'An error occurred',
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: 'An unknown error occurred',
variant: 'destructive',
description:
err.message ??
'We encountered an unknown error while attempting to delete your account. Please try again later.',
});
}
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
variant="neutral"
>
<div>
<AlertTitle>Delete Account</AlertTitle>
<AlertDescription className="mr-2">
Delete the users account and all its contents. This action is irreversible and will
cancel their subscription, so proceed with caution.
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>Delete Account</DialogTitle>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
This action is not reversible. Please be certain.
</AlertDescription>
</Alert>
</DialogHeader>
<div>
<DialogDescription>
To confirm, please enter the accounts email address <br />({user.email}).
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
onClick={onDeleteAccount}
loading={isDeletingUser}
variant="destructive"
disabled={email !== user.email}
>
{isDeletingUser ? 'Deleting account...' : 'Delete Account'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -7,7 +7,7 @@ import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
import { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
@ -20,9 +20,10 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DeleteUserDialog } from './delete-user-dialog';
import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
@ -137,6 +138,10 @@ export default function UserPage({ params }: { params: { id: number } }) {
</fieldset>
</form>
</Form>
<hr className="my-4" />
{user && <DeleteUserDialog user={user} />}
</div>
);
}

View File

@ -19,7 +19,7 @@ export default async function AdminManageUsers({ searchParams = {} }: AdminManag
const [{ users, totalPages }, individualPrices] = await Promise.all([
search(searchString, page, perPage),
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY),
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY).catch(() => []),
]);
const individualPriceIds = individualPrices.map((price) => price.id);

View File

@ -0,0 +1,110 @@
'use client';
import Link from 'next/link';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = {
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => {
const { data: session } = useSession();
const { toast } = useToast();
if (!session) {
return null;
}
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
const isComplete = document.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const role = recipient?.role;
const documentsPath = formatDocumentsPath(document.team?.url);
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
id: document.id,
teamId: team?.id,
});
const documentData = documentWithData?.documentData;
if (!documentData) {
throw new Error('No document available');
}
await downloadPDF({ documentData, fileName: documentWithData.title });
} catch (err) {
toast({
title: 'Something went wrong',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
}
};
return match({
isRecipient,
isPending,
isComplete,
isSigned,
})
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-full" asChild>
<Link href={`/sign/${recipient?.token}`}>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
<Pencil className="-ml-1 mr-2 h-4 w-4" />
Sign
</>
))
.with(RecipientRole.APPROVER, () => (
<>
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
Approve
</>
))
.otherwise(() => (
<>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
View
</>
))}
</Link>
</Button>
))
.with({ isComplete: false }, () => (
<Button className="w-full" asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>Edit</Link>
</Button>
))
.with({ isComplete: true }, () => (
<Button className="w-full" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
Download
</Button>
))
.otherwise(() => null);
};

View File

@ -0,0 +1,160 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Copy, Download, Edit, Loader, MoreHorizontal, Share, Trash2 } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus } from '@documenso/prisma/client';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { ResendDocumentActionItem } from '../_action-items/resend-document';
import { DeleteDocumentDialog } from '../delete-document-dialog';
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
export type DocumentPageViewDropdownProps = {
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
if (!session) {
return null;
}
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = document.User.id === session.user.id;
const isDraft = document.status === DocumentStatus.DRAFT;
const isComplete = document.status === DocumentStatus.COMPLETED;
const isDocumentDeletable = isOwner;
const isCurrentTeamDocument = team && document.team?.url === team.url;
const documentsPath = formatDocumentsPath(team?.url);
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
id: document.id,
teamId: team?.id,
});
const documentData = documentWithData?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: document.title });
} catch (err) {
toast({
title: 'Something went wrong',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
}
};
const nonSignedRecipients = document.Recipient.filter((item) => item.signingStatus !== 'SIGNED');
return (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="end" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
{(isOwner || isCurrentTeamDocument) && !isComplete && (
<DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
)}
{isComplete && (
<DropdownMenuItem onClick={onDownloadClick}>
<Download className="mr-2 h-4 w-4" />
Download
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
<DropdownMenuLabel>Share</DropdownMenuLabel>
<ResendDocumentActionItem
document={document}
recipients={nonSignedRecipients}
team={team}
/>
<DocumentShareButton
documentId={document.id}
token={isOwner ? undefined : recipient?.token}
trigger={({ loading, disabled }) => (
<DropdownMenuItem disabled={disabled || isDraft} onSelect={(e) => e.preventDefault()}>
<div className="flex items-center">
{loading ? <Loader className="mr-2 h-4 w-4" /> : <Share className="mr-2 h-4 w-4" />}
Share Signing Card
</div>
</DropdownMenuItem>
)}
/>
</DropdownMenuContent>
{isDocumentDeletable && (
<DeleteDocumentDialog
id={document.id}
status={document.status}
documentTitle={document.title}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
/>
)}
{isDuplicateDialogOpen && (
<DuplicateDocumentDialog
id={document.id}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
team={team}
/>
)}
</DropdownMenu>
);
};

View File

@ -0,0 +1,72 @@
'use client';
import { useMemo } from 'react';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = {
userId: number;
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
};
};
export const DocumentPageViewInformation = ({
document,
userId,
}: DocumentPageViewInformationProps) => {
const isMounted = useIsMounted();
const { locale } = useLocale();
const documentInformation = useMemo(() => {
let createdValue = DateTime.fromJSDate(document.createdAt).toFormat('MMMM d, yyyy');
let lastModifiedValue = DateTime.fromJSDate(document.updatedAt).toRelative();
if (!isMounted) {
createdValue = DateTime.fromJSDate(document.createdAt)
.setLocale(locale)
.toFormat('MMMM d, yyyy');
lastModifiedValue = DateTime.fromJSDate(document.updatedAt).setLocale(locale).toRelative();
}
return [
{
description: 'Uploaded by',
value: userId === document.userId ? 'You' : document.User.name ?? document.User.email,
},
{
description: 'Created',
value: createdValue,
},
{
description: 'Last modified',
value: lastModifiedValue,
},
];
}, [isMounted, document, locale, userId]);
return (
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
<h1 className="px-4 py-3 font-medium">Information</h1>
<ul className="divide-y border-t">
{documentInformation.map((item) => (
<li
key={item.description}
className="flex items-center justify-between px-4 py-2.5 text-sm last:border-b"
>
<span className="text-muted-foreground">{item.description}</span>
<span>{item.value}</span>
</li>
))}
</ul>
</section>
);
};

View File

@ -0,0 +1,153 @@
'use client';
import { useMemo } from 'react';
import { CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { cn } from '@documenso/ui/lib/utils';
export type DocumentPageViewRecentActivityProps = {
documentId: number;
userId: number;
};
export const DocumentPageViewRecentActivity = ({
documentId,
userId,
}: DocumentPageViewRecentActivityProps) => {
const {
data,
isLoading,
isLoadingError,
refetch,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
{
documentId,
filterForRecentActivity: true,
orderBy: {
column: 'createdAt',
direction: 'asc',
},
perPage: 10,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
);
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
<div className="flex flex-row items-center justify-between border-b px-4 py-3">
<h1 className="text-foreground font-medium">Recent activity</h1>
{/* Can add dropdown menu here for additional options. */}
</div>
{isLoading && (
<div className="flex h-full items-center justify-center py-16">
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
</div>
)}
{isLoadingError && (
<div className="flex h-full flex-col items-center justify-center py-16">
<p className="text-foreground/80 text-sm">Unable to load document history</p>
<button
onClick={async () => refetch()}
className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
>
Click here to retry
</button>
</div>
)}
<AnimateGenericFadeInOut>
{data && (
<ul role="list" className="space-y-6 p-4">
{hasNextPage && (
<li className="relative flex gap-x-4">
<div className="absolute -bottom-6 left-0 top-0 flex w-6 justify-center">
<div className="bg-border w-px" />
</div>
<div className="bg-widget relative flex h-6 w-6 flex-none items-center justify-center">
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
</div>
<button
onClick={async () => fetchNextPage()}
className="text-foreground/70 hover:text-muted-foreground text-xs"
>
{isFetchingNextPage ? 'Loading...' : 'Load older activity'}
</button>
</li>
)}
{documentAuditLogs.length === 0 && (
<div className="flex items-center justify-center py-4">
<p className="text-muted-foreground/70 text-sm">No recent activity</p>
</div>
)}
{documentAuditLogs.map((auditLog, auditLogIndex) => (
<li key={auditLog.id} className="relative flex gap-x-4">
<div
className={cn(
auditLogIndex === documentAuditLogs.length - 1 ? 'h-6' : '-bottom-6',
'absolute left-0 top-0 flex w-6 justify-center',
)}
>
<div className="bg-border w-px" />
</div>
<div className="bg-widget text-foreground/40 relative flex h-6 w-6 flex-none items-center justify-center">
{match(auditLog.type)
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, () => (
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
<CheckCheckIcon className="h-3 w-3" aria-hidden="true" />
</div>
))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => (
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
<CheckIcon className="h-3 w-3" aria-hidden="true" />
</div>
))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
<MailOpen className="h-3 w-3" aria-hidden="true" />
</div>
))
.otherwise(() => (
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
))}
</div>
<p className="text-muted-foreground dark:text-muted-foreground/70 flex-auto py-0.5 text-xs leading-5">
<span className="text-foreground font-medium">
{formatDocumentAuditLogAction(auditLog, userId).prefix}
</span>{' '}
{formatDocumentAuditLogAction(auditLog, userId).description}
</p>
<time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5">
{DateTime.fromJSDate(auditLog.createdAt).toRelative({ style: 'short' })}
</time>
</li>
))}
</ul>
)}
</AnimateGenericFadeInOut>
</section>
);
};

View File

@ -0,0 +1,115 @@
import Link from 'next/link';
import { CheckIcon, Clock, MailIcon, MailOpenIcon, PenIcon, PlusIcon } from 'lucide-react';
import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Document, Recipient } from '@documenso/prisma/client';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
export type DocumentPageViewRecipientsProps = {
document: Document & {
Recipient: Recipient[];
};
documentRootPath: string;
};
export const DocumentPageViewRecipients = ({
document,
documentRootPath,
}: DocumentPageViewRecipientsProps) => {
const recipients = document.Recipient;
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
<div className="flex flex-row items-center justify-between px-4 py-3">
<h1 className="text-foreground font-medium">Recipients</h1>
{document.status !== DocumentStatus.COMPLETED && (
<Link
href={`${documentRootPath}/${document.id}/edit?step=signers`}
title="Modify recipients"
className="flex flex-row items-center justify-between"
>
{recipients.length === 0 ? (
<PlusIcon className="ml-2 h-4 w-4" />
) : (
<PenIcon className="ml-2 h-3 w-3" />
)}
</Link>
)}
</div>
<ul className="text-muted-foreground divide-y border-t">
{recipients.length === 0 && (
<li className="flex flex-col items-center justify-center py-6 text-sm">No recipients</li>
)}
{recipients.map((recipient) => (
<li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm">
<AvatarWithText
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
secondaryText={
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
}
/>
{document.status !== DocumentStatus.DRAFT &&
recipient.signingStatus === SigningStatus.SIGNED && (
<Badge variant="default">
{match(recipient.role)
.with(RecipientRole.APPROVER, () => (
<>
<CheckIcon className="mr-1 h-3 w-3" />
Approved
</>
))
.with(RecipientRole.CC, () =>
document.status === DocumentStatus.COMPLETED ? (
<>
<MailIcon className="mr-1 h-3 w-3" />
Sent
</>
) : (
<>
<CheckIcon className="mr-1 h-3 w-3" />
Ready
</>
),
)
.with(RecipientRole.SIGNER, () => (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
Signed
</>
))
.with(RecipientRole.VIEWER, () => (
<>
<MailOpenIcon className="mr-1 h-3 w-3" />
Viewed
</>
))
.exhaustive()}
</Badge>
)}
{document.status !== DocumentStatus.DRAFT &&
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
<Badge variant="secondary">
<Clock className="mr-1 h-3 w-3" />
Pending
</Badge>
)}
</li>
))}
</ul>
</section>
);
};

View File

@ -1,22 +1,34 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ChevronLeft, Users2 } from 'lucide-react';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus } from '@documenso/prisma/client';
import type { Team } from '@documenso/prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status';
import { DocumentPageViewButton } from './document-page-view-button';
import { DocumentPageViewDropdown } from './document-page-view-dropdown';
import { DocumentPageViewInformation } from './document-page-view-information';
import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
import { DocumentPageViewRecipients } from './document-page-view-recipients';
export type DocumentPageViewProps = {
params: {
@ -44,6 +56,10 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
teamId: team?.id,
}).catch(() => null);
const isDocumentHistoryEnabled = await getServerComponentFlag(
'app_document_page_view_history_sheet',
);
if (!document || !document.documentData) {
redirect(documentRootPath);
}
@ -67,16 +83,16 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
documentMeta.password = securePassword;
}
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
documentId,
userId: user.id,
}),
getFieldsForDocument({
documentId,
userId: user.id,
}),
]);
const recipients = await getRecipientsForDocument({
documentId,
teamId: team?.id,
userId: user.id,
});
const documentWithRecipients = {
...document,
Recipient: recipients,
};
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
@ -85,47 +101,105 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
Documents
</Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div className="flex flex-row justify-between">
<div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatus inheritColor status={document.status} className="text-muted-foreground" />
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatusComponent
inheritColor
status={document.status}
className="text-muted-foreground"
/>
{recipients.length > 0 && (
<div className="text-muted-foreground flex items-center">
<Users2 className="mr-2 h-5 w-5" />
{recipients.length > 0 && (
<div className="text-muted-foreground flex items-center">
<Users2 className="mr-2 h-5 w-5" />
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
<span>{recipients.length} Recipient(s)</span>
</StackAvatarsWithTooltip>
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
<span>{recipients.length} Recipient(s)</span>
</StackAvatarsWithTooltip>
</div>
)}
</div>
</div>
{isDocumentHistoryEnabled && (
<div className="self-end">
<DocumentHistorySheet documentId={document.id} userId={user.id}>
<Button variant="outline">
<Clock9 className="mr-1.5 h-4 w-4" />
Document history
</Button>
</DocumentHistorySheet>
</div>
)}
</div>
{document.status !== InternalDocumentStatus.COMPLETED && (
<EditDocumentForm
className="mt-8"
document={document}
user={user}
documentMeta={documentMeta}
recipients={recipients}
fields={fields}
documentData={documentData}
documentRootPath={documentRootPath}
/>
)}
<div className="mt-6 grid w-full grid-cols-12 gap-8">
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer document={document} key={documentData.id} documentData={documentData} />
</CardContent>
</Card>
{document.status === InternalDocumentStatus.COMPLETED && (
<div className="mx-auto mt-12 max-w-2xl">
<LazyPDFViewer
document={document}
key={documentData.id}
documentMeta={documentMeta}
documentData={documentData}
/>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<div className="space-y-6">
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
<div className="flex flex-row items-center justify-between px-4">
<h3 className="text-foreground text-2xl font-semibold">
Document {FRIENDLY_STATUS_MAP[document.status].label.toLowerCase()}
</h3>
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm ">
{match(document.status)
.with(
DocumentStatus.COMPLETED,
() => 'This document has been signed by all recipients',
)
.with(
DocumentStatus.DRAFT,
() => 'This document is currently a draft and has not been sent',
)
.with(DocumentStatus.PENDING, () => {
const pendingRecipients = recipients.filter(
(recipient) => recipient.signingStatus === 'NOT_SIGNED',
);
return `Waiting on ${pendingRecipients.length} recipient${
pendingRecipients.length > 1 ? 's' : ''
}`;
})
.exhaustive()}
</p>
<div className="mt-4 border-t px-4 pt-4">
<DocumentPageViewButton document={documentWithRecipients} team={team} />
</div>
</section>
{/* Document information section. */}
<DocumentPageViewInformation document={documentWithRecipients} userId={user.id} />
{/* Recipients section. */}
<DocumentPageViewRecipients
document={documentWithRecipients}
documentRootPath={documentRootPath}
/>
{/* Recent activity section. */}
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
</div>
</div>
)}
</div>
</div>
);
};

View File

@ -1,12 +1,15 @@
'use client';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import type { DocumentData, DocumentMeta, Field, Recipient, User } from '@documenso/prisma/client';
import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -24,14 +27,11 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
className?: string;
user: User;
document: DocumentWithData;
recipients: Recipient[];
documentMeta: DocumentMeta | null;
fields: Field[];
documentData: DocumentData;
initialDocument: DocumentWithDetails;
documentRootPath: string;
};
@ -40,26 +40,83 @@ const EditDocumentSteps: EditDocumentStep[] = ['title', 'signers', 'fields', 'su
export const EditDocumentForm = ({
className,
document,
recipients,
fields,
documentMeta,
user: _user,
documentData,
initialDocument,
documentRootPath,
}: EditDocumentFormProps) => {
const { toast } = useToast();
const router = useRouter();
const searchParams = useSearchParams();
const team = useOptionalCurrentTeam();
// controlled stepper state
const [step, setStep] = useState<EditDocumentStep>(
document.status === DocumentStatus.DRAFT ? 'title' : 'signers',
);
const utils = trpc.useUtils();
const { data: document, refetch: refetchDocument } =
trpc.document.getDocumentWithDetailsById.useQuery(
{
id: initialDocument.id,
teamId: team?.id,
},
{
initialData: initialDocument,
...SKIP_QUERY_BATCH_META,
},
);
const { Recipient: recipients, Field: fields } = document;
const { mutateAsync: addTitle } = trpc.document.setTitleForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
);
},
});
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newFields) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
);
},
});
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newRecipients) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
);
},
});
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
);
},
});
const { mutateAsync: addTitle } = trpc.document.setTitleForDocument.useMutation();
const { mutateAsync: addFields } = trpc.field.addFields.useMutation();
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation();
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation();
const { mutateAsync: setPasswordForDocument } =
trpc.document.setPasswordForDocument.useMutation();
@ -86,14 +143,33 @@ export const EditDocumentForm = ({
},
};
const [step, setStep] = useState<EditDocumentStep>(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const searchParamStep = searchParams?.get('step') as EditDocumentStep | undefined;
let initialStep: EditDocumentStep =
document.status === DocumentStatus.DRAFT ? 'title' : 'signers';
if (
searchParamStep &&
documentFlow[searchParamStep] !== undefined &&
!(recipients.length === 0 && (searchParamStep === 'subject' || searchParamStep === 'fields'))
) {
initialStep = searchParamStep;
}
return initialStep;
});
const onAddTitleFormSubmit = async (data: TAddTitleFormSchema) => {
try {
// Custom invocation server action
await addTitle({
documentId: document.id,
teamId: team?.id,
title: data.title,
});
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('signers');
@ -110,13 +186,15 @@ export const EditDocumentForm = ({
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
try {
// Custom invocation server action
await addSigners({
documentId: document.id,
teamId: team?.id,
signers: data.signers,
});
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('fields');
} catch (err) {
console.error(err);
@ -131,13 +209,14 @@ export const EditDocumentForm = ({
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
try {
// Custom invocation server action
await addFields({
documentId: document.id,
fields: data.fields,
});
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('subject');
} catch (err) {
console.error(err);
@ -156,6 +235,7 @@ export const EditDocumentForm = ({
try {
await sendDocument({
documentId: document.id,
teamId: team?.id,
meta: {
subject,
message,
@ -192,6 +272,15 @@ export const EditDocumentForm = ({
const currentDocumentFlow = documentFlow[step];
/**
* Refresh the data in the background when steps change.
*/
useEffect(() => {
void refetchDocument();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step]);
return (
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
@ -200,10 +289,10 @@ export const EditDocumentForm = ({
>
<CardContent className="p-2">
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
key={document.documentData.id}
documentData={document.documentData}
document={document}
password={documentMeta?.password}
password={document.documentMeta?.password}
onPasswordSubmit={onPasswordSubmit}
/>
</CardContent>

View File

@ -0,0 +1,103 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ChevronLeft, Users2 } from 'lucide-react';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
export type DocumentEditPageViewProps = {
params: {
id: string;
};
team?: Team;
};
export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
const { id } = params;
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentWithDetailsById({
id: documentId,
userId: user.id,
teamId: team?.id,
}).catch(() => null);
if (!document) {
redirect(documentRootPath);
}
if (document.status === InternalDocumentStatus.COMPLETED) {
redirect(`${documentRootPath}/${documentId}`);
}
const { documentMeta, Recipient: recipients } = document;
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
const securePassword = Buffer.from(
symmetricDecrypt({
key,
data: documentMeta.password,
}),
).toString('utf-8');
documentMeta.password = securePassword;
}
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
Documents
</Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatus inheritColor status={document.status} className="text-muted-foreground" />
{recipients.length > 0 && (
<div className="text-muted-foreground flex items-center">
<Users2 className="mr-2 h-5 w-5" />
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
<span>{recipients.length} Recipient(s)</span>
</StackAvatarsWithTooltip>
</div>
)}
</div>
<EditDocumentForm
className="mt-8"
initialDocument={document}
documentRootPath={documentRootPath}
/>
</div>
);
};

View File

@ -0,0 +1,11 @@
import { DocumentEditPageView } from './document-edit-page-view';
export type DocumentPageProps = {
params: {
id: string;
};
};
export default function DocumentEditPage({ params }: DocumentPageProps) {
return <DocumentEditPageView params={params} />;
}

View File

@ -0,0 +1,165 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
export type DocumentLogsDataTableProps = {
documentId: number;
};
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
hourCycle: 'h12',
};
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
const parser = new UAParser();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.document.findDocumentAuditLogs.useQuery(
{
documentId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
page,
perPage,
});
};
const uppercaseFistLetter = (text: string) => {
return text.charAt(0).toUpperCase() + text.slice(1);
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
return (
<DataTable
columns={[
{
header: 'Time',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
},
{
header: 'User',
accessorKey: 'name',
cell: ({ row }) =>
row.original.name || row.original.email ? (
<div>
{row.original.name && (
<p className="truncate" title={row.original.name}>
{row.original.name}
</p>
)}
{row.original.email && (
<p className="truncate" title={row.original.email}>
{row.original.email}
</p>
)}
</div>
) : (
<p>N/A</p>
),
},
{
header: 'Action',
accessorKey: 'type',
cell: ({ row }) => (
<span>
{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}
</span>
),
},
{
header: 'IP Address',
accessorKey: 'ipAddress',
},
{
header: 'Browser',
cell: ({ row }) => {
if (!row.original.userAgent) {
return 'N/A';
}
parser.setUA(row.original.userAgent);
const result = parser.getResult();
return result.browser.name ?? 'N/A';
},
},
]}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
error={{
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell className="w-1/2 py-4 pr-4">
<div className="ml-2 flex flex-grow flex-col">
<Skeleton className="h-4 w-1/3 max-w-[8rem]" />
<Skeleton className="mt-1 h-4 w-1/2 max-w-[12rem]" />
</div>
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-10 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-10 rounded-full" />
</TableCell>
</>
),
}}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};

View File

@ -0,0 +1,151 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ChevronLeft, DownloadIcon } from 'lucide-react';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Recipient, Team } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card } from '@documenso/ui/primitives/card';
import { FRIENDLY_STATUS_MAP } from '~/components/formatter/document-status';
import { DocumentLogsDataTable } from './document-logs-data-table';
export type DocumentLogsPageViewProps = {
params: {
id: string;
};
team?: Team;
};
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
const { id } = params;
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
const { user } = await getRequiredServerComponentSession();
const [document, recipients] = await Promise.all([
getDocumentById({
id: documentId,
userId: user.id,
teamId: team?.id,
}).catch(() => null),
getRecipientsForDocument({
documentId,
userId: user.id,
teamId: team?.id,
}),
]);
if (!document || !document.documentData) {
redirect(documentRootPath);
}
const documentInformation: { description: string; value: string }[] = [
{
description: 'Document title',
value: document.title,
},
{
description: 'Document ID',
value: document.id.toString(),
},
{
description: 'Document status',
value: FRIENDLY_STATUS_MAP[document.status].label,
},
{
description: 'Created by',
value: document.User.name ?? document.User.email,
},
{
description: 'Date created',
value: document.createdAt.toISOString(),
},
{
description: 'Last updated',
value: document.updatedAt.toISOString(),
},
{
description: 'Time zone',
value: document.documentMeta?.timezone ?? 'N/A',
},
];
const formatRecipientText = (recipient: Recipient) => {
let text = recipient.email;
if (recipient.name) {
text = `${recipient.name} (${recipient.email})`;
}
return `${text} - ${recipient.role}`;
};
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link
href={`${documentRootPath}/${document.id}`}
className="flex items-center text-[#7AC455] hover:opacity-80"
>
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
Document
</Link>
<div className="flex flex-col justify-between sm:flex-row">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<Button variant="outline" className="mr-2 w-full sm:w-auto">
<DownloadIcon className="mr-1.5 h-4 w-4" />
Download certificate
</Button>
<Button className="w-full sm:w-auto">
<DownloadIcon className="mr-1.5 h-4 w-4" />
Download PDF
</Button>
</div>
</div>
<section className="mt-6">
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
{documentInformation.map((info, i) => (
<div className="text-foreground text-sm" key={i}>
<h3 className="font-semibold">{info.description}</h3>
<p className="text-muted-foreground">{info.value}</p>
</div>
))}
<div className="text-foreground text-sm">
<h3 className="font-semibold">Recipients</h3>
<ul className="text-muted-foreground list-inside list-disc">
{recipients.map((recipient) => (
<li key={`recipient-${recipient.id}`}>
<span className="-ml-2">{formatRecipientText(recipient)}</span>
</li>
))}
</ul>
</div>
</Card>
</section>
<section className="mt-6">
<DocumentLogsDataTable documentId={document.id} />
</section>
</div>
);
};

View File

@ -0,0 +1,11 @@
import { DocumentLogsPageView } from './document-logs-page-view';
export type DocumentsLogsPageProps = {
params: {
id: string;
};
};
export default function DocumentsLogsPage({ params }: DocumentsLogsPageProps) {
return <DocumentLogsPageView params={params} />;
}

View File

@ -108,88 +108,86 @@ export const ResendDocumentActionItem = ({
};
return (
<>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
<History className="mr-2 h-4 w-4" />
Resend
</DropdownMenuItem>
</DialogTrigger>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
<History className="mr-2 h-4 w-4" />
Resend
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-sm" hideClose>
<DialogHeader>
<DialogTitle>
<h1 className="text-center text-xl">Who do you want to remind?</h1>
</DialogTitle>
</DialogHeader>
<DialogContent className="sm:max-w-sm" hideClose>
<DialogHeader>
<DialogTitle asChild>
<h1 className="text-center text-xl">Who do you want to remind?</h1>
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
<FormField
control={form.control}
name="recipients"
render={({ field: { value, onChange } }) => (
<>
{recipients.map((recipient) => (
<FormItem
key={recipient.id}
className="flex flex-row items-center justify-between gap-x-3"
<Form {...form}>
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
<FormField
control={form.control}
name="recipients"
render={({ field: { value, onChange } }) => (
<>
{recipients.map((recipient) => (
<FormItem
key={recipient.id}
className="flex flex-row items-center justify-between gap-x-3"
>
<FormLabel
className={cn('my-2 flex items-center gap-2 font-normal', {
'opacity-50': !value.includes(recipient.id),
})}
>
<FormLabel
className={cn('my-2 flex items-center gap-2 font-normal', {
'opacity-50': !value.includes(recipient.id),
})}
>
<StackAvatar
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
{recipient.email}
</FormLabel>
<StackAvatar
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
{recipient.email}
</FormLabel>
<FormControl>
<Checkbox
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
checkClassName="text-white"
value={recipient.id}
checked={value.includes(recipient.id)}
onCheckedChange={(checked: boolean) =>
checked
? onChange([...value, recipient.id])
: onChange(value.filter((v) => v !== recipient.id))
}
/>
</FormControl>
</FormItem>
))}
</>
)}
/>
</form>
</Form>
<FormControl>
<Checkbox
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
checkClassName="text-white"
value={recipient.id}
checked={value.includes(recipient.id)}
onCheckedChange={(checked: boolean) =>
checked
? onChange([...value, recipient.id])
: onChange(value.filter((v) => v !== recipient.id))
}
/>
</FormControl>
</FormItem>
))}
</>
)}
/>
</form>
</Form>
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<DialogClose asChild>
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
disabled={isSubmitting}
>
Cancel
</Button>
</DialogClose>
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
Send reminder
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<DialogClose asChild>
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
disabled={isSubmitting}
>
Cancel
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</>
</DialogClose>
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
Send reminder
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -94,7 +94,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true },
() => (
<Button className="w-32" asChild>
<Link href={`${documentsPath}/${row.id}`}>
<Link href={`${documentsPath}/${row.id}/edit`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
Edit
</Link>

Some files were not shown because too many files have changed in this diff Show More