This commit is contained in:
David Nguyen
2025-02-03 14:10:28 +11:00
parent 28fb35327d
commit b2af10173a
141 changed files with 7340 additions and 394 deletions

View File

@ -9,6 +9,7 @@ import 'swagger-ui-react/swagger-ui.css';
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
export const OpenApiDocsPage = () => {
// Todo
const { resolvedTheme } = useTheme();
useEffect(() => {

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
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 { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
@ -22,15 +22,18 @@ test.describe('Document API', () => {
});
// Test with sendCompletionEmails: false
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
const response = await request.post(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendCompletionEmails: false,
},
},
data: {
sendCompletionEmails: false,
},
});
);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
@ -48,7 +51,7 @@ test.describe('Document API', () => {
// Test with sendCompletionEmails: true
const response2 = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
@ -110,15 +113,18 @@ test.describe('Document API', () => {
expiresIn: null,
});
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
const response = await request.post(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendEmail: true,
},
},
data: {
sendEmail: true,
},
});
);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);

View File

@ -7,7 +7,7 @@ import {
ZSuccessfulUpdateTeamMemberResponseSchema,
ZUnsuccessfulResponseSchema,
} from '@documenso/api/v1/schema';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
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';
@ -32,11 +32,14 @@ test.describe('Team API', () => {
expiresIn: null,
});
const response = await request.get(`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members`, {
headers: {
Authorization: `Bearer ${token}`,
const response = await request.get(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
});
);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
@ -74,7 +77,7 @@ test.describe('Team API', () => {
const newUser = await seedUser();
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/invite`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/invite`,
{
headers: {
Authorization: `Bearer ${token}`,
@ -126,7 +129,7 @@ test.describe('Team API', () => {
expect(member).toBeTruthy();
const response = await request.put(
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
@ -171,7 +174,7 @@ test.describe('Team API', () => {
expect(member).toBeTruthy();
const response = await request.delete(
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
@ -221,7 +224,7 @@ test.describe('Team API', () => {
expect(ownerMember).toBeTruthy();
const response = await request.delete(
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${ownerMember.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${ownerMember.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
@ -261,7 +264,7 @@ test.describe('Team API', () => {
});
const response = await request.delete(
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/team/${team.id}/members/${member.id}`,
{
headers: {
Authorization: `Bearer ${token}`,

View File

@ -1,6 +1,6 @@
import { type Page } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
type LoginOptions = {
page: Page;
@ -23,7 +23,7 @@ export const apiSignin = async ({
const csrfToken = await getCsrfToken(page);
await request.post(`${WEBAPP_BASE_URL}/api/auth/callback/credentials`, {
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/callback/credentials`, {
form: {
email,
password,
@ -32,7 +32,7 @@ export const apiSignin = async ({
},
});
await page.goto(`${WEBAPP_BASE_URL}${redirectPath}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${redirectPath}`);
};
export const apiSignout = async ({ page }: { page: Page }) => {
@ -40,20 +40,20 @@ export const apiSignout = async ({ page }: { page: Page }) => {
const csrfToken = await getCsrfToken(page);
await request.post(`${WEBAPP_BASE_URL}/api/auth/signout`, {
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/signout`, {
form: {
csrfToken,
json: true,
},
});
await page.goto(`${WEBAPP_BASE_URL}/signin`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/signin`);
};
const getCsrfToken = async (page: Page) => {
const { request } = page.context();
const response = await request.fetch(`${WEBAPP_BASE_URL}/api/auth/csrf`, {
const response = await request.fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/csrf`, {
method: 'get',
});

View File

@ -1,6 +1,6 @@
import { test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@ -50,7 +50,7 @@ test('[TEAMS]: delete team', async ({ page }) => {
await page.getByRole('button', { name: 'Delete' }).click();
// Check that we have been redirected to the teams page.
await page.waitForURL(`${WEBAPP_BASE_URL}/settings/teams`);
await page.waitForURL(`${NEXT_PUBLIC_WEBAPP_URL()}/settings/teams`);
});
test('[TEAMS]: update team', async ({ page }) => {
@ -81,5 +81,5 @@ test('[TEAMS]: update team', async ({ page }) => {
await page.getByRole('button', { name: 'Update team' }).click();
// Check we have been redirected to the new team URL and the name is updated.
await page.waitForURL(`${WEBAPP_BASE_URL}/t/${updatedTeamId}/settings`);
await page.waitForURL(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${updatedTeamId}/settings`);
});

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam, seedTeamEmailVerification } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@ -43,7 +43,7 @@ test('[TEAMS]: accept team email request', async ({ page }) => {
teamId: team.id,
});
await page.goto(`${WEBAPP_BASE_URL}/team/verify/email/${teamEmailVerification.token}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/team/verify/email/${teamEmailVerification.token}`);
await expect(page.getByRole('heading')).toContainText('Team email verified!');
});

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam, seedTeamInvite } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@ -49,7 +49,7 @@ test('[TEAMS]: accept team invitation without account', async ({ page }) => {
teamId: team.id,
});
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/team/invite/${teamInvite.token}`);
await expect(page.getByRole('heading')).toContainText('Team invitation');
});
@ -62,7 +62,7 @@ test('[TEAMS]: accept team invitation with account', async ({ page }) => {
teamId: team.id,
});
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/team/invite/${teamInvite.token}`);
await expect(page.getByRole('heading')).toContainText('Invitation accepted!');
});

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam, seedTeamTransfer } from '@documenso/prisma/seed/teams';
import { apiSignin } from '../fixtures/authentication';
@ -60,6 +60,6 @@ test.skip('[TEAMS]: accept team transfer', async ({ page }) => {
newOwnerUserId: newOwnerMember.userId,
});
await page.goto(`${WEBAPP_BASE_URL}/team/verify/transfer/${teamTransferRequest.token}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/team/verify/transfer/${teamTransferRequest.token}`);
await expect(page.getByRole('heading')).toContainText('Team ownership transferred!');
});

View File

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import { customAlphabet } from 'nanoid';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
DIRECT_TEMPLATE_RECIPIENT_NAME,
@ -52,8 +52,8 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) =>
});
const urls = [
`${WEBAPP_BASE_URL}/t/${team.url}/templates/${teamTemplate.id}`,
`${WEBAPP_BASE_URL}/templates/${personalTemplate.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${teamTemplate.id}`,
`${NEXT_PUBLIC_WEBAPP_URL()}/templates/${personalTemplate.id}`,
];
// Run test for personal and team templates.
@ -108,7 +108,7 @@ test('[DIRECT_TEMPLATES]: toggle direct template link', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
// Navigate to template settings and disable access.
await page.goto(`${WEBAPP_BASE_URL}${formatTemplatesPath(template.team?.url)}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatTemplatesPath(template.team?.url)}`);
await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
await page.getByRole('menuitem', { name: 'Direct link' }).click();
await page.getByRole('switch').click();
@ -153,7 +153,7 @@ test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
// Navigate to template settings and delete the access.
await page.goto(`${WEBAPP_BASE_URL}${formatTemplatesPath(template.team?.url)}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatTemplatesPath(template.team?.url)}`);
await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
await page.getByRole('menuitem', { name: 'Direct link' }).click();
await page.getByRole('button', { name: 'Remove' }).click();
@ -241,7 +241,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
// Check that the owner has the documents.
for (const template of [personalDirectTemplate, teamDirectTemplate]) {
await page.goto(`${WEBAPP_BASE_URL}${formatDocumentsPath(template.team?.url)}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(template.team?.url)}`);
await expect(async () => {
// Check that the document is in the 'All' tab.
@ -314,7 +314,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 2 recipients', async ({
// Check that the owner has the documents.
for (const template of [personalDirectTemplate, teamDirectTemplate]) {
await page.goto(`${WEBAPP_BASE_URL}${formatDocumentsPath(template.team?.url)}`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(template.team?.url)}`);
// Check that the document is in the 'All' tab.
await checkDocumentTabCount(page, 'All', 1);

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedTemplate } from '@documenso/prisma/seed/templates';
@ -43,11 +43,11 @@ test('[TEMPLATES]: view templates', async ({ page }) => {
});
// Owner should see both team templates.
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
await expect(page.getByRole('main')).toContainText('Showing 2 results');
// Only should only see their personal template.
await page.goto(`${WEBAPP_BASE_URL}/templates`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/templates`);
await expect(page.getByRole('main')).toContainText('Showing 1 result');
});
@ -92,7 +92,7 @@ test('[TEMPLATES]: delete template', async ({ page }) => {
await expect(page.getByText('Template deleted').first()).toBeVisible();
// Team member should be able to delete all templates.
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
for (const template of ['Team template 1', 'Team template 2']) {
await page
@ -144,7 +144,7 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => {
await expect(page.getByText('Template duplicated').first()).toBeVisible();
await expect(page.getByRole('main')).toContainText('Showing 2 results');
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
// Duplicate team template.
await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
@ -196,7 +196,7 @@ test('[TEMPLATES]: use template', async ({ page }) => {
await page.waitForURL('/documents');
await expect(page.getByRole('main')).toContainText('Showing 1 result');
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
await page.waitForTimeout(1000);
// Use team template.

View File

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { seedUser } from '@documenso/prisma/seed/users';
@ -17,7 +17,7 @@ test('[USER] delete account', async ({ page }) => {
await expect(page.getByRole('button', { name: 'Confirm Deletion' })).not.toBeDisabled();
await page.getByRole('button', { name: 'Confirm Deletion' }).click();
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);
await page.waitForURL(`${NEXT_PUBLIC_WEBAPP_URL()}/signin`);
// Verify that the user no longer exists in the database
await expect(getUserByEmail({ email: user.email })).rejects.toThrow();

View File

@ -8,10 +8,11 @@ import { prisma } from '@documenso/prisma';
export type SessionValidationResult =
| {
session: Session;
user: Pick<
User,
'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' // Todo
>;
user: User;
// user: Pick<
// User,
// 'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' // Todo
// >;
isAuthenticated: true;
}
| { session: null; user: null; isAuthenticated: false };

View File

@ -1,4 +1,4 @@
import { APP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FREE_PLAN_LIMITS } from './constants';
import type { TLimitsResponseSchema } from './schema';
@ -12,7 +12,7 @@ export type GetLimitsOptions = {
export const getLimits = async ({ headers, teamId }: GetLimitsOptions = {}) => {
const requestHeaders = headers ?? {};
const url = new URL('/api/limits', APP_BASE_URL() ?? 'http://localhost:3000');
const url = new URL('/api/limits', NEXT_PUBLIC_WEBAPP_URL());
if (teamId) {
requestHeaders['team-id'] = teamId.toString();

View File

@ -7,7 +7,7 @@ import {
} from '@documenso/lib/constants/feature-flags';
export function useAnalytics() {
const featureFlags = useFeatureFlags();
// const featureFlags = useFeatureFlags();
const isPostHogEnabled = extractPostHogConfig();
/**
@ -30,27 +30,29 @@ export function useAnalytics() {
* @param eventFlag The event to check against feature flags to determine whether tracking is enabled.
*/
const startSessionRecording = (eventFlag?: string) => {
const isSessionRecordingEnabled = featureFlags.getFlag(FEATURE_FLAG_GLOBAL_SESSION_RECORDING);
const isSessionRecordingEnabledForEvent = Boolean(eventFlag && featureFlags.getFlag(eventFlag));
return;
// const isSessionRecordingEnabled = featureFlags.getFlag(FEATURE_FLAG_GLOBAL_SESSION_RECORDING);
// const isSessionRecordingEnabledForEvent = Boolean(eventFlag && featureFlags.getFlag(eventFlag));
if (!isPostHogEnabled || !isSessionRecordingEnabled || !isSessionRecordingEnabledForEvent) {
return;
}
// if (!isPostHogEnabled || !isSessionRecordingEnabled || !isSessionRecordingEnabledForEvent) {
// return;
// }
posthog.startSessionRecording();
// posthog.startSessionRecording();
};
/**
* Stop the current session recording.
*/
const stopSessionRecording = () => {
const isSessionRecordingEnabled = featureFlags.getFlag(FEATURE_FLAG_GLOBAL_SESSION_RECORDING);
return;
// const isSessionRecordingEnabled = featureFlags.getFlag(FEATURE_FLAG_GLOBAL_SESSION_RECORDING);
if (!isPostHogEnabled || !isSessionRecordingEnabled) {
return;
}
// if (!isPostHogEnabled || !isSessionRecordingEnabled) {
// return;
// }
posthog.stopSessionRecording();
// posthog.stopSessionRecording();
};
return {

View File

@ -5,14 +5,15 @@ import type { Session, User } from '@prisma/client';
interface AuthProviderProps {
children: React.ReactNode;
session: Session;
user: User;
session: DocumensoSession | null;
}
const SessionContext = createContext<{
export type DocumensoSession = {
user: User; // Todo: Exclude password
session: Session;
} | null>(null);
};
const SessionContext = createContext<DocumensoSession | null>(null);
export const useSession = () => {
const context = useContext(SessionContext);
@ -24,6 +25,15 @@ export const useSession = () => {
return context;
};
export const SessionProvider = ({ children, session, user }: AuthProviderProps) => {
return <SessionContext.Provider value={{ session, user }}>{children}</SessionContext.Provider>;
export const useOptionalSession = () => {
return (
useContext(SessionContext) || {
user: null,
session: null,
}
);
};
export const SessionProvider = ({ children, session }: AuthProviderProps) => {
return <SessionContext.Provider value={session}>{children}</SessionContext.Provider>;
};

View File

@ -3,21 +3,10 @@ import { env } from '@documenso/lib/utils/env';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
// Todo: env('NEXT_PUBLIC_WEBAPP_URL')
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
export const NEXT_PUBLIC_WEBAPP_URL = () =>
env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
env('NEXT_PRIVATE_INTERNAL_WEBAPP_URL') ?? NEXT_PUBLIC_WEBAPP_URL();
export const IS_APP_MARKETING = env('NEXT_PUBLIC_PROJECT') === 'marketing';
export const IS_APP_WEB = env('NEXT_PUBLIC_PROJECT') === 'web';
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const IS_APP_WEB_I18N_ENABLED = true;
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
export const APP_BASE_URL = () =>
IS_APP_WEB ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PUBLIC_MARKETING_URL();
export const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000';
export const MARKETING_BASE_URL = NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001';

View File

@ -1,6 +1,6 @@
import { env } from '@documenso/lib/utils/env';
import { APP_BASE_URL, WEBAPP_BASE_URL } from './app';
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
const NEXT_PUBLIC_FEATURE_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED');
const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
@ -24,7 +24,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
app_allow_encrypted_documents: false,
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_document_page_view_history_sheet: false,
app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag.
app_passkey: true,
app_public_profile: true,
marketing_header_single_player_mode: false,
marketing_profiles_announcement_bar: true,
@ -35,7 +35,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
*/
export function extractPostHogConfig(): { key: string; host: string } | null {
const postHogKey = NEXT_PUBLIC_POSTHOG_KEY();
const postHogHost = `${APP_BASE_URL()}/ingest`;
const postHogHost = `${NEXT_PUBLIC_WEBAPP_URL()}/ingest`;
if (!postHogKey || !postHogHost) {
return null;

View File

@ -1,4 +1,4 @@
import { APP_BASE_URL } from './app';
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
export const DEFAULT_STANDARD_FONT_SIZE = 12;
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
@ -6,4 +6,4 @@ export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
export const MIN_STANDARD_FONT_SIZE = 8;
export const MIN_HANDWRITING_FONT_SIZE = 20;
export const CAVEAT_FONT_PATH = () => `${APP_BASE_URL()}/fonts/caveat.ttf`;
export const CAVEAT_FONT_PATH = () => `${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`;

View File

@ -8,7 +8,7 @@ import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
@ -60,8 +60,8 @@ export const run = async ({
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: invitedMember.user.name || '',
memberEmail: invitedMember.user.email,
teamName: team.name,

View File

@ -8,7 +8,7 @@ import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
@ -50,8 +50,8 @@ export const run = async ({
for (const member of team.members) {
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: oldMember.name || '',
memberEmail: oldMember.email,
teamName: team.name,

View File

@ -1,6 +1,6 @@
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
import { getTeamPrices } from '@documenso/ee/server-only/stripe/get-team-prices';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
@ -32,7 +32,7 @@ export const createTeamPendingCheckoutSession = async ({
const stripeCheckoutSession = await getCheckoutSession({
customerId: teamPendingCreation.customerId,
priceId,
returnUrl: `${WEBAPP_BASE_URL}/settings/teams`,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/teams`,
subscriptionMetadata: {
pendingTeamId: pendingTeamId.toString(),
},

View File

@ -7,7 +7,7 @@ import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@ -127,7 +127,7 @@ export const sendTeamEmailVerificationEmail = async (
const template = createElement(ConfirmTeamEmailTemplate, {
assetBaseUrl,
baseUrl: WEBAPP_BASE_URL,
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
teamName: team.name,
teamUrl: team.url,
token,

View File

@ -7,7 +7,7 @@ import { nanoid } from 'nanoid';
import { mailer } from '@documenso/email/mailer';
import { TeamInviteEmailTemplate } from '@documenso/email/templates/team-invite';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@ -154,8 +154,8 @@ export const sendTeamMemberInviteEmail = async ({
team,
}: SendTeamMemberInviteEmailOptions) => {
const template = createElement(TeamInviteEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
senderName,
token,
teamName: team.name,

View File

@ -4,7 +4,7 @@ import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma';
@ -74,7 +74,7 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
const template = createElement(TeamEmailRemovedTemplate, {
assetBaseUrl,
baseUrl: WEBAPP_BASE_URL,
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
teamEmail: team.teamEmail?.email ?? '',
teamName: team.name,
teamUrl: team.url,

View File

@ -5,7 +5,7 @@ import type { Team, TeamGlobalSettings } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { AppError } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
@ -96,8 +96,8 @@ type SendTeamDeleteEmailOptions = {
export const sendTeamDeleteEmail = async ({ email, isOwner, team }: SendTeamDeleteEmailOptions) => {
const template = createElement(TeamDeleteEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
teamUrl: team.url,
isOwner,
});

View File

@ -4,7 +4,7 @@ import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma';
@ -89,8 +89,8 @@ export const requestTeamOwnershipTransfer = async ({
});
const template = createElement(TeamTransferRequestTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
senderName: userName,
teamName: team.name,
teamUrl: team.url,

View File

@ -2,7 +2,7 @@ import { z } from 'zod';
import type { TFeatureFlagValue } from '@documenso/lib/client-only/providers/feature-flag.types';
import { ZFeatureFlagValueSchema } from '@documenso/lib/client-only/providers/feature-flag.types';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
/**
@ -23,7 +23,7 @@ export const getFlag = async (
return LOCAL_FEATURE_FLAGS[flag] ?? true;
}
const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`);
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
return await fetch(url, {
@ -58,7 +58,7 @@ export const getAllFlags = async (
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${APP_BASE_URL()}/api/feature-flag/all`);
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
return fetch(url, {
headers: {
@ -86,7 +86,7 @@ export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFla
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${APP_BASE_URL()}/api/feature-flag/all`);
const url = new URL(`${NEXT_PUBLIC_WEBAPP_URL()}/api/feature-flag/all`);
return fetch(url, {
next: {

View File

@ -10,7 +10,7 @@ import path from 'node:path';
import { env } from '@documenso/lib/utils/env';
import { APP_BASE_URL } from '../../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { alphaid } from '../id';
@ -22,7 +22,7 @@ export const getPresignPostUrl = async (fileName: string, contentType: string) =
const token: JWT | null = null;
try {
const baseUrl = APP_BASE_URL() ?? 'http://localhost:3000';
const baseUrl = NEXT_PUBLIC_WEBAPP_URL();
// Todo
// token = await getToken({

View File

@ -1,17 +1,17 @@
import { WEBAPP_BASE_URL } from '../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
import { PASSKEY_TIMEOUT } from '../constants/auth';
/**
* Extracts common fields to identify the RP (relying party)
*/
export const getAuthenticatorOptions = () => {
const webAppBaseUrl = new URL(WEBAPP_BASE_URL);
const webAppBaseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL());
const rpId = webAppBaseUrl.hostname;
return {
rpName: 'Documenso',
rpId,
origin: WEBAPP_BASE_URL,
origin: NEXT_PUBLIC_WEBAPP_URL(),
timeout: PASSKEY_TIMEOUT,
};
};

View File

@ -2,7 +2,7 @@
type EnvironmentVariable = keyof NodeJS.ProcessEnv;
export const env = (variable: EnvironmentVariable | (string & {})) => {
export const env = (variable: EnvironmentVariable | (string & {})): string | undefined => {
// console.log({
// ['typeof window']: typeof window,
// ['process.env']: process.env,

View File

@ -2,7 +2,6 @@ import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension
import type { I18n, MessageDescriptor } from '@lingui/core';
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../constants/app';
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
import { APP_I18N_OPTIONS } from '../constants/i18n';
import { env } from './env';
@ -88,11 +87,6 @@ export const extractLocaleData = ({
lang = langHeader.lang;
}
// Override web app to be English.
if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
lang = 'en';
}
// Filter out locales that are not valid.
const locales = (langHeader?.locales ?? []).filter((locale) => {
try {

View File

@ -1,15 +1,15 @@
import { WEBAPP_BASE_URL } from '../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export const formatUserProfilePath = (
profileUrl: string,
options: { excludeBaseUrl?: boolean } = {},
) => {
return `${!options?.excludeBaseUrl ? WEBAPP_BASE_URL : ''}/p/${profileUrl}`;
return `${!options?.excludeBaseUrl ? NEXT_PUBLIC_WEBAPP_URL() : ''}/p/${profileUrl}`;
};
export const formatTeamProfilePath = (
profileUrl: string,
options: { excludeBaseUrl?: boolean } = {},
) => {
return `${!options?.excludeBaseUrl ? WEBAPP_BASE_URL : ''}/p/${profileUrl}`;
return `${!options?.excludeBaseUrl ? NEXT_PUBLIC_WEBAPP_URL() : ''}/p/${profileUrl}`;
};

View File

@ -1,9 +1,9 @@
import { WEBAPP_BASE_URL } from '../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../constants/teams';
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
const formattedBaseUrl = (baseUrl ?? WEBAPP_BASE_URL).replace(/https?:\/\//, '');
const formattedBaseUrl = (baseUrl ?? NEXT_PUBLIC_WEBAPP_URL()).replace(/https?:\/\//, '');
return `${formattedBaseUrl}/t/${teamUrl}`;
};

View File

@ -1,9 +1,9 @@
import type { Recipient } from '@prisma/client';
import { WEBAPP_BASE_URL } from '../constants/app';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export const formatDirectTemplatePath = (token: string) => {
return `${WEBAPP_BASE_URL}/d/${token}`;
return `${NEXT_PUBLIC_WEBAPP_URL()}/d/${token}`;
};
/**

View File

@ -1,4 +1,4 @@
import Link from 'next/link';
import { Link } from 'react-router';
import { Button } from '../primitives/button';
import { Card, CardContent } from '../primitives/card';
@ -25,7 +25,7 @@ export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToAct
size="lg"
asChild
>
<Link href={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
<Link to={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
Get started
</Link>
</Button>

View File

@ -1,5 +1,3 @@
'use client';
import { useState } from 'react';
import type { DocumentData } from '@prisma/client';
@ -7,17 +5,18 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
import { Dialog, DialogOverlay, DialogPortal, DialogTrigger } from '../../primitives/dialog';
import { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';
export type DocumentDialogProps = {
trigger?: React.ReactNode;
documentData: DocumentData;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
/**
* A dialog which renders the provided document.
*/
export default function DocumentDialog({ documentData, ...props }: DocumentDialogProps) {
export default function DocumentDialog({ trigger, documentData, ...props }: DocumentDialogProps) {
const [documentLoaded, setDocumentLoaded] = useState(false);
const onDocumentLoad = () => {
@ -29,6 +28,12 @@ export default function DocumentDialog({ documentData, ...props }: DocumentDialo
<DialogPortal>
<DialogOverlay className="bg-black/80" />
{trigger && (
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger}
</DialogTrigger>
)}
<DialogPrimitive.Content
className={cn(
'animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 pointer-events-none fixed z-50 h-screen w-screen overflow-y-auto px-2 py-14 opacity-0 transition-opacity lg:py-32',

View File

@ -1,5 +1,3 @@
'use client';
import type { HTMLAttributes } from 'react';
import { useState } from 'react';

View File

@ -1,10 +1,5 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { StaticImageData } from 'next/image';
import Image from 'next/image';
import type { Signature } from '@prisma/client';
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
import { P, match } from 'ts-pattern';
@ -16,7 +11,7 @@ export type SigningCardProps = {
className?: string;
name: string;
signature?: Signature;
signingCelebrationImage?: StaticImageData;
signingCelebrationImage?: string;
};
/**
@ -212,7 +207,7 @@ const SigningCardContent = ({ className, name, signature }: SigningCardContentPr
};
type SigningCardImageProps = {
signingCelebrationImage: StaticImageData;
signingCelebrationImage: string;
};
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
@ -232,7 +227,7 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
duration: 0.5,
}}
>
<Image
<img
src={signingCelebrationImage}
alt="background pattern"
className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
@ -240,7 +235,6 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
}}
priority
/>
</motion.div>
);

View File

@ -1,5 +0,0 @@
export const THEMES_TYPE = {
DARK: 'dark',
LIGHT: 'light',
SYSTEM: 'system'
};

View File

@ -1,22 +1,20 @@
import { motion } from 'framer-motion';
import { Monitor, MoonStar, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Theme, useTheme } from 'remix-themes';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { THEMES_TYPE } from './constants';
export const ThemeSwitcher = () => {
const { theme, setTheme } = useTheme();
const [theme, setTheme] = useTheme();
const isMounted = useIsMounted();
return (
<div className="bg-muted flex items-center gap-x-1 rounded-full p-1">
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.LIGHT)}
onClick={() => setTheme(Theme.LIGHT)}
>
{isMounted && theme === THEMES_TYPE.LIGHT && (
{isMounted && theme === Theme.LIGHT && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-color-burn"
layoutId="selected-theme"
@ -27,9 +25,9 @@ export const ThemeSwitcher = () => {
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.DARK)}
onClick={() => setTheme(Theme.DARK)}
>
{isMounted && theme === THEMES_TYPE.DARK && (
{isMounted && theme === Theme.DARK && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
layoutId="selected-theme"
@ -41,9 +39,9 @@ export const ThemeSwitcher = () => {
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.SYSTEM)}
onClick={() => setTheme(null)}
>
{isMounted && theme === THEMES_TYPE.SYSTEM && (
{isMounted && theme === null && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
layoutId="selected-theme"