Compare commits

...

1 Commits

Author SHA1 Message Date
1cd60e1abb feat: runtime env
Support runtime environment variables using server components.

This will mean docker images can change env vars for runtime as required.
2023-11-12 13:10:30 +11:00
29 changed files with 254 additions and 70 deletions

View File

@ -8,10 +8,13 @@ import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-se
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { getRuntimeEnv } from '@documenso/lib/universal/runtime-env/get-runtime-env';
export const createBillingPortal = async () => {
const { user } = await getRequiredServerComponentSession();
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
let stripeCustomer: Stripe.Customer | null = null;
@ -43,6 +46,6 @@ export const createBillingPortal = async () => {
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
};

View File

@ -10,6 +10,7 @@ import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-se
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { getRuntimeEnv } from '@documenso/lib/universal/runtime-env/get-runtime-env';
export type CreateCheckoutOptions = {
priceId: string;
@ -18,6 +19,8 @@ export type CreateCheckoutOptions = {
export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
const { user } = await getRequiredServerComponentSession();
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
let stripeCustomer: Stripe.Customer | null = null;
@ -32,7 +35,7 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
}
@ -53,6 +56,6 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
return getCheckoutSession({
customerId: stripeCustomer.id,
priceId,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
};

View File

@ -2,7 +2,8 @@ import { Metadata } from 'next';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
import { appBaseUrl } from '@documenso/lib/constants/app';
import { getRuntimeEnv } from '@documenso/lib/universal/runtime-env/get-runtime-env';
type SharePageProps = {
params: { slug: string };
@ -16,12 +17,12 @@ export function generateMetadata({ params: { slug } }: SharePageProps) {
title: 'Documenso - Join the open source signing revolution',
description: 'I just signed with Documenso!',
type: 'website',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
images: [`${appBaseUrl()}/share/${slug}/opengraph`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
images: [`${appBaseUrl()}/share/${slug}/opengraph`],
description: 'I just signed with Documenso!',
},
} satisfies Metadata;
@ -30,10 +31,12 @@ export function generateMetadata({ params: { slug } }: SharePageProps) {
export default function SharePage() {
const userAgent = headers().get('User-Agent') ?? '';
const { NEXT_PUBLIC_MARKETING_URL } = getRuntimeEnv();
// https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
return null;
}
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
redirect(NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
}

View File

@ -6,6 +6,8 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { RuntimeEnvProvider } from '@documenso/lib/universal/runtime-env';
import { getRuntimeEnv } from '@documenso/lib/universal/runtime-env/get-runtime-env';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
@ -17,31 +19,39 @@ import { PostHogPageview } from '~/providers/posthog';
import './globals.css';
export const dynamic = 'force-dynamic';
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: {
// We do this so NEXT_PUBLIC variables will be evaluated at runtime.
// eslint-disable-next-line @typescript-eslint/require-await
export const generateMetadata = async () => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
return {
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: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${process.env.NEXT_PUBLIC_WEBAPP_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',
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: [`${NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${NEXT_PUBLIC_WEBAPP_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.',
},
};
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
@ -67,19 +77,21 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</Suspense>
<body>
<LocaleProvider locale={locale}>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<RuntimeEnvProvider>
<LocaleProvider locale={locale}>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
</RuntimeEnvProvider>
</body>
</html>
);

View File

@ -1,3 +1,5 @@
import { getRuntimeEnv } from '@documenso/lib/universal/runtime-env/get-runtime-env';
/**
* getAssetBuffer is used to retrieve array buffers for various assets
* that are hosted in the `public` folder.
@ -8,7 +10,9 @@
* @param path The path to the asset, relative to the `public` folder.
*/
export const getAssetBuffer = async (path: string) => {
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const baseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
return fetch(new URL(path, baseUrl)).then(async (res) => res.arrayBuffer());
};

View File

@ -10,7 +10,7 @@ export type GetLimitsOptions = {
export const getLimits = async ({ headers }: GetLimitsOptions = {}) => {
const requestHeaders = headers ?? {};
const url = new URL(`${APP_BASE_URL}/api/limits`);
const url = new URL('/api/limits', APP_BASE_URL ?? 'http://localhost:3000');
return fetch(url, {
headers: {

View File

@ -1,3 +1,5 @@
import { getRuntimeEnv } from '../universal/runtime-env/get-runtime-env';
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
@ -6,3 +8,21 @@ export const APP_FOLDER = IS_APP_MARKETING ? 'marketing' : 'web';
export const APP_BASE_URL = IS_APP_WEB
? process.env.NEXT_PUBLIC_WEBAPP_URL
: process.env.NEXT_PUBLIC_MARKETING_URL;
export const appBaseUrl = () => {
const { NEXT_PUBLIC_WEBAPP_URL, NEXT_PUBLIC_MARKETING_URL } = getRuntimeEnv();
if (IS_APP_WEB) {
return NEXT_PUBLIC_WEBAPP_URL;
}
if (IS_APP_MARKETING) {
return NEXT_PUBLIC_MARKETING_URL;
}
if (typeof window !== 'undefined') {
return window.location.origin;
}
return NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000';
};

View File

@ -1,4 +1,4 @@
import { APP_BASE_URL } from './app';
import { appBaseUrl } from './app';
/**
* The flag name for global session recording feature flag.
@ -25,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
*/
export function extractPostHogConfig(): { key: string; host: string } | null {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const postHogHost = `${APP_BASE_URL}/ingest`;
const postHogHost = `${appBaseUrl()}/ingest`;
if (!postHogKey || !postHogHost) {
return null;

View File

@ -1,9 +1,7 @@
import { APP_BASE_URL } from './app';
export const DEFAULT_STANDARD_FONT_SIZE = 15;
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 = `/fonts/caveat.ttf`;

View File

@ -5,11 +5,15 @@ import { render } from '@documenso/email/render';
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
import { prisma } from '@documenso/prisma';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
export interface SendForgotPasswordOptions {
userId: number;
}
export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions) => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -29,8 +33,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
}
const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`;
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const resetPasswordLink = `${NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl,

View File

@ -5,18 +5,22 @@ import { render } from '@documenso/email/render';
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
import { prisma } from '@documenso/prisma';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
export interface SendResetPasswordOptions {
userId: number;
}
export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(ResetPasswordTemplate, {
assetBaseUrl,

View File

@ -5,6 +5,7 @@ import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
import { getFile } from '../../universal/upload/get-file';
export interface SendDocumentOptions {
@ -12,6 +13,8 @@ export interface SendDocumentOptions {
}
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const document = await prisma.document.findUnique({
where: {
id: documentId,
@ -36,12 +39,12 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) =>
document.Recipient.map(async (recipient) => {
const { email, name, token } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
});
await mailer.sendMail({

View File

@ -8,12 +8,16 @@ import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-em
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
export type SendDocumentOptions = {
documentId: number;
userId: number;
};
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -59,8 +63,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
return;
}
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,

View File

@ -5,12 +5,16 @@ import { render } from '@documenso/email/render';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
export interface SendPendingEmailOptions {
documentId: number;
recipientId: number;
}
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
const { NEXT_PUBLIC_WEBAPP_URL } = getRuntimeEnv();
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -41,7 +45,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
const { email, name } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentPendingEmailTemplate, {
documentName: document.title,

View File

@ -5,12 +5,15 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
/**
* Get all the evaluated feature flags based on the current user if possible.
*/
export default async function handlerFeatureFlagAll(req: Request) {
const { NEXT_PUBLIC_WEBAPP_URL, NEXT_PUBLIC_MARKETING_URL } = getRuntimeEnv();
const requestHeaders = Object.fromEntries(req.headers.entries());
const nextReq = new NextRequest(req, {
@ -38,11 +41,11 @@ export default async function handlerFeatureFlagAll(req: Request) {
const origin = req.headers.get('origin');
if (origin) {
if (origin.startsWith(process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) {
if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}

View File

@ -6,6 +6,8 @@ import { JWT, getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { getRuntimeEnv } from '../../universal/runtime-env/get-runtime-env';
/**
* Evaluate a single feature flag based on the current user if possible.
*
@ -13,6 +15,8 @@ import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-po
* @returns A Response with the feature flag value.
*/
export default async function handleFeatureFlagGet(req: Request) {
const { NEXT_PUBLIC_WEBAPP_URL, NEXT_PUBLIC_MARKETING_URL } = getRuntimeEnv();
const { searchParams } = new URL(req.url ?? '');
const flag = searchParams.get('flag');
@ -57,11 +61,11 @@ export default async function handleFeatureFlagGet(req: Request) {
const origin = req.headers.get('Origin');
if (origin) {
if (origin.startsWith(process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) {
if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}

View File

@ -12,9 +12,11 @@ import { FieldType } from '@documenso/prisma/client';
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { appBaseUrl } from '../../constants/app';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
// Fetch the font file from the public URL.
const fontResponse = await fetch(CAVEAT_FONT_PATH);
const fontResponse = await fetch(new URL(CAVEAT_FONT_PATH, appBaseUrl()));
const fontCaveat = await fontResponse.arrayBuffer();
const isSignatureField = isSignatureFieldType(field.type);

View File

@ -1,6 +1,7 @@
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { appBaseUrl } from '../../constants/app';
import { CAVEAT_FONT_PATH } from '../../constants/pdf';
export async function insertTextInPDF(
@ -12,7 +13,7 @@ export async function insertTextInPDF(
useHandwritingFont = true,
): Promise<string> {
// Fetch the font file from the public URL.
const fontResponse = await fetch(CAVEAT_FONT_PATH);
const fontResponse = await fetch(new URL(CAVEAT_FONT_PATH, appBaseUrl()));
const fontCaveat = await fontResponse.arrayBuffer();
const pdfDoc = await PDFDocument.load(pdfAsBase64);

View File

@ -0,0 +1,3 @@
export type PickStartsWith<T extends object, S extends string> = {
[K in keyof T as K extends `${S}${string}` ? K : never]: T[K];
};

View File

@ -4,7 +4,7 @@ import {
TFeatureFlagValue,
ZFeatureFlagValueSchema,
} from '@documenso/lib/client-only/providers/feature-flag.types';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
import { appBaseUrl } from '@documenso/lib/constants/app';
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
/**
@ -24,7 +24,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(`${appBaseUrl()}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
const response = await fetch(url, {
@ -57,7 +57,7 @@ export const getAllFlags = async (
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`);
const url = new URL(`${appBaseUrl()}/api/feature-flag/all`);
return fetch(url, {
headers: {
@ -82,7 +82,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(`${appBaseUrl()}/api/feature-flag/all`);
return fetch(url, {
next: {

View File

@ -0,0 +1,26 @@
'use client';
import React, { useContext } from 'react';
import { PublicEnv } from './types';
export type RuntimeEnvClientProviderProps = {
value: PublicEnv;
children: React.ReactNode;
};
const RuntimeEnvContext = React.createContext<PublicEnv | null>(null);
export const useRuntimeEnv = () => {
const context = useContext(RuntimeEnvContext);
if (!context) {
throw new Error('useRuntimeEnv must be used within a RuntimeEnvProvider');
}
return context;
};
export const RuntimeEnvClientProvider = ({ value, children }: RuntimeEnvClientProviderProps) => {
return <RuntimeEnvContext.Provider value={value}>{children}</RuntimeEnvContext.Provider>;
};

View File

@ -0,0 +1,22 @@
import { PublicEnv } from './types';
declare global {
interface Window {
__unstable_runtimeEnv: PublicEnv;
}
}
export const getRuntimeEnv = () => {
if (typeof window === 'undefined') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return Object.entries(process.env)
.filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) as PublicEnv;
}
if (typeof window !== 'undefined' && window.__unstable_runtimeEnv) {
return window.__unstable_runtimeEnv;
}
throw new Error('RuntimeEnv is not available');
};

View File

@ -0,0 +1 @@
export { RuntimeEnvProvider, type RuntimeEnvProviderProps } from './server';

View File

@ -0,0 +1,29 @@
'use server';
import React from 'react';
import { RuntimeEnvClientProvider } from './client';
import { PublicEnv } from './types';
export type RuntimeEnvProviderProps = {
children: React.ReactNode;
};
export const RuntimeEnvProvider = ({ children }: RuntimeEnvProviderProps) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const publicEnv = Object.entries(process.env)
.filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) as PublicEnv;
return (
<RuntimeEnvClientProvider value={publicEnv}>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.__unstable_runtimeEnv = ${JSON.stringify(publicEnv)}`,
}}
/>
</RuntimeEnvClientProvider>
);
};

View File

@ -0,0 +1,3 @@
import { PickStartsWith } from '../../types/pick-starts-with';
export type PublicEnv = PickStartsWith<typeof process.env, 'NEXT_PUBLIC_'>;

View File

@ -4,6 +4,7 @@ import { match } from 'ts-pattern';
import { DocumentDataType } from '@documenso/prisma/client';
import { createDocumentData } from '../../server-only/document-data/create-document-data';
import { getRuntimeEnv } from '../runtime-env/get-runtime-env';
type File = {
name: string;
@ -12,7 +13,9 @@ type File = {
};
export const putFile = async (file: File) => {
const { type, data } = await match(process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT)
const { NEXT_PUBLIC_UPLOAD_TRANSPORT } = getRuntimeEnv();
const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file));

View File

@ -12,6 +12,7 @@ import path from 'node:path';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { getServerComponentSession } from '../../next-auth/get-server-session';
import { alphaid } from '../id';
import { getRuntimeEnv } from '../runtime-env/get-runtime-env';
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
const client = getS3Client();
@ -103,7 +104,9 @@ export const deleteS3File = async (key: string) => {
};
const getS3Client = () => {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
const { NEXT_PUBLIC_UPLOAD_TRANSPORT } = getRuntimeEnv();
if (NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new Error('Invalid upload transport');
}

View File

@ -0,0 +1,20 @@
import { useRuntimeEnv } from './runtime-env/client';
/* eslint-disable turbo/no-undeclared-env-vars */
export const useBaseUrl = () => {
const { NEXT_PUBLIC_WEBAPP_URL } = useRuntimeEnv();
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
if (NEXT_PUBLIC_WEBAPP_URL) {
return NEXT_PUBLIC_WEBAPP_URL;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
};

View File

@ -11,6 +11,7 @@ import {
TOAST_DOCUMENT_SHARE_SUCCESS,
} from '@documenso/lib/constants/toast';
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
import { useRuntimeEnv } from '@documenso/lib/universal/runtime-env/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -37,6 +38,7 @@ export const DocumentShareButton = ({
trigger,
}: DocumentShareButtonProps) => {
const { toast } = useToast();
const { NEXT_PUBLIC_WEBAPP_URL } = useRuntimeEnv();
const { copyShareLink, createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
@ -64,7 +66,7 @@ export const DocumentShareButton = ({
const onCopyClick = async () => {
if (shareLink) {
await copyShareLink(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}`);
await copyShareLink(`${NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}`);
} else {
await createAndCopyShareLink({
token,
@ -88,7 +90,7 @@ export const DocumentShareButton = ({
}
// Ensuring we've prewarmed the opengraph image for the Twitter
await fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${slug}/opengraph`, {
await fetch(`${NEXT_PUBLIC_WEBAPP_URL}/share/${slug}/opengraph`, {
// We don't care about the response, so we can use no-cors
mode: 'no-cors',
});
@ -96,7 +98,7 @@ export const DocumentShareButton = ({
window.open(
generateTwitterIntent(
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${slug}`,
`${NEXT_PUBLIC_WEBAPP_URL}/share/${slug}`,
),
'_blank',
);
@ -141,7 +143,7 @@ export const DocumentShareButton = ({
'animate-pulse': !shareLink?.slug,
})}
>
{process.env.NEXT_PUBLIC_WEBAPP_URL}/share/{shareLink?.slug || '...'}
{NEXT_PUBLIC_WEBAPP_URL}/share/{shareLink?.slug || '...'}
</span>
<div
className={cn('bg-muted/40 mt-4 aspect-video overflow-hidden rounded-lg border', {
@ -150,7 +152,7 @@ export const DocumentShareButton = ({
>
{shareLink?.slug && (
<img
src={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}/opengraph`}
src={`${NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}/opengraph`}
alt="sharing link"
className="h-full w-full object-cover"
/>