diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index cef36ee3f..4b3066d20 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -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`, }); }; diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts index 2f07c37dd..76e7b8b19 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts @@ -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`, }); }; diff --git a/apps/web/src/app/(share)/share/[slug]/page.tsx b/apps/web/src/app/(share)/share/[slug]/page.tsx index 51684d384..e890a3b24 100644 --- a/apps/web/src/app/(share)/share/[slug]/page.tsx +++ b/apps/web/src/app/(share)/share/[slug]/page.tsx @@ -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'); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index a81437aee..e41476f34 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -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 - - - - - - {children} - - - + + + + + + + {children} + + + - - - + + + + ); diff --git a/apps/web/src/helpers/get-asset-buffer.ts b/apps/web/src/helpers/get-asset-buffer.ts index 85887071e..fc9a27ba9 100644 --- a/apps/web/src/helpers/get-asset-buffer.ts +++ b/apps/web/src/helpers/get-asset-buffer.ts @@ -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()); }; diff --git a/packages/ee/server-only/limits/client.ts b/packages/ee/server-only/limits/client.ts index 7f48e6856..b49fcda01 100644 --- a/packages/ee/server-only/limits/client.ts +++ b/packages/ee/server-only/limits/client.ts @@ -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: { diff --git a/packages/lib/constants/app.ts b/packages/lib/constants/app.ts index 827fcef0a..dcb2ce070 100644 --- a/packages/lib/constants/app.ts +++ b/packages/lib/constants/app.ts @@ -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'; +}; diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts index e972b47c2..2b2055731 100644 --- a/packages/lib/constants/feature-flags.ts +++ b/packages/lib/constants/feature-flags.ts @@ -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 = { */ 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; diff --git a/packages/lib/constants/pdf.ts b/packages/lib/constants/pdf.ts index eba72ab56..34c61af17 100644 --- a/packages/lib/constants/pdf.ts +++ b/packages/lib/constants/pdf.ts @@ -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`; diff --git a/packages/lib/server-only/auth/send-forgot-password.ts b/packages/lib/server-only/auth/send-forgot-password.ts index e62d5e176..24f19e708 100644 --- a/packages/lib/server-only/auth/send-forgot-password.ts +++ b/packages/lib/server-only/auth/send-forgot-password.ts @@ -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, diff --git a/packages/lib/server-only/auth/send-reset-password.ts b/packages/lib/server-only/auth/send-reset-password.ts index 9479f1a45..670f78877 100644 --- a/packages/lib/server-only/auth/send-reset-password.ts +++ b/packages/lib/server-only/auth/send-reset-password.ts @@ -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, diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts index bb6d06b41..7cceff537 100644 --- a/packages/lib/server-only/document/send-completed-email.ts +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -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({ diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index febe619f0..4a83568d8 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -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, diff --git a/packages/lib/server-only/document/send-pending-email.ts b/packages/lib/server-only/document/send-pending-email.ts index 75861be78..245dff25c 100644 --- a/packages/lib/server-only/document/send-pending-email.ts +++ b/packages/lib/server-only/document/send-pending-email.ts @@ -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, diff --git a/packages/lib/server-only/feature-flags/all.ts b/packages/lib/server-only/feature-flags/all.ts index 40e759221..cca5bcfba 100644 --- a/packages/lib/server-only/feature-flags/all.ts +++ b/packages/lib/server-only/feature-flags/all.ts @@ -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); } } diff --git a/packages/lib/server-only/feature-flags/get.ts b/packages/lib/server-only/feature-flags/get.ts index 36aafc7b7..a79f2075f 100644 --- a/packages/lib/server-only/feature-flags/get.ts +++ b/packages/lib/server-only/feature-flags/get.ts @@ -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); } } diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index 9da0e0bf1..4186e2411 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -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); diff --git a/packages/lib/server-only/pdf/insert-text-in-pdf.ts b/packages/lib/server-only/pdf/insert-text-in-pdf.ts index 248702b6e..727e84d8d 100644 --- a/packages/lib/server-only/pdf/insert-text-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-text-in-pdf.ts @@ -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 { // 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); diff --git a/packages/lib/types/pick-starts-with.ts b/packages/lib/types/pick-starts-with.ts new file mode 100644 index 000000000..529c0aa7b --- /dev/null +++ b/packages/lib/types/pick-starts-with.ts @@ -0,0 +1,3 @@ +export type PickStartsWith = { + [K in keyof T as K extends `${S}${string}` ? K : never]: T[K]; +}; diff --git a/packages/lib/universal/get-feature-flag.ts b/packages/lib/universal/get-feature-flag.ts index 38707d41b..940d7637e 100644 --- a/packages/lib/universal/get-feature-flag.ts +++ b/packages/lib/universal/get-feature-flag.ts @@ -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(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 {children}; +}; diff --git a/packages/lib/universal/runtime-env/get-runtime-env.ts b/packages/lib/universal/runtime-env/get-runtime-env.ts new file mode 100644 index 000000000..2a44a4bb8 --- /dev/null +++ b/packages/lib/universal/runtime-env/get-runtime-env.ts @@ -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'); +}; diff --git a/packages/lib/universal/runtime-env/index.ts b/packages/lib/universal/runtime-env/index.ts new file mode 100644 index 000000000..462077480 --- /dev/null +++ b/packages/lib/universal/runtime-env/index.ts @@ -0,0 +1 @@ +export { RuntimeEnvProvider, type RuntimeEnvProviderProps } from './server'; diff --git a/packages/lib/universal/runtime-env/server.tsx b/packages/lib/universal/runtime-env/server.tsx new file mode 100644 index 000000000..651cbc6b5 --- /dev/null +++ b/packages/lib/universal/runtime-env/server.tsx @@ -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 ( + + {children} + +