This commit is contained in:
David Nguyen
2025-02-11 02:04:00 +11:00
parent d24f67d922
commit 548d92c2fc
22 changed files with 1260 additions and 1152 deletions

View File

@ -3,11 +3,12 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -55,15 +56,15 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => { const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
try { try {
await resetPassword({ await authClient.emailPassword.resetPassword({
password, password,
token, token,
}); });
await navigate('/signin');
form.reset(); form.reset();
toast({ toast({
@ -71,8 +72,6 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
description: _(msg`Your password has been updated successfully.`), description: _(msg`Your password has been updated successfully.`),
duration: 5000, duration: 5000,
}); });
navigate('/signin');
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);

View File

@ -5,7 +5,7 @@ import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { trpc } from '@documenso/trpc/react'; import { authClient } from '@documenso/auth/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -42,11 +42,9 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => { const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
try { try {
await sendConfirmationEmail({ email }); await authClient.emailPassword.resendVerifyEmail({ email });
toast({ toast({
title: _(msg`Confirmation email sent`), title: _(msg`Confirmation email sent`),
@ -59,6 +57,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
form.reset(); form.reset();
} catch (err) { } catch (err) {
toast({ toast({
variant: 'destructive',
title: _(msg`An error occurred while sending your confirmation email`), title: _(msg`An error occurred while sending your confirmation email`),
description: _(msg`Please try again and make sure you enter the correct email address.`), description: _(msg`Please try again and make sure you enter the correct email address.`),
}); });

View File

@ -17,6 +17,7 @@ import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { SessionProvider } from '@documenso/lib/client-only/providers/session'; import { SessionProvider } from '@documenso/lib/client-only/providers/session';
import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n'; import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { createPublicEnv } from '@documenso/lib/utils/env';
import { extractLocaleData } from '@documenso/lib/utils/i18n'; import { extractLocaleData } from '@documenso/lib/utils/i18n';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
import { Toaster } from '@documenso/ui/primitives/toaster'; import { Toaster } from '@documenso/ui/primitives/toaster';
@ -99,9 +100,7 @@ export async function loader({ request }: Route.LoaderArgs) {
lang, lang,
theme: getTheme(), theme: getTheme(),
session, session,
__ENV__: Object.fromEntries( publicEnv: createPublicEnv(),
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')), // Todo: I'm pretty sure this will leak?
),
}, },
{ {
headers: { headers: {
@ -112,7 +111,7 @@ export async function loader({ request }: Route.LoaderArgs) {
} }
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
const { __ENV__, theme, lang } = useLoaderData<typeof loader>() || {}; const { publicEnv, theme, lang } = useLoaderData<typeof loader>() || {};
// const [theme] = useTheme(); // const [theme] = useTheme();
@ -145,7 +144,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(__ENV__)}`, __html: `window.__ENV__ = ${JSON.stringify(publicEnv)}`,
}} }}
/> />
</body> </body>

View File

@ -11,6 +11,8 @@ import { env } from '@documenso/lib/utils/env';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
import type { Route } from './+types/signin';
export function meta() { export function meta() {
return [{ title: 'Sign In' }]; return [{ title: 'Sign In' }];
} }
@ -18,13 +20,24 @@ export function meta() {
export function loader() { export function loader() {
const session = getOptionalLoaderSession(); const session = getOptionalLoaderSession();
// SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
if (session) { if (session) {
throw redirect('/documents'); throw redirect('/documents');
} }
return {
isGoogleSSOEnabled,
isOIDCSSOEnabled,
oidcProviderLabel,
};
} }
export default function SignIn() { export default function SignIn({ loaderData }: Route.ComponentProps) {
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData;
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">
@ -39,12 +52,12 @@ export default function SignIn() {
<hr className="-mx-6 my-4" /> <hr className="-mx-6 my-4" />
<SignInForm <SignInForm
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} isGoogleSSOEnabled={isGoogleSSOEnabled}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED} isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={OIDC_PROVIDER_LABEL} oidcProviderLabel={oidcProviderLabel}
/> />
{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && ( {env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="text-muted-foreground mt-6 text-center text-sm">
<Trans> <Trans>
Don't have an account?{' '} Don't have an account?{' '}

View File

@ -5,6 +5,8 @@ import { env } from '@documenso/lib/utils/env';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
import type { Route } from './+types/signup';
export function meta() { export function meta() {
return [{ title: 'Sign Up' }]; return [{ title: 'Sign Up' }];
} }
@ -12,17 +14,28 @@ export function meta() {
export function loader() { export function loader() {
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
// SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
throw redirect('/signin'); throw redirect('/signin');
} }
return {
isGoogleSSOEnabled,
isOIDCSSOEnabled,
};
} }
export default function SignUp() { export default function SignUp({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isOIDCSSOEnabled } = loaderData;
return ( return (
<SignUpForm <SignUpForm
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16" className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} isGoogleSSOEnabled={isGoogleSSOEnabled}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED} isOIDCSSOEnabled={isOIDCSSOEnabled}
/> />
); );
} }

View File

@ -3,6 +3,22 @@ import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required'; import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
import { EmbedPaywall } from '~/components/embed/embed-paywall'; import { EmbedPaywall } from '~/components/embed/embed-paywall';
import type { Route } from './+types/_layout';
// Todo: Test
export function headers({ loaderHeaders }: Route.HeadersArgs) {
const origin = loaderHeaders.get('Origin') ?? '*';
// Allow third parties to iframe the document.
return {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Origin': origin,
'Content-Security-Policy': `frame-ancestors ${origin}`,
'Referrer-Policy': 'strict-origin-when-cross-origin',
'X-Content-Type-Options': 'nosniff',
};
}
export default function Layout() { export default function Layout() {
return <Outlet />; return <Outlet />;
} }

View File

@ -6,11 +6,12 @@
"build": "sh .bin/build.sh", "build": "sh .bin/build.sh",
"build:app": "cross-env NODE_ENV=production react-router build", "build:app": "cross-env NODE_ENV=production react-router build",
"build:server": "cross-env NODE_ENV=production rollup -c rollup.config.mjs", "build:server": "cross-env NODE_ENV=production rollup -c rollup.config.mjs",
"dev": "react-router dev", "dev": "npm run with:env -- react-router dev",
"start": "cross-env NODE_ENV=production node build/server/main.js", "start": "npm run with:env -- cross-env NODE_ENV=production node build/server/main.js",
"clean": "rimraf .react-router && rimraf node_modules", "clean": "rimraf .react-router && rimraf node_modules",
"typecheck": "react-router typegen && tsc", "typecheck": "react-router typegen && tsc",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs",
"with:env": "dotenv -e ../../.env -e ../../.env.local --"
}, },
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",

View File

@ -2,21 +2,18 @@ import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite'; import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite'; import serverAdapter from 'hono-react-router-adapter/vite';
import path from 'path';
import tailwindcss from 'tailwindcss'; import tailwindcss from 'tailwindcss';
import { defineConfig, loadEnv } from 'vite'; import { defineConfig } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros'; import macrosPlugin from 'vite-plugin-babel-macros';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
/**
* Note: We load the env variables externally so we can have runtime enviroment variables
* for docker.
*
* Do not configure any envs here.
*/
export default defineConfig({ export default defineConfig({
envDir: path.join(__dirname, '../../'),
envPrefix: '__DO_NOT_USE_OR_YOU_WILL_BE_FIRED__',
define: {
'process.env': {
...process.env,
...loadEnv('development', path.join(__dirname, '../../'), ''),
},
},
css: { css: {
postcss: { postcss: {
plugins: [tailwindcss, autoprefixer], plugins: [tailwindcss, autoprefixer],
@ -37,18 +34,12 @@ export default defineConfig({
], ],
ssr: { ssr: {
noExternal: ['react-dropzone', 'plausible-tracker', 'pdfjs-dist'], noExternal: ['react-dropzone', 'plausible-tracker', 'pdfjs-dist'],
external: ['@node-rs/bcrypt', '@node-rs/bcrypt-wasm32-wasi', '@prisma/client'], external: ['@node-rs/bcrypt', '@prisma/client', '@documenso/tailwind-config'],
}, },
optimizeDeps: { optimizeDeps: {
entries: ['./app/**/*', '../../packages/ui/**/*', '../../packages/lib/**/*'], entries: ['./app/**/*', '../../packages/ui/**/*', '../../packages/lib/**/*'],
include: ['prop-types', 'file-selector', 'attr-accept'], include: ['prop-types', 'file-selector', 'attr-accept'],
exclude: [ exclude: ['node_modules', '@node-rs/bcrypt', '@documenso/pdf-sign', 'sharp'],
'node_modules',
'@node-rs/bcrypt',
'@node-rs/bcrypt-wasm32-wasi',
'@documenso/pdf-sign',
'sharp',
],
}, },
resolve: { resolve: {
alias: { alias: {

View File

@ -9,10 +9,11 @@ const config: LinguiConfig = {
catalogs: [ catalogs: [
{ {
path: '<rootDir>/packages/lib/translations/{locale}/web', path: '<rootDir>/packages/lib/translations/{locale}/web',
include: ['apps/remix/src', 'packages/ui', 'packages/lib', 'packages/email'], include: ['apps/remix/app', 'packages/ui', 'packages/lib', 'packages/email'],
exclude: ['**/node_modules/**'], exclude: ['**/node_modules/**'],
}, },
], ],
compileNamespace: 'es',
}; };
export default config; export default config;

2189
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -80,4 +80,4 @@
"trigger.dev": { "trigger.dev": {
"endpointId": "documenso-app" "endpointId": "documenso-app"
} }
} }

View File

@ -19,7 +19,7 @@
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@ts-rest/core": "^3.30.5", "@ts-rest/core": "^3.30.5",
"@ts-rest/open-api": "^3.33.0", "@ts-rest/open-api": "^3.33.0",
"@ts-rest/serverless": "^3.51.0", "@ts-rest/serverless": "^3.30.5",
"@types/swagger-ui-react": "^4.18.3", "@types/swagger-ui-react": "^4.18.3",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"superjson": "^1.13.1", "superjson": "^1.13.1",

View File

@ -8,6 +8,7 @@ import type { AuthAppType } from '../server';
import { handleSignInRedirect } from '../server/lib/utils/redirect'; import { handleSignInRedirect } from '../server/lib/utils/redirect';
import type { import type {
TForgotPasswordSchema, TForgotPasswordSchema,
TResendVerifyEmailSchema,
TResetPasswordSchema, TResetPasswordSchema,
TSignUpSchema, TSignUpSchema,
TUpdatePasswordSchema, TUpdatePasswordSchema,
@ -79,6 +80,13 @@ export class AuthClient {
await this.handleError(response); await this.handleError(response);
}, },
resendVerifyEmail: async (data: TResendVerifyEmailSchema) => {
const response = await this.client['email-password']['resend-verify-email'].$post({
json: data,
});
await this.handleError(response);
},
verifyEmail: async (data: TVerifyEmailSchema) => { verifyEmail: async (data: TVerifyEmailSchema) => {
const response = await this.client['email-password']['verify-email'].$post({ json: data }); const response = await this.client['email-password']['verify-email'].$post({ json: data });
await this.handleError(response); await this.handleError(response);

View File

@ -30,6 +30,7 @@ import { getRequiredSession, getSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context'; import type { HonoAuthContext } from '../types/context';
import { import {
ZForgotPasswordSchema, ZForgotPasswordSchema,
ZResendVerifyEmailSchema,
ZResetPasswordSchema, ZResetPasswordSchema,
ZSignInSchema, ZSignInSchema,
ZSignUpSchema, ZSignUpSchema,
@ -196,17 +197,17 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/** /**
* Resend verification email endpoint. * Resend verification email endpoint.
*/ */
.post('/resend-email', zValidator('json', ZVerifyEmailSchema), async (c) => { .post('/resend-verify-email', zValidator('json', ZResendVerifyEmailSchema), async (c) => {
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token }); const { email } = c.req.valid('json');
// If email is verified, automatically authenticate user. await jobsClient.triggerJob({
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) { name: 'send.signup.confirmation.email',
await onAuthorize({ userId }, c); payload: {
} email,
},
return c.json({
state,
}); });
return c.text('OK', 201);
}) })
/** /**
* Forgot password endpoint. * Forgot password endpoint.

View File

@ -68,6 +68,12 @@ export const ZVerifyEmailSchema = z.object({
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>; export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
export const ZResendVerifyEmailSchema = z.object({
email: z.string().email().min(1),
});
export type TResendVerifyEmailSchema = z.infer<typeof ZResendVerifyEmailSchema>;
export const ZUpdatePasswordSchema = z.object({ export const ZUpdatePasswordSchema = z.object({
currentPassword: ZCurrentPasswordSchema, currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema, password: ZPasswordSchema,

View File

@ -2,8 +2,8 @@ import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import { ResendTransport } from '@documenso/nodemailer-resend';
// import { ResendTransport } from '@documenso/nodemailer-resend';
import { MailChannelsTransport } from './transports/mailchannels'; import { MailChannelsTransport } from './transports/mailchannels';
/** /**
@ -63,14 +63,13 @@ const getTransport = (): Transporter => {
); );
} }
// Todo if (transport === 'resend') {
// if (transport === 'resend') { return createTransport(
// return createTransport( ResendTransport.makeTransport({
// ResendTransport.makeTransport({ apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '',
// apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '', }),
// }), );
// ); }
// }
if (transport === 'smtp-api') { if (transport === 'smtp-api') {
if (!env('NEXT_PRIVATE_SMTP_HOST') || !env('NEXT_PRIVATE_SMTP_APIKEY')) { if (!env('NEXT_PRIVATE_SMTP_HOST') || !env('NEXT_PRIVATE_SMTP_APIKEY')) {

View File

@ -35,11 +35,9 @@
"@react-email/section": "0.0.10", "@react-email/section": "0.0.10",
"@react-email/tailwind": "0.0.9", "@react-email/tailwind": "0.0.9",
"@react-email/text": "0.0.6", "@react-email/text": "0.0.6",
"@lingui/macro": "^5.2.0", "nodemailer": "6.9.9",
"@lingui/react": "^5.2.0", "react-email": "1.9.5",
"nodemailer": "^6.9.9", "resend": "2.0.0"
"react-email": "^1.9.5",
"resend": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",

View File

@ -1,11 +1,10 @@
import * as ReactEmail from '@react-email/render'; import * as ReactEmail from '@react-email/render';
import config from '@documenso/tailwind-config';
import { Tailwind } from './components'; import { Tailwind } from './components';
import { BrandingProvider, type BrandingSettings } from './providers/branding'; import { BrandingProvider, type BrandingSettings } from './providers/branding';
// Todo:
// import config from '@documenso/tailwind-config';
export type RenderOptions = ReactEmail.Options & { export type RenderOptions = ReactEmail.Options & {
branding?: BrandingSettings; branding?: BrandingSettings;
}; };
@ -18,7 +17,7 @@ export const render = (element: React.ReactNode, options?: RenderOptions) => {
config={{ config={{
theme: { theme: {
extend: { extend: {
// colors: config.theme.extend.colors, colors: config.theme.extend.colors,
}, },
}, },
}} }}
@ -37,7 +36,7 @@ export const renderAsync = async (element: React.ReactNode, options?: RenderOpti
config={{ config={{
theme: { theme: {
extend: { extend: {
// colors: config.theme.extend.colors, colors: config.theme.extend.colors,
}, },
}, },
}} }}

View File

@ -14,7 +14,7 @@ type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export async function loadCatalog(lang: SupportedLanguages): Promise<{ export async function loadCatalog(lang: SupportedLanguages): Promise<{
[k: string]: Messages; [k: string]: Messages;
}> { }> {
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js'; const extension = env('NODE_ENV') === 'development' ? 'po' : 'mjs';
const { messages } = await import(`../../translations/${lang}/web.${extension}`); const { messages } = await import(`../../translations/${lang}/web.${extension}`);

View File

@ -2,3 +2,4 @@
# Compiled translations. # Compiled translations.
*.js *.js
*.mjs

View File

@ -1,18 +1,21 @@
/// <reference types="@documenso/tsconfig/process-env.d.ts" /> /// <reference types="@documenso/tsconfig/process-env.d.ts" />
declare global {
interface Window {
__ENV__?: Record<string, string | undefined>;
}
}
type EnvironmentVariable = keyof NodeJS.ProcessEnv; type EnvironmentVariable = keyof NodeJS.ProcessEnv;
export const env = (variable: EnvironmentVariable | (string & object)): string | undefined => { export const env = (variable: EnvironmentVariable | (string & object)): string | undefined => {
// console.log({
// ['typeof window']: typeof window,
// ['process.env']: process.env,
// ['window.__ENV__']: typeof window !== 'undefined' && window.__ENV__,
// });
// This may need ot be import.meta.env.SSR depending on vite.
if (typeof window !== 'undefined' && typeof window.__ENV__ === 'object') { if (typeof window !== 'undefined' && typeof window.__ENV__ === 'object') {
return window.__ENV__[variable]; return window.__ENV__[variable];
} }
return process.env[variable]; return process?.env?.[variable];
}; };
// Todo: Test
export const createPublicEnv = () =>
Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_PUBLIC_')));

View File

@ -3,13 +3,11 @@ import { i18n } from '@lingui/core';
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n'; import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
import { APP_I18N_OPTIONS } from '../constants/i18n'; import { APP_I18N_OPTIONS } from '../constants/i18n';
import { env } from './env';
export async function dynamicActivate(locale: string) { export async function dynamicActivate(locale: string) {
// const extension = import.meta.env.PROD env('NODE_ENV') === 'development' ? 'po' : 'js'; const extension = env('NODE_ENV') === 'development' ? 'po' : 'mjs';
// eslint-disable-next-line turbo/no-undeclared-env-vars
const extension = import.meta.env.PROD ? 'js' : 'po';
// Todo: Use extension (currently breaks).
const { messages } = await import(`../translations/${locale}/web.${extension}`); const { messages } = await import(`../translations/${locale}/web.${extension}`);
i18n.loadAndActivate({ locale, messages }); i18n.loadAndActivate({ locale, messages });