Skip to content

Commit

Permalink
fix(react-router): Add E2E tests & improve redirect URL env var loadi…
Browse files Browse the repository at this point in the history
…ng (#4747)
  • Loading branch information
LekoArts authored Dec 10, 2024
1 parent ffa631d commit 4ed3754
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 18 deletions.
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

0 comments on commit 4ed3754

Please sign in to comment.