Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-news-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/react-router': patch
---

Improve environment variable loading for certain values
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
strategy:
fail-fast: false
matrix:
test-name: [ 'generic', 'express', 'quickstart', 'ap-flows', 'elements', 'sessions', 'astro', 'expo-web', 'tanstack-start', 'tanstack-router', 'vue', 'nuxt']
test-name: [ 'generic', 'express', 'quickstart', 'ap-flows', 'elements', 'sessions', 'astro', 'expo-web', 'tanstack-start', 'tanstack-router', 'vue', 'nuxt', 'react-router']
test-project: ['chrome']
include:
- test-name: 'nextjs'
Expand Down
17 changes: 14 additions & 3 deletions integration/templates/react-router-node/app/routes/protected.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { redirect } from 'react-router';
import { UserProfile } from '@clerk/react-router';
import { getAuth } from '@clerk/react-router/ssr.server';
import { createClerkClient } from '@clerk/react-router/api.server';
import type { Route } from './+types/profile';

export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);

if (!userId) {
return redirect('/sign-in?redirect_url=' + args.request.url);
return redirect('/sign-in');
}

return {};
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(userId);

return {
user,
};
}

export default function Profile(args: Route.ComponentProps) {
export default function Profile({ loaderData }: Route.ComponentProps) {
return (
<div>
<h1>Protected</h1>
<UserProfile />
<ul>
<li>First name: {loaderData.user.firstName}</li>
<li>Email: {loaderData.user.emailAddresses[0].emailAddress}</li>
</ul>
</div>
);
}
41 changes: 40 additions & 1 deletion integration/tests/react-router/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';

import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
Expand Down Expand Up @@ -49,5 +49,44 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:

await u.po.userButton.toHaveVisibleMenuItems([/Manage account/i, /Sign out$/i]);
});

test('redirects to sign-in when unauthenticated', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/protected');
await u.page.waitForURL(`${app.serverUrl}/sign-in`);
await u.po.signIn.waitForMounted();
});

test('renders control components contents', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToAppHome();
await expect(u.page.getByText('SignedOut')).toBeVisible();

await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
await expect(u.page.getByText('SignedIn')).toBeVisible();
});

test('renders user profile with SSR data', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.po.userButton.waitForMounted();
await u.page.goToRelative('/protected');
await u.po.userProfile.waitForMounted();

// Fetched from an API endpoint (/api/me), which is server-rendered.
// This also verifies that the server middleware is working.
await expect(u.page.getByText(`First name: ${fakeUser.firstName}`)).toBeVisible();
await expect(u.page.getByText(`Email: ${fakeUser.email}`)).toBeVisible();
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type ClerkProviderPropsWithState = ReactRouterClerkProviderProps & {
clerkState?: ClerkState;
};

function ClerkProviderBase({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element {
function ClerkProviderBase({ children, ...rest }: ClerkProviderPropsWithState) {
const awaitableNavigate = useAwaitableNavigate();
const isSpaMode = _isSpaMode();

Expand Down
5 changes: 3 additions & 2 deletions packages/react-router/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export type WithClerkState<U = any> = {

export type ReactRouterClerkProviderProps = Without<ClerkProviderProps, 'publishableKey' | 'initialState'> & {
/**
* Used to override the default CLERK_PUBLISHABLE_KEY env variable if needed.
* This is optional for React Router as the ClerkProvider will automatically use the CLERK_PUBLISHABLE_KEY env variable if it exists.
* Used to override the default VITE_CLERK_PUBLISHABLE_KEY env variable if needed.
* This is optional for React Router (in SSR mode) as the ClerkProvider will automatically use the VITE_CLERK_PUBLISHABLE_KEY env variable if it exists.
* If you use React Router in SPA mode or as a library, you have to pass the publishableKey prop.
*/
publishableKey?: string;
children: React.ReactNode;
Expand Down
19 changes: 10 additions & 9 deletions packages/react-router/src/ssr/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO
const { request, context } = args;
const clerkRequest = createClerkRequest(patchRequest(request));

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// Fetch environment variables across React Router runtime.
// 1. First check if the user passed the key in the getAuth() function or the rootAuthLoader().
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
// 3. Then try from import.meta.env if exists (Vite).
// 4. Then try from globalThis (Cloudflare Workers).
// 5. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context);
const publishableKey = overrides.publishableKey || getPublicEnvVariables(context).publishableKey;
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
Expand All @@ -33,13 +34,13 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO
const signInUrl = overrides.signInUrl || getPublicEnvVariables(context).signInUrl;
const signUpUrl = overrides.signUpUrl || getPublicEnvVariables(context).signUpUrl;
const signInForceRedirectUrl =
overrides.signInForceRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL', context) || '';
overrides.signInForceRedirectUrl || getPublicEnvVariables(context).signInForceRedirectUrl;
const signUpForceRedirectUrl =
overrides.signUpForceRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL', context) || '';
overrides.signUpForceRedirectUrl || getPublicEnvVariables(context).signUpForceRedirectUrl;
const signInFallbackRedirectUrl =
overrides.signInFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context) || '';
overrides.signInFallbackRedirectUrl || getPublicEnvVariables(context).signInFallbackRedirectUrl;
const signUpFallbackRedirectUrl =
overrides.signUpFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context) || '';
overrides.signUpFallbackRedirectUrl || getPublicEnvVariables(context).signUpFallbackRedirectUrl;
const afterSignInUrl = overrides.afterSignInUrl || getPublicEnvVariables(context).afterSignInUrl;
const afterSignUpUrl = overrides.afterSignUpUrl || getPublicEnvVariables(context).afterSignUpUrl;

Expand Down
9 changes: 9 additions & 0 deletions packages/react-router/src/ssr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,17 @@ export type RouteInfo = {
export type GetAuthReturn = Promise<AuthObject>;

export type RootAuthLoaderOptions = {
/**
* Used to override the default VITE_CLERK_PUBLISHABLE_KEY env variable if needed.
*/
publishableKey?: string;
/**
* Used to override the CLERK_JWT_KEY env variable if needed.
*/
jwtKey?: string;
/**
* Used to override the CLERK_SECRET_KEY env variable if needed.
*/
secretKey?: string;
/**
* @deprecated This option will be removed in the next major version.
Expand Down
12 changes: 12 additions & 0 deletions packages/react-router/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ export const getPublicEnvVariables = (context: AppLoadContext | undefined) => {
telemetryDebug:
isTruthy(getEnvVariable('VITE_CLERK_TELEMETRY_DEBUG', context)) ||
isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG', context)),
signInForceRedirectUrl:
getEnvVariable('VITE_CLERK_SIGN_IN_FORCE_REDIRECT_URL', context) ||
getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL', context),
signUpForceRedirectUrl:
getEnvVariable('VITE_CLERK_SIGN_UP_FORCE_REDIRECT_URL', context) ||
getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL', context),
signInFallbackRedirectUrl:
getEnvVariable('VITE_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context) ||
getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context),
signUpFallbackRedirectUrl:
getEnvVariable('VITE_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context) ||
getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context),
afterSignInUrl:
getEnvVariable('VITE_CLERK_AFTER_SIGN_IN_URL', context) || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context),
afterSignUpUrl:
Expand Down
1 change: 0 additions & 1 deletion packages/react-router/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const createErrorMessage = (msg: string) => {

For more info, check out the docs: https://clerk.com/docs,
or come say hi in our discord server: https://clerk.com/discord

`;
};

Expand Down
Loading