Compare commits
868 Commits
v1.2.1
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
| dc035ed08c | |||
| fb2fd17ad8 | |||
| 6285ef2cc0 | |||
| e2987b3ef1 | |||
| d97ab04d57 | |||
| 665c943d8f | |||
| 8fe6533ef5 | |||
| fd170f095b | |||
| 1400c335a5 | |||
| 03bf16522d | |||
| 627265f016 | |||
| 08b693ff95 | |||
| 97ce3530e0 | |||
| 33f3565715 | |||
| 950a697115 | |||
| fc70f78e61 | |||
| aa52316ee3 | |||
| ea64ccae29 | |||
| b87154001a | |||
| d4a7eb299e | |||
| 2ef619226e | |||
| 65c07032de | |||
| 56c550c9d2 | |||
| d1ffcb00f3 | |||
| 58481f66b8 | |||
| 484f603a6b | |||
| 48a8f5fe07 | |||
| cbe6270494 | |||
| b436331d7d | |||
| 81ee582f1c | |||
| 81ab220f1e | |||
| cc60437dcd | |||
| 171b8008f8 | |||
| 5c00b82894 | |||
| 369357aadd | |||
| 117d9427c3 | |||
| 7a689aecae | |||
| 1c54f69a5a | |||
| a56bf6a192 | |||
| a54eb54ef7 | |||
| 956562d3b4 | |||
| f386dd31a7 | |||
| c644d527df | |||
| 47cf20931a | |||
| b491bd4db9 | |||
| 038370012f | |||
| 4d2228f679 | |||
| 0aa111cd6e | |||
| ba30d4368d | |||
| 899205dde8 | |||
| 9eaecfcef2 | |||
| 26141050b7 | |||
| 5b4152ffc5 | |||
| bd703fb620 | |||
| 2296924ef6 | |||
| 6603aa6f2e | |||
| a6ddc114d9 | |||
| abb49c349c | |||
| 006b732edb | |||
| 5210fe2963 | |||
| 994368156f | |||
| 3eddfcc805 | |||
| 43400c07de | |||
| 715c14a6ae | |||
| 606966b357 | |||
| 24852f3c68 | |||
| 48ee977617 | |||
| fc34f1c045 | |||
| 1725af71b6 | |||
| c71347aeb9 | |||
| 007687bdee | |||
| f5a1d9a625 | |||
| 72fd1eead2 | |||
| 5289ae2fbc | |||
| c4c6e67249 | |||
| 5377d27c6a | |||
| 1cd7dd236b | |||
| 67beb8225c | |||
| 94198e7584 | |||
| facafe0997 | |||
| 8c1686f113 | |||
| a8752098f6 | |||
| 3e15b5d745 | |||
| 0dfdf36bda | |||
| 574cd176c2 | |||
| 48858cfdd0 | |||
| 2facc0e331 | |||
| e7071f1f5a | |||
| b95f7176e2 | |||
| 6d754acfcd | |||
| 796456929e | |||
| de9c9f4aab | |||
| b972056c8f | |||
| 69871e7d39 | |||
| 9cfd769356 | |||
| bd20c5499f | |||
| 3c6cc7fd46 | |||
| 4ac800fa78 | |||
| 3598bd0139 | |||
| d62838f4a0 | |||
| 8de8139b85 | |||
| a7594c9b3c | |||
| f012826b6b | |||
| 38752f95f3 | |||
| 4379b43ad9 | |||
| 8859b2779f | |||
| e29bfbf5e0 | |||
| 17c6a4bd55 | |||
| d6668ad204 | |||
| 91e1fe5e8f | |||
| fd4d5468cf | |||
| d5c4885c67 | |||
| 564f0dd945 | |||
| 524a7918d5 | |||
| 0db2e6643d | |||
| f5967e28c3 | |||
| 4926b6de50 | |||
| d6c8a3d32c | |||
| a9bb559568 | |||
| 8d1da3df72 | |||
| 00c71fd66c | |||
| df8d394c28 | |||
| bc3c9424c4 | |||
| 6643c4b9fc | |||
| ec7b69f1a4 | |||
| 0488442652 | |||
| cc483016d8 | |||
| 025af6e9f4 | |||
| e5497efe7c | |||
| bba1ea81d6 | |||
| 364aaa4cb6 | |||
| af6ec5df42 | |||
| 35c1b0bcee | |||
| 487bc026f9 | |||
| 3fb57c877e | |||
| 27e7e51789 | |||
| 52afae331e | |||
| 27a69819f9 | |||
| 4dc9e1295b | |||
| a8413fa031 | |||
| 3b65447b0f | |||
| d8911ee97b | |||
| c10cfbf6e1 | |||
| 884eab36eb | |||
| d0b9cee500 | |||
| a178e1d86f | |||
| 1fd29f7e89 | |||
| d3f4e20f1c | |||
| 3ebeb347c5 | |||
| f6c2b6c1c5 | |||
| efb90ca5fb | |||
| 9ac346443d | |||
| f2aa0cd714 | |||
| bbcb90d8a5 | |||
| c2cf25b138 | |||
| 63c23301a9 | |||
| 1a23744d2a | |||
| 7f31ab1ce3 | |||
| 7ac34099ee | |||
| c744482b84 | |||
| b71cb66dd3 | |||
| afe99e5ec9 | |||
| 6f958b9320 | |||
| f646fa29d7 | |||
| 373e806bd9 | |||
| c3c00dfe05 | |||
| 9e1b2e5cc3 | |||
| 608e622f69 | |||
| 415f79f821 | |||
| 62b4a13d4d | |||
| 19714fb807 | |||
| 7631c6e90e | |||
| d462ca0b46 | |||
| b433225762 | |||
| ad92b1ac23 | |||
| a4806f933e | |||
| 3b5f8d149a | |||
| 0d41c6babf | |||
| 9b5346efef | |||
| e8b209eb82 | |||
| 4476cf8fd1 | |||
| 0fdb7f7a8d | |||
| 05aa52b44a | |||
| 40343d1c72 | |||
| 08201240d2 | |||
| 32b0b1bcda | |||
| 61ca34eee1 | |||
| 41843691e8 | |||
| c463d5a0ed | |||
| 8afe669978 | |||
| fd4ea3aca5 | |||
| ee2cb0eedf | |||
| 1b32c5a17f | |||
| 6376112f9d | |||
| ddfd4b9e1b | |||
| 5bec549868 | |||
| 2f728f401b | |||
| bc60278bac | |||
| e9664d6369 | |||
| 3b3346e6af | |||
| ee35b4a24b | |||
| 47b06fa290 | |||
| b9dccdb359 | |||
| 5e00280486 | |||
| f0fd5506fc | |||
| ff3b49656c | |||
| e47ca1d6b6 | |||
| 59cdf3203e | |||
| 92f44cd304 | |||
| 3ce5b9e0a0 | |||
| e7f8f4e188 | |||
| 6c9303012c | |||
| 6fc3803ad2 | |||
| f7e7c6dedf | |||
| 0c426983bb | |||
| 9ae51a0072 | |||
| d694f4a17b | |||
| 80f767b321 | |||
| 4ec8eeeac1 | |||
| 8e813ab2ac | |||
| 51f60926ce | |||
| 2ccc2f22de | |||
| f6eddaa9f6 | |||
| 35b2405921 | |||
| 7b77084dee | |||
| a34334c4dc | |||
| 0c8b24ba75 | |||
| 8dad3607cf | |||
| cffb7907b5 | |||
| b9b57f16c0 | |||
| 03d2a2a278 | |||
| 82efc5f589 | |||
| 8a9588ca65 | |||
| f723a34464 | |||
| dd1d657057 | |||
| 10ef5b6e51 | |||
| b5b74a788c | |||
| 9525b9bd63 | |||
| d382e03085 | |||
| b8fa45380b | |||
| 2cce6dc2e5 | |||
| 927cb1a934 | |||
| 579a2f96e5 | |||
| 8a82952b34 | |||
| 70494fa5bb | |||
| 41ccefe212 | |||
| e83a4becee | |||
| a03ce728f3 | |||
| d29caaf823 | |||
| 7e9b96f059 | |||
| 691255da3f | |||
| f6f9c301da | |||
| a17d4a2a39 | |||
| 73aae6f1e3 | |||
| 328d16483c | |||
| 6dd2abfe51 | |||
| 452545dab1 | |||
| 437410c73a | |||
| 36a95f6153 | |||
| 0f03ad4a6b | |||
| 8674ad4c88 | |||
| 98e00739c8 | |||
| 00c36782ff | |||
| 665ccd7628 | |||
| e5fe3d897d | |||
| bfb1c65f98 | |||
| 3b8b87a90b | |||
| 819e58dd61 | |||
| 8c435d48b7 | |||
| 0c8a89a2ea | |||
| 85885b5ad5 | |||
| 5cc4204788 | |||
| e226f012a9 | |||
| 4ad722ba00 | |||
| 1927cc4067 | |||
| c2ec092404 | |||
| ebe23335f8 | |||
| e5be219b99 | |||
| aa87a86a5f | |||
| 1cc7c95a33 | |||
| 9f576eb47c | |||
| bd82ad7a0b | |||
| cc1b888174 | |||
| 5576cdc2b0 | |||
| ecc9dc63ea | |||
| 11fe380338 | |||
| f376c7b951 | |||
| 90477dfd00 | |||
| dd81f946b4 | |||
| f4e1df7f3a | |||
| 3510d8b6b0 | |||
| 1590fa9457 | |||
| ce09c32bf5 | |||
| 077da72b68 | |||
| c8869b3088 | |||
| 8bd1647a2d | |||
| 00a7389af3 | |||
| ae00290b6f | |||
| e3e2cfbcfd | |||
| 8ab77e0e55 | |||
| 93e34a5abd | |||
| f0a511e238 | |||
| 837ff531a9 | |||
| b02263eea1 | |||
| f5a0c1733f | |||
| 18368b5801 | |||
| b225cc8139 | |||
| b498f8edb7 | |||
| b1261510d2 | |||
| 65d762dd4b | |||
| deea6b1535 | |||
| af30443f5a | |||
| a4b1f7c983 | |||
| 7dd2bbd8ab | |||
| 488464e3e7 | |||
| 1ec549b869 | |||
| c2daa964c0 | |||
| 53d18eb3c8 | |||
| 21016216b6 | |||
| b5b0aeef6d | |||
| 8faf3afbbe | |||
| 0044b1f617 | |||
| 75f8305023 | |||
| 36e32639c9 | |||
| 25164abc14 | |||
| 1b8b81b16c | |||
| 4bdd41c7c5 | |||
| a31057d0d1 | |||
| 70165c4469 | |||
| 8dceeffff7 | |||
| 15c22d3897 | |||
| a54d09ab19 | |||
| 15ebe6dbaf | |||
| 5805d8a903 | |||
| f7c6b53258 | |||
| 4778270d3c | |||
| 3c51a1bc3d | |||
| fcfb741363 | |||
| a54159a9ec | |||
| 5bd0dde4a5 | |||
| 6b8e11b535 | |||
| c7dac2f4de | |||
| 4cb0d6999d | |||
| 2a74ce06ef | |||
| 6ea6dda99d | |||
| 187a988f9c | |||
| 5bef2fba91 | |||
| df3ba11655 | |||
| 9cf72e1442 | |||
| 7226d5ac53 | |||
| dbd63498e0 | |||
| 4ca154d88a | |||
| d34c862688 | |||
| a5e51e63a1 | |||
| 14c77d7c92 | |||
| a5a0fd9187 | |||
| 77193b93c4 | |||
| a10957d909 | |||
| 688e32dfc2 | |||
| b1b3b84d82 | |||
| b09acaf94e | |||
| fab4992e13 | |||
| d103c68a73 | |||
| a06b40af3c | |||
| 0900836040 | |||
| 9ea56bddd1 | |||
| 0b83671c78 | |||
| 41cbf3ba3c | |||
| 8165a090d1 | |||
| c436559787 | |||
| 15e191f62d | |||
| f973dfd09c | |||
| a58fee2da6 | |||
| 1232f54a23 | |||
| 306e5ff31f | |||
| 34825aaf3a | |||
| dd29845934 | |||
| a48bda0d27 | |||
| 48321f2f62 | |||
| 2abcdd7533 | |||
| df132a51ab | |||
| 8287722f59 | |||
| aba6b58c14 | |||
| b6c9213b66 | |||
| 22e3a79a72 | |||
| 6ee896048e | |||
| 4d286e01d1 | |||
| 3106c325d7 | |||
| b9e5905469 | |||
| 39c6cbf66a | |||
| c6dbaaea21 | |||
| ac6da9ab45 | |||
| 791a22cb5f | |||
| 11299d3f92 | |||
| 9301b8ef4d | |||
| ad6fad1182 | |||
| 5687503dfc | |||
| 0186f2dfed | |||
| f98567ea87 | |||
| 2815b1a809 | |||
| 5d6cdbef89 | |||
| 26d4bbf010 | |||
| 960914aeb5 | |||
| d83769b410 | |||
| cd240ae8a4 | |||
| a1459b41fd | |||
| a0cf2a2c75 | |||
| a30b73ce86 | |||
| 46d163d9d6 | |||
| 681a89cfe1 | |||
| 4d6e780abe | |||
| 7f3f6f5312 | |||
| 019db27b1d | |||
| e5f4edc120 | |||
| 25291b64eb | |||
| fddd860d15 | |||
| fe2093fe7c | |||
| 7fbe9b519c | |||
| bd3c64658a | |||
| 769eaa0ed9 | |||
| 49cddfab38 | |||
| 3e12a05ab8 | |||
| a76504c0a4 | |||
| abab0c0a22 | |||
| c680cfc24f | |||
| cab875f68a | |||
| 6daaa3a6d4 | |||
| 61958989b4 | |||
| 4c5b910a59 | |||
| f72b669f67 | |||
| 536cafde31 | |||
| d052f02013 | |||
| 4878cf388f | |||
| 149f416be7 | |||
| 524a2f4ea9 | |||
| 82b87739d0 | |||
| c432261dd8 | |||
| 1852aa4b05 | |||
| a868ecf2d2 | |||
| 071475769c | |||
| b1bb345929 | |||
| 1a82740d0f | |||
| 51608ed390 | |||
| 8ebef831ac | |||
| 20e2976731 | |||
| 3a32bc62c5 | |||
| 0209127136 | |||
| ddb9dd11d7 | |||
| b3ba77dfed | |||
| 4f990a7030 | |||
| e26debe836 | |||
| e91bb78f2d | |||
| 8641884515 | |||
| 748bf6de6b | |||
| d13cf743bf | |||
| 58f4b72939 | |||
| 09b5621542 | |||
| 98df273ebc | |||
| 47b8cc598c | |||
| e97b9b4f1c | |||
| b3514bd0c7 | |||
| e0889426bb | |||
| e2a5638f50 | |||
| 33ab8797a5 | |||
| c7e0b73c97 | |||
| b6bdbf72a7 | |||
| 718f5664ac | |||
| 58477e060a | |||
| 2e719288ff | |||
| 2431db06f5 | |||
| 754e9b15f7 | |||
| 7d39e3d065 | |||
| 7bb00c1c89 | |||
| 7ef771533a | |||
| 70c58470c1 | |||
| cad48236a0 | |||
| bf26f2cb9d | |||
| 58e2eda5e9 | |||
| 94ddb0252b | |||
| 2f696ddd13 | |||
| edeeaa5651 | |||
| 1dd543247e | |||
| 2636d5fd16 | |||
| fe4345eeb9 | |||
| 0c339b78b6 | |||
| 30752815e7 | |||
| bc989075ba | |||
| 4c09867b55 | |||
| 4d93ed60c5 | |||
| 9ed16c64d8 | |||
| 94e72534e0 | |||
| c6457e75e0 | |||
| f5930dc934 | |||
| c970abc871 | |||
| d5b3df1648 | |||
| 142c1c003e | |||
| a06c628653 | |||
| 7ca3697303 | |||
| d598677dc5 | |||
| 8ac2209493 | |||
| 8f3a52e1fd | |||
| 861225b7c4 | |||
| 7210d48b64 | |||
| 5cf2f0a30e | |||
| 9c4ec34a3c | |||
| 7ece6ef239 | |||
| e42088a5bf | |||
| ec3ba0e922 | |||
| 56683aa998 | |||
| 39be53ace8 | |||
| 7fbf124b89 | |||
| 1f142e334a | |||
| f4c24fd944 | |||
| 3541a805e5 | |||
| 08f82b23dc | |||
| 27d8098511 | |||
| ada46a5f47 | |||
| 747a7b0aea | |||
| 6053a4a40a | |||
| cc090adce0 | |||
| 1bda74b3aa | |||
| 9427143951 | |||
| 7e15058a3a | |||
| 620ae41fcc | |||
| f8125aec54 | |||
| 375df71f5c | |||
| 7b6d6fb1b9 | |||
| c0bb5205e1 | |||
| 1676f5bf6c | |||
| f514d55d27 | |||
| 9d6ee94708 | |||
| f3df0d9c13 | |||
| a3a4480b03 | |||
| 4af5ce3a6b | |||
| 4ae19a9e63 | |||
| 6d5fe4eea3 | |||
| 354e16901c | |||
| 09aa10dad6 | |||
| 014c09bd91 | |||
| 927a656c57 | |||
| 671fd916b5 | |||
| 751fb5275c | |||
| a3ddbc15e9 | |||
| b2cca9afb6 | |||
| c7a04c7184 | |||
| 8619e02d04 | |||
| 91c89e8bfb | |||
| fdeab19a7f | |||
| fd2a61f651 | |||
| e2fa01509d | |||
| 311c8da8fc | |||
| 56f65f3bb3 | |||
| 75ad8a4885 | |||
| db36f69273 | |||
| 49ecfc1a2c | |||
| ffee2b2c9a | |||
| 2f18518961 | |||
| d451a7acce | |||
| d8aecc4092 | |||
| d766b58f42 | |||
| e90dd518df | |||
| ee0af566a9 | |||
| 11dd93451a | |||
| 2be022b9fc | |||
| 0fac7d7b70 | |||
| ab8c8e2a57 | |||
| b115d85fb7 | |||
| 51d140cf9a | |||
| caec2895cc | |||
| 61967b22c1 | |||
| 8b8ca8578b | |||
| 576544344f | |||
| 1efadb19f5 | |||
| e5c2263e92 | |||
| bc1d5cea0a | |||
| 6aed075c56 | |||
| e63122a718 | |||
| 08011f9545 | |||
| 6e22eff5a1 | |||
| 4909eee401 | |||
| 5a28eaa4ff | |||
| 98667dac15 | |||
| e8c2ca8890 | |||
| 9e433af112 | |||
| 7762b1db65 | |||
| a3e560899a | |||
| f652ca9b73 | |||
| 9cc8bbdfc3 | |||
| 1191e1d9c3 | |||
| 9c1e1f50a8 | |||
| b6aface982 | |||
| b28a7f9702 | |||
| efb9e9f3ec | |||
| a7672545d7 | |||
| 1a10cd2ae1 | |||
| 204388888d | |||
| 0d977e783e | |||
| 0d15b80c2d | |||
| 4e9cce0df0 | |||
| 6f726565e8 | |||
| 3b82ba57f3 | |||
| 9ff44f10a6 | |||
| 16d97783f2 | |||
| 91dd10ec9b | |||
| a94b829ee0 | |||
| 1bc885478d | |||
| b4b146ee49 | |||
| 4aefb80989 | |||
| 560352492d | |||
| 67aebaac1a | |||
| a593e045b5 | |||
| 84b0c2756b | |||
| 58b3a127ea | |||
| 7e71e06e04 | |||
| 68953d1253 | |||
| 1a73f3e007 | |||
| d73ef57794 | |||
| ea0120abc8 | |||
| b501ffdee9 | |||
| 31050d6b7b | |||
| ed1998278a | |||
| eeb6a072aa | |||
| b09071ebc7 | |||
| 66bb56047a | |||
| 1d4e78e579 | |||
| f9d26e6b3f | |||
| 3054d84ba7 | |||
| a71078cbd5 | |||
| 6065140715 | |||
| 34a59d2db3 | |||
| ba37633ecd | |||
| 46e83d65bb | |||
| 142c93aa63 | |||
| 3eb1a17d3c | |||
| 6d1ad179d4 | |||
| 4fd6a0d5b6 | |||
| fface15a22 | |||
| 8eed13e275 | |||
| 346078dbbe | |||
| c8337d7dcc | |||
| 75630ef19d | |||
| 634807328e | |||
| 2bbbe1098a | |||
| d24b9de254 | |||
| c86f79dd7b | |||
| 0c12e34c38 | |||
| 26b604dbd0 | |||
| 308f55f3d4 | |||
| e5b7bf81fa | |||
| b35f050409 | |||
| ce6f523230 | |||
| 9e57de512a | |||
| 9b5d64cc1a | |||
| fc372d0aa9 | |||
| e470020b16 | |||
| 0a9006430f | |||
| 8e754405f8 | |||
| 6be119ac95 | |||
| b8a45dd5e3 | |||
| 5660b99df7 | |||
| 8c5216cd44 | |||
| e646c4cf08 | |||
| d8cbe1d5ba | |||
| 5d56b152d6 | |||
| 5c16b10dc2 | |||
| 9bcd6e39e7 | |||
| 2202fa3d04 | |||
| c1a6a327af | |||
| 837c17f1f3 | |||
| d731532fbf | |||
| 6a26ab4b2b | |||
| b76d2cea3b | |||
| d02f6774b2 | |||
| a1215df91a | |||
| 77facba8b4 | |||
| 53c570151f | |||
| e900706ab0 | |||
| bed788f78f | |||
| 72a7dc6c05 | |||
| 3fb711cb42 | |||
| 8a8a5510cb | |||
| ce67de9a1c | |||
| cf5841a895 | |||
| 341481d6db | |||
| 8f5634268d | |||
| 5d6f69dc19 | |||
| 5307fa6453 | |||
| 0bb86963a9 | |||
| fb0d9b8ef9 | |||
| 918c6f19f2 | |||
| 3f89f8725b | |||
| c4800f74b9 | |||
| 2bc5d15af2 | |||
| d8eff192fe | |||
| eb84d7ff3c | |||
| 32633f96d2 | |||
| 27b7e29be7 | |||
| de4a0b2560 | |||
| 5a11de1db9 | |||
| f7cf33c61b | |||
| 8f4fea2f14 | |||
| 5a32b5cafd | |||
| 8d1b960aa8 | |||
| 2b25806c33 | |||
| 6d58e60a65 | |||
| 2ae9e29903 | |||
| dd56836121 | |||
| e9312ada51 | |||
| 1c52c7ebcd | |||
| 495cd35f7c | |||
| 5a5d00fb2e | |||
| 1aa0fc3101 | |||
| d283cc2d26 | |||
| 48cdf43dcb | |||
| 1d9593dd0f | |||
| 9ad94f9862 | |||
| 972c20f906 | |||
| 519c645d06 | |||
| 7babd82470 | |||
| 6a56905fea | |||
| 298396c86c | |||
| 268a5c6508 | |||
| c40c9b20ec | |||
| a22ada5f41 | |||
| fb46b09e4f | |||
| 2a448fe06d | |||
| 84a0c39810 | |||
| 1af909835d | |||
| 01caa949d9 | |||
| 17486b961d | |||
| 775a1b774d | |||
| c949c4701b | |||
| eda635e2db | |||
| 2056de2e16 | |||
| da03fc1fd0 | |||
| 075fdd1f88 | |||
| 006a559026 | |||
| ff64671e49 | |||
| 089ba1c30e | |||
| cbcd893cfd | |||
| 83dfe92d7a | |||
| e43e8f8c4a | |||
| 82da337a56 | |||
| 6e10947d00 | |||
| 682cb37786 | |||
| 5809480f02 | |||
| 19736ce60b | |||
| 1eeb5fb103 | |||
| 88534fa1c6 | |||
| 31a9127c9e | |||
| 6d34ebd91b | |||
| f2d4c0721d | |||
| f9139a54a5 | |||
| 2d931b2c9b | |||
| e79d385534 | |||
| 8ecd8a7d10 | |||
| 5c1d30bfbb | |||
| 95041fa2e4 | |||
| 49736d2587 | |||
| ee5ce78c82 | |||
| 3b3987dcf8 | |||
| 78a1ee2af0 | |||
| dbdef79263 | |||
| 323380d757 | |||
| e4b7747f66 | |||
| 0697e7f817 | |||
| e1d3874e79 | |||
| 497d9140d2 | |||
| 7d22957404 | |||
| 66c0db91da | |||
| 38e5b1d3ce | |||
| 54401b94ae | |||
| 09dcc2cac0 | |||
| d8d36ae8e2 | |||
| dfec8df31e | |||
| bc38009392 | |||
| 5e9bc56329 | |||
| 48f6765e76 | |||
| 7feba02e08 | |||
| b9282f11b0 | |||
| 4e799e68ef | |||
| 1831084970 | |||
| 38ad3a1922 | |||
| 935601ad16 | |||
| 7b4e38a032 | |||
| 2c5d547cdf | |||
| c313da5028 | |||
| 5b98bac53b | |||
| d7e44fc068 | |||
| e03db5c6b3 | |||
| d58433c8a0 | |||
| 419f27536b | |||
| 9a7e5d333d | |||
| 39b97a97fe | |||
| 328b2e7604 | |||
| 43b1a89c76 | |||
| cd6184406d | |||
| 1a34f9fa7a | |||
| 3ff7b188d7 | |||
| 684e5272d2 | |||
| 11ae6d3c16 | |||
| 6c5526dd49 | |||
| 936e75fd30 | |||
| b39a42ecd2 | |||
| e81183f324 | |||
| bfc630aa6a | |||
| 2068d980ff | |||
| 0baa2696b4 | |||
| 741201822a | |||
| 520522bef7 | |||
| 8ab1b0cf6b | |||
| bfedabdc10 | |||
| 52e696c90e | |||
| 02d91d9cd4 | |||
| ac529a89fc | |||
| f181099e74 | |||
| 02e96bbd0a | |||
| 36e48e67ee | |||
| d8588b780a | |||
| ef84f5ba98 | |||
| 68120794f8 | |||
| 88dc797423 | |||
| a43be0432b | |||
| 0f11cc0b4b | |||
| f310139a13 | |||
| 859b789018 | |||
| 340c929806 | |||
| 43b1a14415 | |||
| 40a4ec4436 | |||
| eccf63dcfd | |||
| a98b429052 | |||
| c46a69f865 | |||
| 369d08ae6e | |||
| a906833657 | |||
| 4733f1e84b | |||
| ab0d38eaf4 | |||
| b903de983b | |||
| 6b519a67c2 | |||
| 39d18e93c5 | |||
| 7dac5072f7 | |||
| 6be4b7ae90 | |||
| 6bbeaa084c | |||
| 231a307b89 | |||
| 0b2dce2238 | |||
| 1e29dfd823 | |||
| dc56c2abf2 | |||
| 62809e9506 | |||
| 318dfcafc3 | |||
| 4ff8592e8f | |||
| 76800674ee | |||
| d43d40fd6b | |||
| e1732de81d | |||
| 6a5fc7a5fb | |||
| 13997d3dca | |||
| 2deaad5c34 | |||
| 0e40658201 | |||
| d347359d2f | |||
| fdf5b3908d | |||
| fbee6eedc1 | |||
| 80fe7ccdf5 | |||
| 2ccede72ea | |||
| 309b56168a | |||
| 5c8a77ee8f | |||
| b3008fb272 | |||
| 6d6c93539f | |||
| 4a6b3edc05 | |||
| 24d9906557 | |||
| b4f1a5abce |
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
52
.env.example
@ -4,8 +4,10 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# [[CRYPTO]]
|
||||
# Application Key for symmetric encryption and decryption
|
||||
# This should be a random string of at least 32 characters
|
||||
# REQUIRED: This should be a random string of at least 32 characters
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
|
||||
# REQUIRED: This should be a random string of at least 32 characters
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
||||
|
||||
# [[AUTH OPTIONAL]]
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||
@ -20,24 +22,40 @@ 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_password"
|
||||
# [[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=
|
||||
|
||||
# [[STORAGE]]
|
||||
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
|
||||
# OPTIONAL: Defines the endpoint to use for the S3 storage transport. Relevant when using third-party S3-compatible providers.
|
||||
NEXT_PRIVATE_UPLOAD_ENDPOINT=
|
||||
NEXT_PRIVATE_UPLOAD_ENDPOINT="http://127.0.0.1:9002"
|
||||
# OPTIONAL: Defines the force path style to use for the S3 storage transport. Relevant when using third-party S3-compatible providers.
|
||||
# This will change it from using virtual hosts <bucket>.domain.com/<path> to fully qualified paths domain.com/<bucket>/<path>
|
||||
NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE="false"
|
||||
# OPTIONAL: Defines the region to use for the S3 storage transport. Defaults to us-east-1.
|
||||
NEXT_PRIVATE_UPLOAD_REGION=
|
||||
NEXT_PRIVATE_UPLOAD_REGION="unknown"
|
||||
# REQUIRED: Defines the bucket to use for the S3 storage transport.
|
||||
NEXT_PRIVATE_UPLOAD_BUCKET=
|
||||
NEXT_PRIVATE_UPLOAD_BUCKET="documenso"
|
||||
# OPTIONAL: Defines the access key ID to use for the S3 storage transport.
|
||||
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=
|
||||
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso"
|
||||
# OPTIONAL: Defines the secret access key to use for the S3 storage transport.
|
||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=
|
||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
|
||||
|
||||
# [[SMTP]]
|
||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
||||
@ -72,21 +90,27 @@ NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=
|
||||
NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=
|
||||
# OPTIONAL: The private key to use for DKIM signing.
|
||||
NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=
|
||||
# OPTIONAL: Displays the maximum document upload limit to the user in MBs
|
||||
NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=5
|
||||
|
||||
# [[STRIPE]]
|
||||
NEXT_PRIVATE_STRIPE_API_KEY=
|
||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
|
||||
NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID=
|
||||
|
||||
# [[FEATURES]]
|
||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
# OPTIONAL: Defines the host to use for PostHog.
|
||||
NEXT_PUBLIC_POSTHOG_HOST="https://eu.posthog.com"
|
||||
# OPTIONAL: Leave blank to disable billing.
|
||||
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]]
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -1,11 +1,10 @@
|
||||
name: "Bug Report"
|
||||
labels: ["bug"]
|
||||
name: 'Bug Report'
|
||||
labels: ['bug']
|
||||
description: Create a bug report to help us improve
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value:
|
||||
Thank you for reporting an issue.
|
||||
value: Thank you for reporting an issue.
|
||||
Please fill in as much of the form below as you're able to.
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -1,9 +1,9 @@
|
||||
name: "Feature Request"
|
||||
name: 'Feature Request'
|
||||
description: Suggest a new idea or enhancement for this project
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please provide a clear and concise title for your feature request
|
||||
value: Please provide a clear and concise title for your feature request
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature Description
|
||||
@ -32,4 +32,5 @@ body:
|
||||
- label: I have provided a detailed description of the requested feature.
|
||||
- label: I have explained the use case or scenario for this feature.
|
||||
- label: I have included any relevant technical details or design suggestions.
|
||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||
- label: I want to work on creating a PR for this issue if approved
|
||||
|
||||
56
.github/ISSUE_TEMPLATE/improvement.yml
vendored
@ -1,35 +1,39 @@
|
||||
name: "General Improvement"
|
||||
description: Suggest a minor enhancement or improvement for this project
|
||||
name: 'General Improvement Request'
|
||||
description: 'Suggest a minor enhancement or improvement for this project'
|
||||
title: '[Title for your improvement suggestion]'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please provide a clear and concise title for your improvement suggestion
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Improvement Description
|
||||
description: Describe the improvement you are suggesting in detail. Explain what specific aspect of the project it addresses or enhances.
|
||||
label: 'Describe the improvement you are suggesting in detail'
|
||||
description: 'Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Rationale
|
||||
description: Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.
|
||||
- type: textarea
|
||||
label: 'Additional Information & Alternatives (optional)'
|
||||
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: assignee
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: If you have a suggestion for how this improvement could be implemented, describe it here. Include any technical details, design suggestions, or other relevant information.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives (optional)
|
||||
description: Are there any alternative approaches to achieve the same improvement? Describe other ways to address the issue or enhance the project.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any additional context or information that might be relevant to the improvement suggestion.
|
||||
label: 'Do you want to work on this improvement?'
|
||||
multiple: false
|
||||
options:
|
||||
- 'No'
|
||||
- 'Yes'
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check the boxes that apply to this improvement suggestion.
|
||||
label: 'Please check the boxes that apply to this improvement suggestion.'
|
||||
options:
|
||||
- label: I have searched the existing issues and improvement suggestions to avoid duplication.
|
||||
- label: I have provided a clear description of the improvement being suggested.
|
||||
- label: I have explained the rationale behind this improvement.
|
||||
- label: I have included any relevant technical details or design suggestions.
|
||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||
- label: 'I have searched the existing issues and improvement suggestions to avoid duplication.'
|
||||
- label: 'I have provided a clear description of the improvement being suggested.'
|
||||
- label: 'I have explained the rationale behind this improvement.'
|
||||
- label: 'I have included any relevant technical details or design suggestions.'
|
||||
- label: 'I understand that this is a suggestion and that there is no guarantee of implementation.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
24
.github/actions/cache-build/action.yml
vendored
Normal 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
@ -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'
|
||||
19
.github/actions/playwright-install/action.yml
vendored
Normal 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
|
||||
32
.github/dependabot.yml
vendored
@ -4,29 +4,29 @@ updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
interval: 'weekly'
|
||||
target-branch: 'main'
|
||||
labels:
|
||||
- "ci dependencies"
|
||||
- "ci"
|
||||
- 'ci dependencies'
|
||||
- 'ci'
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/marketing"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/apps/marketing'
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
interval: 'weekly'
|
||||
target-branch: 'main'
|
||||
labels:
|
||||
- "npm dependencies"
|
||||
- "frontend"
|
||||
- 'npm dependencies'
|
||||
- 'frontend'
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/web"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/apps/web'
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
interval: 'weekly'
|
||||
target-branch: 'main'
|
||||
labels:
|
||||
- "npm dependencies"
|
||||
- "frontend"
|
||||
- 'npm dependencies'
|
||||
- 'frontend'
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
21
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
'apps: marketing':
|
||||
- apps/marketing/**
|
||||
|
||||
'apps: web':
|
||||
- apps/web/**
|
||||
|
||||
'version bump 👀':
|
||||
- '**/package.json'
|
||||
- '**/package-lock.json'
|
||||
|
||||
'🚨 migrations 🚨':
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
|
||||
'🚨 e2e changes 🚨':
|
||||
- packages/app-tests/e2e/**
|
||||
|
||||
'🚨 .env changes 🚨':
|
||||
- .env.example
|
||||
|
||||
'pkg: ee changes':
|
||||
- packages/ee/**
|
||||
55
.github/workflows/ci.yml
vendored
@ -1,52 +1,67 @@
|
||||
name: "Continuous Integration"
|
||||
name: 'Continuous Integration'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
build_app:
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Build Docker Image
|
||||
run: ./docker/build.sh
|
||||
- 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
|
||||
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
@ -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
|
||||
39
.github/workflows/codeql-analysis.yml
vendored
@ -1,11 +1,11 @@
|
||||
name: "CodeQL"
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@ -19,30 +19,23 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
language: ['javascript']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
- name: Build Documenso
|
||||
run: npm run build
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
24
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Push to release branch
|
||||
run: |
|
||||
git checkout release || git checkout -b release
|
||||
git merge --ff-only main
|
||||
git push origin release
|
||||
51
.github/workflows/e2e-tests.yml
vendored
@ -1,51 +1,44 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ['main']
|
||||
jobs:
|
||||
e2e_tests:
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
- 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/node-install
|
||||
|
||||
- name: Start Services
|
||||
run: npm run dx:up
|
||||
|
||||
- uses: ./.github/actions/playwright-install
|
||||
|
||||
- name: Create the database
|
||||
run: npm run prisma:migrate-dev
|
||||
|
||||
- name: Seed the database
|
||||
run: npm run prisma:seed
|
||||
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run ci
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
name: test-results
|
||||
path: 'packages/app-tests/**/test-results/*'
|
||||
retention-days: 30
|
||||
env:
|
||||
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
29
.github/workflows/first-interaction.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: 'Welcome New Contributors'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened']
|
||||
issues:
|
||||
types: ['opened']
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
welcome-message:
|
||||
name: Welcome Contributors
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |
|
||||
Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀
|
||||
<br /> Feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||
issue-message: |
|
||||
Thank you for opening your first issue and for being a part of the open signing revolution!
|
||||
<br /> One of our team members will review it and get back to you as soon as it possible 💚
|
||||
<br /> Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||
63
.github/workflows/issue-assignee-check.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: 'Issue Assignee Check'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: ['assigned']
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
countIssues:
|
||||
if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: npm
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm install @octokit/rest@18
|
||||
|
||||
- name: Check Assigned User's Issue Count
|
||||
id: parse-comment
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||
|
||||
const username = context.payload.issue.assignee.login;
|
||||
console.log(`Username Extracted: ${username}`);
|
||||
|
||||
const { data: issues } = await octokit.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
assignee: username,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
const issueCount = issues.length;
|
||||
console.log(`Issue Count For ${username}: ${issueCount}`);
|
||||
|
||||
if (issueCount > 3) {
|
||||
let issueCountMessage = `### 🚨 Documenso Police 🚨`;
|
||||
issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`;
|
||||
|
||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: issueCountMessage,
|
||||
headers: {
|
||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
21
.github/workflows/issue-opened.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: 'Label Issues'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: ['opened', 'reopened']
|
||||
|
||||
jobs:
|
||||
label_issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["needs triage"]
|
||||
})
|
||||
20
.github/workflows/pr-labeler.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: 'PR Labeler'
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sync-labels: ''
|
||||
64
.github/workflows/pr-review-reminder.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: 'PR Review Reminder'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened', 'reopened', 'ready_for_review', 'review_requested']
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
checkPRs:
|
||||
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'reopened' || 'ready_for_review' || 'review_requested')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: npm
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm install @octokit/rest@18
|
||||
|
||||
- name: Check user's PRs awaiting review
|
||||
id: parse-prs
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||
|
||||
const username = context.payload.pull_request.user.login;
|
||||
console.log(`Username Extracted: ${username}`);
|
||||
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
});
|
||||
|
||||
const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review'));
|
||||
const prCount = userPullRequests.length;
|
||||
console.log(`PR Count for ${username}: ${prCount}`);
|
||||
|
||||
if (prCount > 3) {
|
||||
let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`;
|
||||
|
||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: prReminderMessage,
|
||||
headers: {
|
||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
133
.github/workflows/publish.yml
vendored
Normal 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
|
||||
45
.github/workflows/semantic-pull-requests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Validate PR Name"
|
||||
name: 'Validate PR Name'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@ -9,13 +9,54 @@ on:
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR creator's previous activity
|
||||
id: check_activity
|
||||
run: |
|
||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
||||
ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
||||
if [ "$ACTIVITY" -eq 0 ]; then
|
||||
echo "::set-output name=is_new::true"
|
||||
else
|
||||
echo "::set-output name=is_new::false"
|
||||
fi
|
||||
|
||||
- name: Count PRs created by user
|
||||
id: count_prs
|
||||
run: |
|
||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
||||
PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
||||
echo "::set-output name=pr_count::$PR_COUNT"
|
||||
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey There! and thank you for opening this pull request! 📝👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions for pull request titles! 💚🚀
|
||||
|
||||
24
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: 'Mark Stale Issues and PRs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */8 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-pr-stale: 90
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 180
|
||||
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
|
||||
close-pr-message: 'This PR has been closed because of inactivity.'
|
||||
exempt-pr-labels: 'WIP,on-hold,needs review'
|
||||
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage'
|
||||
@ -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
|
||||
|
||||
9
.vscode/settings.json
vendored
@ -1,10 +1,15 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.useAliasesForRenames": false,
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true
|
||||
}
|
||||
|
||||
7
.well-known/security.txt
Normal 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
|
||||
56
README.md
@ -13,9 +13,9 @@
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
|
||||
<a href="https://documen.so/live">Upcoming Releases</a>
|
||||
·
|
||||
<a href="https://documen.so/launches">Upcoming Launches</a>
|
||||
<a href="https://documen.so/roadmap">Roadmap</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@ -27,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
|
||||
@ -91,7 +82,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
||||
- [NextAuth.js](https://next-auth.js.org/) - Authentication
|
||||
- [react-email](https://react.email/) - Email Templates
|
||||
- [tRPC](https://trpc.io/) - API
|
||||
- [Node SignPDF](https://github.com/vbuch/node-signpdf) - Digital Signature
|
||||
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures
|
||||
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
||||
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
||||
- [Stripe](https://stripe.com/) - Payments
|
||||
@ -105,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)
|
||||
|
||||
@ -115,10 +106,12 @@ To run Documenso locally, you will need
|
||||
|
||||
Want to get up and running quickly? Follow these steps:
|
||||
|
||||
1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account.
|
||||
|
||||
After forking the repository, clone it to your local device by using the following command:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
git clone https://github.com/<your-username>/documenso
|
||||
```
|
||||
|
||||
2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively, just run `cp .env.example .env` to get started with our handpicked defaults.
|
||||
@ -139,21 +132,25 @@ npm run d
|
||||
|
||||
1. **App** - http://localhost:3000
|
||||
2. **Incoming Mail Access** - http://localhost:9000
|
||||
|
||||
3. **Database Connection Details**
|
||||
|
||||
- **Port**: 54320
|
||||
- **Connection**: Use your favorite database client to connect using the provided port.
|
||||
|
||||
4. **S3 Storage Dashboard** - http://localhost:9001
|
||||
|
||||
## Developer Setup
|
||||
|
||||
### Manual Setup
|
||||
|
||||
Follow these steps to setup Documenso on your local machine:
|
||||
|
||||
1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account.
|
||||
|
||||
After forking the repository, clone it to your local device by using the following command:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
git clone https://github.com/<your-username>/documenso
|
||||
```
|
||||
|
||||
2. Run `npm i` in the root directory
|
||||
@ -201,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
|
||||
|
||||
@ -278,12 +284,16 @@ WantedBy=multi-user.target
|
||||
|
||||
### Railway
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
[](https://railway.app/template/bG6D4p)
|
||||
|
||||
### Render
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||
|
||||
### Koyeb
|
||||
|
||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I'm not receiving any emails when using the developer quickstart.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Building Documenso — Part 1: Certificates'
|
||||
description: In today's fast-paced world, productivity and efficiency are crucial for success, both in personal and professional endeavors. We all strive to make the most of our time and energy to achieve our goals effectively. However, it's not always easy to stay on track and maintain peak performance. In this blog post, we'll explore 10 valuable tips to help you boost productivity and efficiency in your daily life.
|
||||
description: Let's take a look why you need a signing certificate and how Documenso does it.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
|
||||
117
apps/marketing/content/blog/building-documenso-pt2.mdx
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
title: 'Building Documenso — Part 2: Signature Validity'
|
||||
description: Is a signature valid? And what does that mean? It's a surprisingly complex question; let's take a look.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-04-05
|
||||
tags:
|
||||
- Document Signature
|
||||
- Certificates
|
||||
- Signing
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/eu-validate-1.png"
|
||||
width= "650"
|
||||
height= "650"
|
||||
alt= "A report card for signature validity."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
If a tree does not comply with the EU trust list, does it make a sound when validating?r
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; Signatures can be valid and compliant for different signature levels, even if some validators show higher-level errors. Not all helpful security measures are mandated by law.
|
||||
|
||||
# A valid question
|
||||
|
||||
A few days ago, an early adopter brought up this question in our [Discord](https://documen.so/discord):
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/eu-validate-2.png"
|
||||
width= "650"
|
||||
height= "650"
|
||||
alt= "A report card for signature validity."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
You can check out the validator here: [https://documen.so/eu-validator](https://documen.so/eu-validator)
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
For those unfamiliar with the tool, he used the validator tool of the EU's Digital Signature Service (DSS) Framework to check the signature of a document signed with Documenso. The EU provides this tool to help users and providers check the validity level of their signatures.
|
||||
|
||||
A short refresher from [Building Documenso — Part 1: Certificates](https://documen.so/certs):
|
||||
|
||||
> Documenso inserts all visual signatures into the document and then seals it using the "Documenso Inc." corporate certificate. This makes the resulting PDF document tamper-proof and guarantees it hasn't changed since signing.
|
||||
|
||||
Before we answer if the document was signed correctly, we need to understand what the goal was.
|
||||
|
||||
There are three signature levels in the European eIDAS regulation:
|
||||
|
||||
1. **Simple Electronic Signatures (Level 1/ SES):** This is just a visual signature or even a checkbox on a document.
|
||||
|
||||
2. **Advanded Electronic Signatures (Level 2/ AES)**: An actual crypographic signature (not just a seal on the whole document, but a specific signature), using a certificate linked to the identification data of the signer.
|
||||
|
||||
3. **Qualified Electronic Signatures (Level 3/ QES):** Same as 2. but done by a government-certified entity on certified hardware and after identifying the signer with an official ID document (e.g., passport)
|
||||
|
||||
> 💡 Side Note: Number 2 (AES) is how most people imagine digital signatures. But most of the market uses 1. plus a seal on the whole document under the name of the signing provider (e.g., Documenso). The signer's data is only inserted visually, not in the actual signature. Why? One of the reasons is that it's much easier, and without a readily available open source framework to draw from, it is quite tricky to build. This is something we aim to build (which many have done) and open source (which no one has done).
|
||||
|
||||
From the perspective of eIDAS, Documenso offers Level 1/ SES signatures since it does not adhere to all of the requirements of Level 2/ AES. This means that, technically, there is no legal need to seal the document to achieve this level of validity (at least within eIDAS). We do it anyway since it improves the level of confidence users can have in the signed document. Sealing the document, even though not legally required, is a great example of Documenso's approach to signatures. First, we aim to provide all legal requirements for a given use case. Then, we add any protection that can be added without unwarranted friction to the creation of the signature.
|
||||
|
||||
## Not if valid, but how valid
|
||||
|
||||
**Q: So, is the signature in the image valid?**
|
||||
|
||||
A: Yes, as an eidas Level 1 SES.
|
||||
|
||||
**Q: Then why does it say "Unable to build a certificate chain up to a trusted list"**
|
||||
|
||||
A: The certificate we use to seal the document after inserting the signatures is not on the EU Trust list.
|
||||
|
||||
**Q: Does that mean it is less secure?**
|
||||
|
||||
A: No, it means the provider (Wisekey) is not on a list maintained by the EU. The cryptographic signature is just as strong as any other
|
||||
|
||||
For someone who does not deal with this stuff daily, this can be hard to comprehend. Whether you use a certificate you generated yourself, one generated by a certificate authority (CA) like Wisekey, or one by another on the EU trust list (e.g., Bundesdruckerei), the cryptographic security guaranteeing that the document has not been tampered with is always the same. Many providers like Documenso, DocuSign, PandaDoc, and Digisigner all use this method for their regular plans. That means if you were to run a document signed by them through the validator above, the result would be the same[1]. The interesting question is why? Why do it like this?
|
||||
|
||||
## Certificate Infrastructure is broken
|
||||
|
||||
While there are some actual expenses involved in providing AES and QES, the blunt reality is that it's just good business to charge for them per signature, making it unsuitable for the "standard offerings"; almost no one has the resources to set this up themselves. While this initial process of becoming a QES-certified entity is really expensive, selling the certificates afterward is very lucrative. This leads to less innovation in the space and only big players providing these high-compliance services. Even certificates only used to seal documents without being QES certified are sold for a large range of prices, and they cost almost nothing to produce.
|
||||
|
||||
## Why Though?
|
||||
|
||||
**Q: Why do people buy a certificate for money and not just generate one themselves? Isn't the cryptographic security the same?**
|
||||
|
||||
A: Self-generated certificates are not recognized for higher-level compliance signatures like QES
|
||||
|
||||
**Q: So if you don't need higher-level signatures, you could just generate one yourself?**
|
||||
|
||||
A: Yes, you could. Since eIDAS Level 1 does not require a cert, you could use your own.
|
||||
|
||||
**Q: Why don't more people?**
|
||||
|
||||
A: One reason is that apart from the EU trust list, there are others, like the Adobe trust list. While not legally required, being on that one (like Wisekey) gives you a green checkmark in Adobe PDF, which is how most people check signature validity.
|
||||
|
||||
**Q: Not a question, but all of this sounds weird**
|
||||
|
||||
A: It is. This is one of the reasons why Documenso exists. We plan to make this easier.
|
||||
|
||||
**Q: How?**
|
||||
|
||||
A: By explaining and providing easy-to-use tools and eventually free, highly compliant signature certificates for everyone.
|
||||
|
||||
Eventually, we plan to start a free certificate authority called Let's Sign, named after another instituion that broke the paid certificate paradigm to the benefit of the internet: [Let's Encrypt](https://letsencrypt.org/).
|
||||
|
||||
As always, feel free to connect on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments.
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
\
|
||||
\
|
||||
\
|
||||
[1] The signature format (e.g. PKCS7-B) will vary. It's the format what the signature inserted into the document looks like. eIDAS itself does not specifically require any given format, but the PAdES defined by the EU is mostly used by european providers.
|
||||
87
apps/marketing/content/blog/commodifying-signing.mdx
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Commodifying Signing
|
||||
description: We are creating signing as a public good and are commoditizing it to make it cheaper and better.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-25
|
||||
tags:
|
||||
- Vision
|
||||
- Mission
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/lighthouse.jpeg"
|
||||
width="650"
|
||||
height="650"
|
||||
alt="A lighthouse on a tiny island."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Lighthouses are often used as an example of a public good; As they benefit all maritime users, but no one can be excluded from using them as a navigational aid. Use by one person neither prevents access by other people, nor does it reduce availability to others.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
# Commodifying Signing
|
||||
|
||||
> TLDR; We are creating signing as a public good and are commoditizing it to make it cheaper and better.
|
||||
|
||||
While we are in full-on building mode with Documenso, I think a lot about the big picture of what we are attempting to do. One phrase that keeps popping up is, "We are commodifying signing." Let's dig deeper into what that means.
|
||||
|
||||
Let's start with why we are doing this. Documenso's mission is to solve the domain of signing once and for all for everyone. In so many calls, I hear stories about how organizations build their own solution because the existing ones are too expensive or need to be more flexible. That means not hundreds but probably thousands of companies worldwide have done the same. This is simply wasting humanity's time. Since digital signing systems are understood well enough that seemingly "everyone" can build them, given enough pain, It's time to do it once correctly.
|
||||
|
||||
## Is signing already a commodity?
|
||||
|
||||
> In economics, a **commodity** is an economic good, usually a resource, that has explicitly full or substantial fungibility: that is, the market treats instances of the good as equivalent or nearly so with no regard to who produced them.
|
||||
|
||||
That sounds like the signing market today. There is no shortage of signing providers, and you can get similar signing services from many places. So why is this different from what we want, and why does this not satisfy the market?
|
||||
|
||||
- Signing is expensive and painful when you are locked into your vendor, and they charge by signing volume.
|
||||
- Signing is also expensive and painful when you have to build it yourself since no vendor fits your requirements or you are not allowed to
|
||||
|
||||
To understand why, we need to look at the landscape as it is today:
|
||||
|
||||
- **Commodity**: Signing SaaS
|
||||
- **Private Goods**: Signing Code Base, Regulatory Know-How
|
||||
- **Public Goods**: Web Tech, Digital Signature Algorithms and Standards
|
||||
|
||||
What the current players have done is to commodify the listed public goods into commercial products:
|
||||
|
||||
> […]the action and process of transforming goods, services, ideas, nature, personal information, people, or animals into commodities.
|
||||
> (Let's ignore the end of that list for now and what it says about humanity, yikes)
|
||||
|
||||
While this paradigm brought digital signing to many businesses worldwide, we aim for a different future. To solve signing once and for all, we need to achieve two core points:
|
||||
|
||||
- Making it cheaper so it's profitable for everyone to use
|
||||
- Making it more accessible so everyone can use it (e.g. regulated industries) and flexible enough (extendable, open).
|
||||
|
||||
To achieve this, we must transform the landscape to look like this:
|
||||
|
||||
- **Commodities**: Enterprise Components, Support, Hosting, Self-Host Licenses
|
||||
- **Public Goods**: (no longer private): OS (Open Source) Signing Code Base, OS Regulatory Know-How
|
||||
- **Public Goods**: OS Web Tech, Digital Signature Algorithms and Standards
|
||||
|
||||
## Raising the Bar
|
||||
|
||||
Before creating a commodity, we are raising the bar of what the underlying public good is. Having an open source singing framework you can extend, self-host, and understand makes the resulting solution much more accessible and extendable for everyone. Now for the final feat of making signing cheaper:
|
||||
|
||||
As we have seen, signing has already been commodified. But since it was done by a closed source and, frankly, a very opaque industry, no downward price spiral has ensued. By building Documenso open source with an open culture, we can pierce the veil and trigger what the space has been missing for a long time: Commoditization. If you had to read that again, so did I:
|
||||
|
||||
> In business literature, **commoditization** is defined as the process by which goods that have economic value and are distinguishable in terms of attributes (uniqueness or brand) become simple commodities in the eyes of the market or consumers.
|
||||
|
||||
By only selling what creates value for the customer (hosting a highly available service, keeping it compliant, supporting with technical issues and challenges, preparing industry-specific components), we are commoditizing signing since everyone can do it now: The resources enabling it are public goods, aka. open source. A leveled playing field, as described above, is the perfect environment for a community-first, technology-first, and value-first company like Documenso to flourish.
|
||||
|
||||
## Changing the Game
|
||||
|
||||
In this new world, a company needing signing (literally every company) can decide if the ROI (Return on Investment) of building signing themselves is greater than simply paying for the value-added activities they will need anyway. Pricing our offering not on volume but fixed is a nice additional wedge into the market we intend to use here.
|
||||
|
||||
The market dynamic now changes to who can offer the greatest value added to the public goods, driving the price down as this can be done much more efficiently than locking customers into closed source SaaS. Documenso, being a lean company, which we intend to stay with for a long time, will help kickstart this effect. Open Source capital efficiency is real. Our planned enterprise components, hosting support, and partner ecosystem will all leverage this effect.
|
||||
|
||||
We will grow our community around the public good, the open-source repo, and create an ecosystem around the commodities built on top of it (components, hosting, compliance, support). We will solve signing once and for all, and the world will be better for it. Onwards.
|
||||
|
||||
As always, feel free to connect on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments.
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -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.
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Jan 10th Email Provider Security Incident
|
||||
description: On January 10th, 2022, we were notified by our email provider that they had experienced a security incident.
|
||||
authorName: 'Lucas Smith'
|
||||
authorImage: '/blog/blog-author-lucas.png'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-17
|
||||
tags:
|
||||
- Security
|
||||
---
|
||||
|
||||
On January 10th, 2024 we were notified by our email provider that a security incident had occurred. This security incident which had started on January 7th led to a bad actor obtaining access to their database which contains ours and other customer’s data.
|
||||
|
||||
We understand that during this security incident the following has been accessed:
|
||||
|
||||
- Email addresses.
|
||||
- Metadata on emails sent excluding the email body.
|
||||
|
||||
While the incident is unfortunate we are pleased with the remediation and the processes that our email provider has put in place to help avoid this kind of situation in the future. Since the incident, our provider has rectified the issue and has engaged a security company to conduct an exhaustive investigation and to help improve their security posture moving forward.
|
||||
|
||||
We remain steadfast in our commitment to our current email provider, and will not be taking any further action with relation to changing providers.
|
||||
|
||||
We are now working with our legal counsel to ensure that we provide the appropriate notice to all our customers in each jurisdiction. If you have any further questions on this incident please feel free to contact our support team at [support@documenso.com](mailto:support@documenso.com).
|
||||
|
||||
We appreciate your ongoing support in this matter.
|
||||
|
||||
You can read more on the incident on our providers blog post below:
|
||||
[https://resend.com/blog/incident-report-for-january-10-2024](https://resend.com/blog/incident-report-for-january-10-2024)
|
||||
64
apps/marketing/content/blog/launch-week-2-day-1.mdx
Normal 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.
|
||||
76
apps/marketing/content/blog/launch-week-2-day-2.mdx
Normal 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
|
||||
53
apps/marketing/content/blog/launch-week-2-day-3.mdx
Normal 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
|
||||
63
apps/marketing/content/blog/launch-week-2-day-4.mdx
Normal 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
|
||||
61
apps/marketing/content/blog/launch-week-2-day-5.mdx
Normal 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
|
||||
115
apps/marketing/content/blog/linear-gh.mdx
Normal file
@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Moving from Linear to GitHub & LIVE Roadmap 2.0
|
||||
description: We are leaving linear and are going all in on GitHub. Here is how we do it.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-10
|
||||
tags:
|
||||
- GitHub
|
||||
- Backlog
|
||||
- Roadmap
|
||||
---
|
||||
|
||||
# From Linear to GitHub
|
||||
|
||||
> TLDR; We are leaving Linear and are using only GitHub going forward. We no longer communicate feature timelines, only what we are working on and what's next.
|
||||
|
||||
If you follow us, you know we have been in full-on build mode. We are building, the community is building, it's great. Building is our daily business, so we think a lot about improving our approach to doing it.
|
||||
Our most recent approach is to reduce the number of tools and platforms we use. Every tool we use
|
||||
|
||||
- Reduces the average time you spend on the tool
|
||||
- Reduces your focus
|
||||
- Increases mental load to keep all points of interest in mind
|
||||
|
||||
We thought about where we spend the most time, and hardly surprising: it's GitHub. Not only do we spend a lot of time there, but we also WANT to spend a lot of time there because:
|
||||
|
||||
- It's where the community contributes, and we are all about community
|
||||
- It's where we show the world what we are working on
|
||||
|
||||
# The old structure
|
||||
|
||||
So far, we have been using Linear for our Backlog/ Task Management and synced issues we want to showcase or work on with the community via synclinear.com. Not only did we have our development issues there, but since
|
||||
we have our own resident founding designer, we created a proper design backlog to structure our design workflows.
|
||||
|
||||
# The new structure
|
||||
|
||||
We moved everything to GitHub once we realized our focus was already there. This has a few key benefits:
|
||||
|
||||
- Reducing dilution of attention and time: You can hang out on GitHub without risk of missing much
|
||||
- Putting different aspects of Documenso close to each other: Development, Design, Community
|
||||
- Keep long-term, niche, and very abstract issues out of the main repo so we don't get desensitized by large issue numbers
|
||||
|
||||
To achieve this, we created a few GitHub repositories to host issues, with the main repository remaining the central point of interest, especially for the community.
|
||||
|
||||
## 1. Main Repository - Day to day Issues and the shorter-term roadmap (LIVE Roadmap 2.0)
|
||||
|
||||
> [github.com/documenso/documenso](https://github.com/documenso/documenso)
|
||||
|
||||
Apart from the source code of the Documenso app and website, the main repo houses issues raised by the community and issues where we invite the community to participate.
|
||||
With the overhauling of our issue management, we are also updating our progress communication. While the software and product development process is highly complex,
|
||||
we try to give as much insight into what we do as possible. To that end, we went through 3 phases, three being what we do now.
|
||||
|
||||
1. **One extensive roadmap**: Initially we had one roadmap and were (very) slowly checking off boxes there (via a "Roadmap" milestone). While this is easy, it's also pretty imprecise and not practical as the project grows
|
||||
2. **Estimated releases per quarter**: To give better guidance, we tried communicating our goals for the quarter; a pretty big window we thought we could roughly "hit". While the idea of not being too detailed was good, it is tough to estimate when some significant things are done if you do a lot of minor/ other things in parallel,
|
||||
like working with the community and tuning things you go. Hitting time targets is tricky because there may be better things to do than sticking to that time target. This is always much easier to grasp for the people closely involved. The fallacy is to assume the thing you plan for exists in a vacuum.
|
||||
3. Since we do not want to limit ourselves in choosing the most effective course but still give some insight into what's going on and what's coming up, we updated the live roadmap [https://documen.so/live](https://documen.so/live). It now shows what we are currently working on and what we plan on doing next. We do not provide
|
||||
a specific timeline anymore since we couldn't even if we wanted to. Of course, we set our short-term goals based on what's best for the community. We give updates on the issues being worked on as well as possible.
|
||||
|
||||
## 2. Public Backlog - The longer-term roadmap
|
||||
|
||||
> [github.com/documenso/backlog](https://github.com/documenso/backlog)
|
||||
|
||||
The public backlog houses everything we want to build eventually. We do not provide a specific timeline of when that might happen. If we decide against something, it will be removed from the public backlog, as we consider this our long-term vision for Documenso. If you are interested in something on the roadmap, comment on the issue or post on Discord. This helps us gauge interest in specific features.
|
||||
**Issues in the public backlog are not** available to be worked on. For issues to work on, please check the main repository issues. The issues found here are scoped broader since they are not meant for immediate execution but rather give a sense of where Documenso is going and what we consider part of our domain.
|
||||
|
||||
## 3. Internal Backlog
|
||||
|
||||
> github.com/documenso/backlog-internal
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/gh1.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="GitHub: Development Board"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Our internal Kanban for development
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
This serves as the direct replacement for our Linear backlog. Here, we manage issues that are either too small or short-term for inclusion in the long-term roadmap, yet too specialized or fundamental to be integrated into the main repository. Our development Kanban board is implemented using a GitHub project.
|
||||
|
||||
## 4. Internal Design Backlog
|
||||
|
||||
> github.com/documenso/design-internal
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/gh2.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="GitHub: Design Board"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Our internal Kanban for design
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
This is the design equivalent of the internal backlog. The internal design backlog houses our design projects that include the exploration of new features, detailed UI designs, and improving the platform overall.
|
||||
It's similar to the Kanban board for the development backlog.
|
||||
|
||||
## 5. Public Design Repository
|
||||
|
||||
> [github.com/documenso/backlog-design](https://github.com/documenso/design)
|
||||
|
||||
While the internal design backlog also existed in Linear, the public design repository is new. Since designing in the open is tricky, we opted to publish the detailed design artifacts with the corresponding feature instead.
|
||||
We already have design.documenso.com housing our general design system. Here, we will publish the specifics of how we applied this to each feature. We will publish the first artifacts here soon, what may be in the cards can be found on the [LIVE Roadmap](https://documen.so/live).
|
||||
|
||||
Feel free to 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 to help and would love to hear from you :)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -7,6 +7,8 @@ authorRole: 'Co-Founder'
|
||||
date: 2023-07-13
|
||||
tags:
|
||||
- Manifesto
|
||||
- Open Source
|
||||
- Vision
|
||||
---
|
||||
|
||||
<figure>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Announcing Pre-Seed and Open Metrics
|
||||
description: We are exicited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
|
||||
description: We are excited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
|
||||
292
apps/marketing/content/blog/public-api.mdx
Normal 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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
> 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.
|
||||
56
apps/marketing/content/blog/removing-server-actions.mdx
Normal 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.
|
||||
@ -30,7 +30,7 @@ We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania)
|
||||
|
||||
## Documenso Merch Shop
|
||||
|
||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
|
||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contributing to Documenso.
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
|
||||
68
apps/marketing/content/blog/why-i-started-documenso.mdx
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Why I started Documenso
|
||||
description: I started Documenso because I wanted to build a modern tech company in a growing space with a mission bigger than money, I overpaid for a SSL cert 13 years ago, like encryption and want to help make the internet/ world more open.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-02-06
|
||||
tags:
|
||||
- Founders
|
||||
- Mission
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/burgers.jpeg"
|
||||
width="650"
|
||||
height="100"
|
||||
alt="Burgers, drinks on a table between friends."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Not the burger from the story. But it could be as well, the place is pretty generic.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; I started Documenso because I wanted to build a modern tech company in a growing space with a mission bigger than money, I overpaid for a SSL cert 13 years ago, like encryption, and wanted to help make the world/ Internet more open.
|
||||
|
||||
It's hard to pinpoint when I decided to start Documenso. I first uttered the word "Documenso" while sitting in a restaurant with [Felix](https://twitter.com/flxmgdnz), eating a burger and discussing what's next in late 2022. Shortly after, I sat down with a can of caffeine and started building [Documenso 0.9](https://github.com/documenso/documenso/releases/tag/0.9-developer-preview). Starting Documenso is the most deliberate business decision I ever made. It was deliberate from the personal side and deliberate from the business side.
|
||||
|
||||
Looking at the personal side, I've had some time off and was actively looking for my next move. Looking back, I stumbled into my first company. Less so with the second one, but I joined my co-founders and did not develop the core concept myself. While coming up with Documenso, I was deliberately looking for a few things, based on my previous experiences:
|
||||
|
||||
- An entrepreneurial space that was a big enough opportunity
|
||||
- A huge macro trend, lifting everything in it's space
|
||||
- A mode of working that fits my flow (which, luckily for me, is pretty close to the modern startup/ tech scene)
|
||||
- A more significant impact to be made than just earning lots of money (though there is nothing wrong with that)
|
||||
|
||||
Quick shoutout to everyone feeling even a pinch of imposter syndrome while calling themselves a founder. It was after ten years, slightly after starting Documenso, that I started doing it in my head without cringing. So cut yourself some slack. Considering how long I've been doing this, I would have earned the internal title sooner, and so do you. After grappling with my identity for a second, as is customary for founders, my decision to start this journey came quickly.
|
||||
|
||||
Aside from the personal dimension, I had a clear mindset of what I wanted. The criteria I describe below clicked into place one after another, in no particular order. Having experienced no market demand and a very gritty, grindy market, I was looking for something more fundamental. Something basic, infrastructure-like, with a huge demand. A growing market deeply rooted in the ever-increasing digitalization of the world.
|
||||
|
||||
And to be honest, I just always liked digital signature tools. It's a product that is easy enough to comprehend and build but complex and impactful enough to satisfy a hard need. It's a product you can build very product-driven since the market and domain are well understood. So when asked about what's next for me, I literally said, "Digital, um, let's say… signatures". As it turns out, my first gut feeling was spot on, but how spot on I only realized when I started researching the space. An open source document signing company happens to be the perfect intersection of all the criteria and personal preferences I described above; it's pretty amazing, actually:
|
||||
|
||||
- The global signing market is enormous and rapidly growing
|
||||
- To put it bluntly, the signing space is vast and dominated by one outdated player. Outdated in terms of tech, pricing, and ecosystem
|
||||
- The signing space is also ridiculously opaque for a space based on open web tech, open encryption tech, and open signing standards. Even by closed-source standards
|
||||
- We are currently seeing a renaissance for commercial open source startups, combining venture founder financials with open source mechanics
|
||||
- Rebuilding a fundamental infrastructure as open source with a meaningful scale has a profoundly transformative effect on any space
|
||||
- Working in open source requires being open, cooperative, and inclusive. It also requires quite a bit of context jumping, "going with the flow," and empathy
|
||||
- Apart from fixing the signing space, making Documenso successful would be another domino tile toward open source eating the world, which is great for everyone
|
||||
|
||||
Building a company is so complex it can't be planned out. Basing it on great fundamentals and the expected dynamics is the best founders can do, in my humble opinion. After these fundamental decisions, you are (almost) just along for the ride and need to focus on solving the "conventional" problems of starting a company the best you can. With digital signatures hitting so many points of my personal and professional checklist, this already was a great fit. What got me excited at first, though, apart from the perspective of drinking caffeine and coding, was this:
|
||||
|
||||
Roughly 13 years ago, I was launching my first product. We obviously wanted SSL encryption on the product site, so I had to buy an SSL certificate. ~$200ish, two years validity, from VeriSign, I think. Apart from it being ridiculously complicated to get, it bothered me that we had basically paid $200 for what is essentially a long number someone generated. SSL wasn't even that widespread back then because it was mainly considered important for e-commerce, no wonder considering it cost so much. "Why would I encrypt a blog?". Fast forward to today, and everyone can get a free SSL cert courtesy of [Let's Encrypt](https://letsencrypt.org/) and browsers are basically blocking unencrypted sites. Mostly, it is even built into hosting platforms, so you barely even notice as a developer.
|
||||
|
||||
I had forgotten all about that story until I realized this is where signing is today. A global need fulfilled only by a closed ecosystem, not really state-of-the-art companies, leading to, let's call it, steep prices. I had considered Let's Encrypt a pillar of the open internet for so long that I forgot that they weren't always there. One day, someone said, let's make the internet better. Signing is another domain that should have had an open ecosystem for a long time. Another parallel to that story is the fact that the cryptographic certificates you need for document signing are also stuck in the "pre-Let's Encrypt world." Free document signing certificates via "Let's Sign" are now another to-do on the [long-term roadmap](https://documen.so/roadmap) list for the open signing ecosystem. Effecting this change in any way is a huge driver for me.
|
||||
|
||||
Apart from my personal gripes with the corporate certificate industry, I have always found encryption fascinating. It's such a fundamental force in society when you think about it: Secure Communication, Secure Commerce, and even [internet native, open source money (Bitcoin)](https://github.com/bitcoin/bitcoin) were created using a bit of smart math. All these examples are expressions of very fundamental human behaviors that should be enabled and protected by open infrastructures.
|
||||
|
||||
I never told rthis to anyone before, but since starting Documenso, I realized that I underestimated the impact and importance of open source for quite some time. When I was in University, I distantly remember my mindset of "yeah, open source is nice, but the great, commercially successful products used in the real world are built by closed companies (aka Microsoft)" _shudder_ It was never really a conscious thought, but enough that I started learning MS Silverlight before plain Javascript. It was slowly, over time, that I realized that open web standards are superior to closed ones, and even later, I understood the same holds true for all software. Open source fixes something in the economy I find hard to articulate. I did my best in [Commodifying Signing](https://documenso.com/blog/commodifying-signing).
|
||||
|
||||
To wrap this up, Documenso happens to be the perfect storm of market opportunity, my interests, and my passions. Creating a company in which people want to work for the long term while tackling these issues is a critical side quest of Documenso. This is not only about building the next generation of signing tech; it's also about doing our part to normalize open, healthy, efficient working cultures and tackling relevant problems.
|
||||
|
||||
As always, feel free to connect on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions, comments, thoughts or feelings.
|
||||
|
||||
\
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
13
apps/marketing/content/careers.mdx
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Careers at Documenso
|
||||
---
|
||||
|
||||
# Careers at Documenso
|
||||
|
||||
So you love Documenso and all the things that we do and now you want to work with us to unlock the future of open signing?
|
||||
|
||||
---
|
||||
|
||||
## Open Positions
|
||||
|
||||
Unfortunately we have no open positions available at the moment. Our team has grown and so we must grow with it, please check back from time to time as now is not forever and we may be hiring again in the future.
|
||||
@ -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}` },
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { withContentlayer } = require('next-contentlayer');
|
||||
const { withAxiom } = require('next-axiom');
|
||||
|
||||
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
|
||||
|
||||
@ -21,6 +22,7 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
|
||||
const config = {
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign'],
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
@ -94,4 +96,4 @@ const config = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withContentlayer(config);
|
||||
module.exports = withAxiom(withContentlayer(config));
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -19,6 +19,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@openstatus/react": "^0.0.3",
|
||||
"contentlayer": "^0.3.4",
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.279.0",
|
||||
@ -26,6 +27,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-plausible": "^3.10.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
@ -36,7 +38,7 @@
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.11.0",
|
||||
"recharts": "^2.7.2",
|
||||
"sharp": "0.32.5",
|
||||
"sharp": "^0.33.1",
|
||||
"typescript": "5.2.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
2
apps/marketing/process-env.d.ts
vendored
@ -6,8 +6,6 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_DATABASE_URL: string;
|
||||
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||
|
||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||
|
||||
7
apps/marketing/public/.well-known/security.txt
Normal 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
|
||||
BIN
apps/marketing/public/blog/api-implementation.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
apps/marketing/public/blog/api-package.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/marketing/public/blog/blog-author-catalin.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/marketing/public/blog/burgers.jpeg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
apps/marketing/public/blog/docs.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
apps/marketing/public/blog/eu-validate-1.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
apps/marketing/public/blog/eu-validate-2.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
apps/marketing/public/blog/gh1.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
apps/marketing/public/blog/gh2.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
apps/marketing/public/blog/hooks.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
apps/marketing/public/blog/lighthouse.jpeg
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
apps/marketing/public/blog/profile.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
apps/marketing/public/blog/quickfill.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
apps/marketing/public/blog/template.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
56591
apps/marketing/public/pdf.worker.min.js
vendored
@ -5,17 +5,16 @@ 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: `Documenso - ${document.title}` };
|
||||
return { title: document.title };
|
||||
};
|
||||
|
||||
const mdxComponents: MDXComponents = {
|
||||
|
||||
@ -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>
|
||||
),
|
||||
{
|
||||
@ -7,19 +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: `Documenso - ${blogPost.title}`,
|
||||
title: {
|
||||
absolute: `${blogPost.title} - Documenso Blog`,
|
||||
},
|
||||
description: blogPost.description,
|
||||
openGraph: {
|
||||
images: [`${blogPost.href}/opengraph?${searchParams.toString()}`],
|
||||
},
|
||||
twitter: {
|
||||
images: [`${blogPost.href}/opengraph?${searchParams.toString()}`],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -39,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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
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);
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -4,6 +4,8 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
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 +19,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);
|
||||
@ -29,8 +35,9 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('relative max-w-[100vw] pt-20 md:pt-28', {
|
||||
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
|
||||
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', '/pricing'].includes(pathname),
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@ -38,10 +45,27 @@ 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 bg-[#e7f3df] px-4 py-2.5">
|
||||
<div className="text-foreground text-center text-sm font-medium">
|
||||
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>
|
||||
|
||||
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
|
||||
<div className="relative max-w-screen-xl flex-1 px-4 sm:mx-auto lg:px-8">{children}</div>
|
||||
|
||||
<Footer className="bg-background border-muted mt-24 border-t" />
|
||||
</div>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -47,6 +47,14 @@ export const TEAM_MEMBERS = [
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'October 9th, 2023',
|
||||
},
|
||||
{
|
||||
name: 'Adithya Krishna',
|
||||
role: 'Software Engineer - II',
|
||||
salary: '-',
|
||||
location: 'India',
|
||||
engagement: 'Full-Time',
|
||||
joinDate: 'December 1st, 2023',
|
||||
},
|
||||
];
|
||||
|
||||
export const FUNDING_RAISED = [
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -3,8 +3,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
|
||||
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">Monthly 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%)' }}
|
||||
/>
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth';
|
||||
|
||||
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">Monthly 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%)' }}
|
||||
/>
|
||||
|
||||
@ -1,18 +1,29 @@
|
||||
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',
|
||||
};
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
@ -123,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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
|
||||
@ -29,10 +29,7 @@ export function OpenPageTooltip() {
|
||||
</svg>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
August and earlier: Active subscribers. September and beyond: Numbers of active
|
||||
subscriptions.
|
||||
</p>
|
||||
<p>Active Subscriptions.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
40
apps/marketing/src/app/(marketing)/open/typefully.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { z } from 'zod';
|
||||
@ -5,7 +6,12 @@ import { z } from 'zod';
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
|
||||
import { OSSFriendsContainer } from './container';
|
||||
import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
|
||||
import type { TOSSFriendsSchema } from './schema';
|
||||
import { ZOSSFriendsSchema } from './schema';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'OSS Friends',
|
||||
};
|
||||
|
||||
export default async function OSSFriendsPage() {
|
||||
const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
|
||||
import type { Metadata } from 'next';
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -10,6 +11,11 @@ import { OpenBuildTemplateBento } from '~/components/(marketing)/open-build-temp
|
||||
import { ShareConnectPaidWidgetBento } from '~/components/(marketing)/share-connect-paid-widget-bento';
|
||||
|
||||
export const revalidate = 600;
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Documenso - The Open Source DocuSign Alternative',
|
||||
},
|
||||
};
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
@ -12,6 +11,12 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { PricingTable } from '~/components/(marketing)/pricing-table';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Pricing',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export type PricingPageProps = {
|
||||
searchParams?: {
|
||||
planId?: string;
|
||||
@ -50,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>
|
||||
@ -163,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
|
||||
@ -172,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>{' '}
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
@ -17,15 +18,14 @@ import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add
|
||||
import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
|
||||
import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerHeader,
|
||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type SinglePlayerModeStep = 'fields' | 'sign';
|
||||
const SinglePlayerModeSteps = ['fields', 'sign'] as const;
|
||||
type SinglePlayerModeStep = (typeof SinglePlayerModeSteps)[number];
|
||||
|
||||
// !: This entire file is a hack to get around failed prerendering of
|
||||
// !: the Single Player Mode page. This regression was introduced during
|
||||
@ -86,7 +86,9 @@ export const SinglePlayerClient = () => {
|
||||
setFields(
|
||||
data.fields.map((field, i) => ({
|
||||
id: i,
|
||||
secondaryId: i.toString(),
|
||||
documentId: -1,
|
||||
templateId: null,
|
||||
recipientId: -1,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
@ -149,6 +151,7 @@ export const SinglePlayerClient = () => {
|
||||
const placeholderRecipient: Recipient = {
|
||||
id: -1,
|
||||
documentId: -1,
|
||||
templateId: null,
|
||||
email: '',
|
||||
name: '',
|
||||
token: '',
|
||||
@ -157,6 +160,8 @@ export const SinglePlayerClient = () => {
|
||||
readStatus: 'OPENED',
|
||||
signingStatus: 'NOT_SIGNED',
|
||||
sendStatus: 'NOT_SENT',
|
||||
role: 'SIGNER',
|
||||
authOptions: null,
|
||||
};
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
@ -187,7 +192,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"
|
||||
>
|
||||
@ -199,7 +204,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>
|
||||
@ -226,37 +231,37 @@ export const SinglePlayerClient = () => {
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={currentDocumentFlow.title}
|
||||
description={currentDocumentFlow.description}
|
||||
/>
|
||||
|
||||
{/* Add fields to PDF page. */}
|
||||
{step === 'fields' && (
|
||||
<DocumentFlowFormContainer
|
||||
className="top-24 lg:h-[calc(100vh-7rem)]"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<Stepper
|
||||
currentStep={currentDocumentFlow.stepIndex}
|
||||
setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
|
||||
>
|
||||
{/* Add fields to PDF page. */}
|
||||
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
||||
<AddFieldsFormPartial
|
||||
documentFlow={documentFlow.fields}
|
||||
hideRecipients={true}
|
||||
recipients={uploadedFile ? [placeholderRecipient] : []}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
fields={fields}
|
||||
onSubmit={onFieldsSubmit}
|
||||
isDocumentPdfLoaded={true}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{/* Enter user details and signature. */}
|
||||
{step === 'sign' && (
|
||||
{/* Enter user details and signature. */}
|
||||
|
||||
<AddSignatureFormPartial
|
||||
documentFlow={documentFlow.sign}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
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>
|
||||
</DocumentFlowFormContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { SinglePlayerClient } from './client';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Singleplayer',
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@ -2,7 +2,11 @@ import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
|
||||
import { AxiomWebVitals } from 'next-axiom';
|
||||
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,29 +21,35 @@ import './globals.css';
|
||||
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
|
||||
|
||||
export const metadata = {
|
||||
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.',
|
||||
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();
|
||||
@ -55,8 +65,11 @@ 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>
|
||||
|
||||
<AxiomWebVitals />
|
||||
|
||||
<Suspense>
|
||||
<PostHogPageview />
|
||||
</Suspense>
|
||||
|
||||
31
apps/marketing/src/components/(marketing)/call-to-action.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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')}
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Info } from 'lucide-react';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { claimPlan } from '~/api/claim-plan/fetcher';
|
||||
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
|
||||
export const ZClaimPlanDialogFormSchema = z.object({
|
||||
name: z.string().trim().min(3, { message: 'Please enter a valid name.' }),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export type TClaimPlanDialogFormSchema = z.infer<typeof ZClaimPlanDialogFormSchema>;
|
||||
|
||||
export type ClaimPlanDialogProps = {
|
||||
className?: string;
|
||||
planId: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
|
||||
const params = useSearchParams();
|
||||
const analytics = useAnalytics();
|
||||
const event = usePlausible();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
} = useForm<TClaimPlanDialogFormSchema>({
|
||||
defaultValues: {
|
||||
name: params?.get('name') ?? '',
|
||||
email: params?.get('email') ?? '',
|
||||
},
|
||||
resolver: zodResolver(ZClaimPlanDialogFormSchema),
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
|
||||
try {
|
||||
const delay = new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const [redirectUrl] = await Promise.all([
|
||||
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
|
||||
delay,
|
||||
]);
|
||||
|
||||
event('claim-plan-pricing');
|
||||
analytics.capture('Marketing: Claim plan', { planId, email });
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
} catch (error) {
|
||||
event('claim-plan-failed');
|
||||
analytics.capture('Marketing: Claim plan failure', { planId, email });
|
||||
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: error instanceof Error ? error.message : 'Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSubmitting && !open) {
|
||||
reset();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Claim your plan</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
We're almost there! Please enter your email address and name to claim your plan.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting} className={cn('flex flex-col gap-y-4', className)}>
|
||||
{params?.get('cancelled') === 'true' && (
|
||||
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-yellow-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm leading-5 text-yellow-700">
|
||||
You have cancelled the payment process. If you didn't mean to do this, please
|
||||
try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-muted-foreground">Name</Label>
|
||||
|
||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
||||
|
||||
<FormErrorMessage className="mt-1" error={errors.name} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-muted-foreground">Email</Label>
|
||||
|
||||
<Input type="email" className="mt-2" {...register('email')} />
|
||||
|
||||
<FormErrorMessage className="mt-1" error={errors.email} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="lg" loading={isSubmitting}>
|
||||
Claim the early adopters Plan (
|
||||
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
? 'Monthly'
|
||||
: 'Yearly'}
|
||||
)
|
||||
</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import LogoImage from '@documenso/assets/logo.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||
|
||||
import { StatusWidgetContainer } from './status-widget-container';
|
||||
|
||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const SOCIAL_LINKS = [
|
||||
@ -31,6 +33,7 @@ const FOOTER_LINKS = [
|
||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
||||
{ href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
|
||||
{ href: '/oss-friends', text: 'OSS Friends' },
|
||||
{ href: '/careers', text: 'Careers' },
|
||||
{ href: '/privacy', text: 'Privacy' },
|
||||
];
|
||||
|
||||
@ -38,7 +41,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
return (
|
||||
<div className={cn('border-t py-12', className)} {...props}>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
|
||||
<div>
|
||||
<div className="flex-shrink-0">
|
||||
<Link href="/">
|
||||
<Image
|
||||
src={LogoImage}
|
||||
@ -61,15 +64,19 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<StatusWidgetContainer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid max-w-xs flex-1 grid-cols-2 gap-x-4 gap-y-2">
|
||||
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
|
||||
{FOOTER_LINKS.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
target={link.target}
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm"
|
||||
>
|
||||
{link.text}
|
||||
</Link>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||