Compare commits

...

12 Commits

Author SHA1 Message Date
Catalin Pit
b6fbd84027 chore: allow 1 team for free for platform plan 2025-03-04 12:03:49 +02:00
Catalin Pit
fabb2fc3d2 Merge branch 'feat/rr7' into feat/limit-free-teams-platform-plan 2025-03-04 09:43:44 +02:00
David Nguyen
083a706373 fix: duplex and 2fa refresh 2025-03-04 11:41:38 +11:00
David Nguyen
db326cb4a9 fix: posthog reverse proxy 2025-03-04 10:48:19 +11:00
David Nguyen
d664f571d6 fix: posthog reverse proxy 2025-03-04 10:46:59 +11:00
David Nguyen
7c38970ee8 fix: update error logging 2025-03-04 01:41:39 +11:00
David Nguyen
e08d62c844 fix: remove invalid prisma zod schemas 2025-03-04 01:20:13 +11:00
David Nguyen
25bb6ffe77 fix: imports 2025-03-03 14:49:28 +11:00
Catalin Pit
e79d762710 chore: add label for checkbox and radio fields (#1607) 2025-03-03 13:46:29 +11:00
Mythie
d970976299 fix: remove auto-expand in embeddding 2025-02-28 14:46:15 +11:00
David Nguyen
3dce814ab2 fix: stripe price fetch (#1677)
Currently Stripe prices search is omitting a price for an unknown
reason.

Changed our fetch logic to use `list` instead of `search` allows us to
work around the issue.

It's unknown on the performance impact of using `list` vs `search`
2025-02-28 14:44:06 +11:00
Catalin Pit
2d4b18b1e8 feat: limit free teams to 1 for platform plan 2025-02-25 15:34:15 +02:00
58 changed files with 251 additions and 93 deletions

View File

@@ -1,7 +1,7 @@
import { DocumentStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely

View File

@@ -347,7 +347,7 @@ export const EmbedDirectTemplateClientPage = ({
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">

View File

@@ -287,7 +287,7 @@ export const EmbedSignDocumentClientPage = ({
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">

View File

@@ -6,10 +6,10 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { flushSync } from 'react-dom';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@@ -42,7 +42,7 @@ export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
export const DisableAuthenticatorAppDialog = () => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const { refreshSession } = useSession();
const [isOpen, setIsOpen] = useState(false);
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
@@ -92,7 +92,7 @@ export const DisableAuthenticatorAppDialog = () => {
onCloseTwoFactorDisableDialog();
});
await revalidate();
await refreshSession();
} catch (_err) {
toast({
title: _(msg`Unable to disable two-factor authentication`),

View File

@@ -5,12 +5,12 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { renderSVG } from 'uqr';
import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { downloadFile } from '@documenso/lib/client-only/download-file';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@@ -48,7 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const { refreshSession } = useSession();
const [isOpen, setIsOpen] = useState(false);
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
@@ -74,6 +74,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
try {
const data = await authClient.twoFactor.setup();
await refreshSession();
setSetup2FAData(data);
} catch (err) {
@@ -92,6 +93,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
const onEnable2FAFormSubmit = async ({ token }: TEnable2FAForm) => {
try {
const data = await authClient.twoFactor.enable({ code: token });
await refreshSession();
setRecoveryCodes(data.recoveryCodes);
onSuccess?.();
@@ -139,7 +141,6 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null);
void revalidate();
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -181,6 +181,23 @@ export const DocumentSigningFieldContainer = ({
</button>
)}
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-border border': !field.inserted,
},
{
'bg-documenso-200 border-primary border': field.inserted,
},
)}
>
{field.fieldMeta.label}
</div>
)}
{children}
</FieldRootContainer>
</div>

View File

@@ -62,7 +62,7 @@ export const GenericErrorLayout = ({
const team = useOptionalCurrentTeam();
const { subHeading, heading, message } =
errorCodeMap[errorCode || 404] ?? defaultErrorCodeMap[500];
errorCodeMap[errorCode || 500] ?? defaultErrorCodeMap[500];
return (
<div className="fixed inset-0 z-0 flex h-screen w-screen items-center justify-center">

View File

@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import Plausible from 'plausible-tracker';
import posthog from 'posthog-js';
import {
Links,
Meta,
@@ -181,7 +180,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (errorCode !== 404) {
console.error('[RootErrorBoundary]', error);
posthog.captureException(error);
}
return <GenericErrorLayout errorCode={errorCode} />;

View File

@@ -19,18 +19,30 @@ const posthogProxy = async (request: Request) => {
const headers = new Headers(request.headers);
headers.set('host', hostname);
const response = await fetch(newUrl, {
const fetchOptions: RequestInit = {
method: request.method,
headers,
body: request.body,
// @ts-expect-error - Not really sure about this
duplex: 'half',
});
redirect: 'follow',
};
if (!['GET', 'HEAD'].includes(request.method)) {
fetchOptions.body = request.body;
// @ts-expect-error - It should exist
fetchOptions.duplex = 'half';
}
const response = await fetch(newUrl, fetchOptions);
const responseHeaders = new Headers(response.headers);
responseHeaders.delete('content-encoding');
responseHeaders.delete('content-length');
responseHeaders.delete('transfer-encoding');
responseHeaders.delete('cookie');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
headers: responseHeaders,
});
};

View File

@@ -145,7 +145,9 @@ export default function EmbedDirectTemplatePage() {
recipient={recipient}
fields={fields}
metadata={template.templateMeta}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
/>
</DocumentSigningRecipientProvider>

View File

@@ -169,7 +169,9 @@ export default function EmbedSignDocumentPage() {
fields={fields}
metadata={document.documentMeta}
isCompleted={document.status === DocumentStatus.COMPLETED}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
allRecipients={allRecipients}
/>

View File

@@ -1,4 +1,4 @@
import { fetchRequestHandler } from '@ts-rest/serverless/fetch';
import { TsRestHttpError, fetchRequestHandler } from '@ts-rest/serverless/fetch';
import { Hono } from 'hono';
import { ApiContractV1 } from '@documenso/api/v1/contract';
@@ -29,6 +29,12 @@ tsRestHonoApp.mount('/', async (request) => {
request,
contract: ApiContractV1,
router: ApiContractV1Implementation,
options: {},
options: {
errorHandler: (err) => {
if (err instanceof TsRestHttpError && err.statusCode === 500) {
console.error(err);
}
},
},
});
});

View File

@@ -1,3 +1,5 @@
import type { Prisma } from '@prisma/client';
import { DocumentDataType, DocumentStatus, SigningStatus, TeamMemberRole } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
@@ -50,13 +52,6 @@ import {
} from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import {
DocumentDataType,
DocumentStatus,
SigningStatus,
TeamMemberRole,
} from '@documenso/prisma/client';
import { ApiContractV1 } from './contract';
import { authenticatedMiddleware } from './middleware/authenticated';

View File

@@ -1,10 +1,10 @@
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Team, User } from '@documenso/prisma/client';
type B = {
// appRoute: any;

View File

@@ -1,4 +1,16 @@
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
TemplateType,
} from '@prisma/client';
import { z } from 'zod';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@@ -12,18 +24,6 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
TemplateType,
} from '@documenso/prisma/client';
extendZodWithOpenApi(z);

View File

@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import {
ZFindTeamMembersResponseSchema,
@@ -10,7 +11,6 @@ import {
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';

View File

@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import {
createDocumentAuthOptions,
createRecipientAuthOptions,
} from '@documenso/lib/utils/document-auth';
import { FieldType } from '@documenso/prisma/client';
import {
seedPendingDocumentNoFields,
seedPendingDocumentWithFullFields,

View File

@@ -1,16 +1,16 @@
import { expect, test } from '@playwright/test';
import { DateTime } from 'luxon';
import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@documenso/prisma/client';
} from '@prisma/client';
import { DateTime } from 'luxon';
import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,

View File

@@ -1,10 +1,10 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { PDFDocument } from 'pdf-lib';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';

View File

@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -12,7 +12,7 @@ import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test.describe('[EE_ONLY]', () => {
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
const enterprisePriceId = '';
test.beforeEach(() => {
test.skip(

View File

@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -15,7 +15,7 @@ import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
const enterprisePriceId = '';
// Create a temporary PDF file for testing
function createTempPdfFile() {

View File

@@ -1,3 +1,4 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { OAuth2Client, decodeIdToken } from 'arctic';
import type { Context } from 'hono';
import { deleteCookie } from 'hono/cookie';
@@ -6,7 +7,6 @@ import { nanoid } from 'nanoid';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { OAuthClientOptions } from '../../config';
import { AuthenticationErrorCode } from '../errors/error-codes';

View File

@@ -1,5 +1,6 @@
import { sValidator } from '@hono/standard-validator';
import { compare } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { Hono } from 'hono';
import { DateTime } from 'luxon';
import { z } from 'zod';
@@ -22,7 +23,6 @@ import { updatePassword } from '@documenso/lib/server-only/user/update-password'
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
import { getCsrfCookie } from '../lib/session/session-cookies';

View File

@@ -1,8 +1,8 @@
import { DocumentSource, SubscriptionStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { DocumentSource, SubscriptionStatus } from '@documenso/prisma/client';
import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';

View File

@@ -1,7 +1,8 @@
import type { User } from '@prisma/client';
import { STRIPE_CUSTOMER_TYPE } from '@documenso/lib/constants/billing';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import type { User } from '@documenso/prisma/client';
import { onSubscriptionUpdated } from './webhook/on-subscription-updated';

View File

@@ -4,15 +4,14 @@ import { stripe } from '@documenso/lib/server-only/stripe';
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
const planTypes = typeof plan === 'string' ? [plan] : plan;
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
const { data: prices } = await stripe.prices.search({
query,
const prices = await stripe.prices.list({
expand: ['data.product'],
limit: 100,
});
return prices.filter((price) => price.type === 'recurring');
return prices.data.filter(
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
);
};

View File

@@ -1,10 +1,10 @@
import { type Subscription, type Team, type User } from '@prisma/client';
import type Stripe from 'stripe';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import { type Subscription, type Team, type User } from '@documenso/prisma/client';
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
import { getTeamPrices } from './get-team-prices';

View File

@@ -1,6 +1,7 @@
import { SubscriptionStatus } from '@prisma/client';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionDeletedOptions = {
subscription: Stripe.Subscription;

View File

@@ -1,9 +1,9 @@
import type { Prisma } from '@prisma/client';
import { SubscriptionStatus } from '@prisma/client';
import { match } from 'ts-pattern';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionUpdatedOptions = {
userId?: number;

View File

@@ -1,6 +1,7 @@
import type { Subscription } from '@prisma/client';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';

View File

@@ -1,7 +1,8 @@
import type { Subscription } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getEnterprisePlanPriceIds } from '../stripe/get-enterprise-plan-prices';

View File

@@ -1,11 +1,12 @@
import type { Document, Subscription } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Document, Subscription } from '@documenso/prisma/client';
import { getPlatformPlanPriceIds } from '../stripe/get-platform-plan-prices';
export type IsDocumentPlatformOptions = Pick<Document, 'id' | 'userId' | 'teamId'>;
export type IsDocumentPlatformOptions = Pick<Document, 'userId' | 'teamId'>;
/**
* Whether the user is platform, or has permission to use platform features on

View File

@@ -2,10 +2,10 @@ import { useMemo } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { P, match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RecipientRole } from '@documenso/prisma/client';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';

View File

@@ -1,9 +1,9 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RecipientRole } from '@documenso/prisma/client';
import { Body, Button, Container, Head, Html, Img, Preview, Section, Text } from '../components';
import { useBranding } from '../providers/branding';

View File

@@ -1,9 +1,9 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { RecipientRole } from '@prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { RecipientRole } from '@documenso/prisma/client';
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Text } from '../components';
import { useBranding } from '../providers/branding';

View File

@@ -1,12 +1,12 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React from 'react';
import type { Session } from '@prisma/client';
import { useLocation } from 'react-router';
import { authClient } from '@documenso/auth/client';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { type TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { Session } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/client';
export type AppSession = {

View File

@@ -1,6 +1,7 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import type { TeamGlobalSettings } from '@prisma/client';
import { parse } from 'csv-parse/sync';
import { z } from 'zod';
@@ -10,7 +11,6 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { prisma } from '@documenso/prisma';
import type { TeamGlobalSettings } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';

View File

@@ -1,5 +1,6 @@
import { FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
export type GetFieldsForTokenOptions = {
token: string;

View File

@@ -1,5 +1,6 @@
import { TeamMemberRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
export type GetApiTokensOptions = {
userId: number;

View File

@@ -1,5 +1,6 @@
import { FieldType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';

View File

@@ -5,6 +5,7 @@ import { z } from 'zod';
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
import { isDocumentPlatform as isUserPlatformPlan } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
@@ -60,6 +61,11 @@ export const createTeam = async ({
},
});
const isPlatformPlan = await isUserPlatformPlan({
userId: user.id,
teamId: null,
});
let isPaymentRequired = IS_BILLING_ENABLED();
let customerId: string | null = null;
@@ -68,7 +74,25 @@ export const createTeam = async ({
prices.map((price) => price.id),
);
isPaymentRequired = !subscriptionsContainsActivePlan(user.subscriptions, teamRelatedPriceIds);
const hasTeamRelatedSubscription = subscriptionsContainsActivePlan(
user.subscriptions,
teamRelatedPriceIds,
);
if (isPlatformPlan) {
// For platform users, check if they already have any teams
const existingTeams = await prisma.team.findMany({
where: {
ownerUserId: userId,
},
});
// Payment is required if they already have any team
isPaymentRequired = existingTeams.length > 0;
} else {
// For non-platform users, payment is required if they don't have a team-related subscription
isPaymentRequired = !hasTeamRelatedSubscription;
}
customerId = await createTeamCustomer({
name: user.name ?? teamName,

View File

@@ -2804,6 +2804,8 @@ msgid "Field format"
msgstr "Feldformat"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Feldbeschriftung"
@@ -3171,8 +3173,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Tritt {teamName} auf Documenso bei"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Beschriftung"

View File

@@ -2799,6 +2799,8 @@ msgid "Field format"
msgstr "Field format"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Field label"
@@ -3166,8 +3168,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Join {teamName} on Documenso"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Label"

View File

@@ -2804,6 +2804,8 @@ msgid "Field format"
msgstr "Formato de campo"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Etiqueta de campo"
@@ -3171,8 +3173,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Únete a {teamName} en Documenso"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Etiqueta"

View File

@@ -2804,6 +2804,8 @@ msgid "Field format"
msgstr "Format du champ"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Étiquette du champ"
@@ -3171,8 +3173,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Rejoindre {teamName} sur Documenso"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Étiquette"

View File

@@ -2804,6 +2804,8 @@ msgid "Field format"
msgstr "Formato del campo"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Etichetta del campo"
@@ -3171,8 +3173,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Unisci a {teamName} su Documenso"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Etichetta"

View File

@@ -2804,6 +2804,8 @@ msgid "Field format"
msgstr "Format pola"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Field label"
msgstr "Etykieta pola"
@@ -3171,8 +3173,10 @@ msgid "Join {teamName} on Documenso"
msgstr "Dołącz do {teamName} na Documenso"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/radio-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
msgid "Label"
msgstr "Etykieta"

View File

@@ -11,10 +11,11 @@ generator json {
}
generator zod {
provider = "zod-prisma-types"
createInputTypes = false
writeBarrelFiles = false
useMultipleFiles = true
provider = "zod-prisma-types"
createInputTypes = false
writeBarrelFiles = false
useMultipleFiles = true
useDefaultValidators = false
}
datasource db {

View File

@@ -1,4 +1,4 @@
import type { Document, DocumentData, Recipient } from '@documenso/prisma/client';
import type { Document, DocumentData, Recipient } from '@prisma/client';
export type DocumentWithRecipients = Document & {
recipients: Recipient[];

View File

@@ -1,5 +1,6 @@
import type { Field, Signature } from '@prisma/client';
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
import type { Field, Signature } from '@documenso/prisma/client';
export type FieldWithSignatureAndFieldMeta = Field & {
signature?: Signature | null;

View File

@@ -1,4 +1,4 @@
import type { Field, Signature } from '@documenso/prisma/client';
import type { Field, Signature } from '@prisma/client';
export type FieldWithSignature = Field & {
signature?: Signature | null;

View File

@@ -1,4 +1,4 @@
import type { Field, Recipient } from '@documenso/prisma/client';
import type { Field, Recipient } from '@prisma/client';
export type RecipientWithFields = Recipient & {
fields: Field[];

View File

@@ -1,3 +1,4 @@
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import { z } from 'zod';
@@ -5,7 +6,6 @@ import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Session } from '@documenso/prisma/client';
type CreateTrpcContextOptions = {
c: Context;

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
@@ -174,11 +175,35 @@ export const FieldItem = ({
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
[field.fieldMeta],
);
const radioHasValues = useMemo(
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
[field.fieldMeta],
);
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
return false;
}
if (type === FieldType.RADIO) {
const parsed = ZRadioFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
if (type === FieldType.CHECKBOX) {
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
return false;
};
const fieldHasCheckedValues = useMemo(
() => hasCheckedValues(field.fieldMeta, field.type),
[field.fieldMeta, field.type],
);
const fixedSize = checkBoxHasValues || radioHasValues;
return createPortal(
@@ -218,6 +243,21 @@ export const FieldItem = ({
onMove?.(d.node);
}}
>
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
},
)}
>
{field.fieldMeta.label}
</div>
)}
<div
className={cn(
'relative flex h-full w-full items-center justify-center bg-white',

View File

@@ -125,6 +125,18 @@ export const CheckboxFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div className="mb-2">
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-x-4">
<div className="flex w-2/3 flex-col">
<Label>

View File

@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
@@ -25,6 +27,8 @@ export const RadioFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: RadioFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
@@ -100,6 +104,18 @@ export const RadioFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div>
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
className="bg-background"