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
21 changes: 9 additions & 12 deletions app/(auth)/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { auth } from '@clerk/nextjs/server';
import { SignIn } from '@clerk/nextjs';
import dynamic from 'next/dynamic';
import { redirect } from 'next/navigation';

import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import {
DEFAULT_CLERK_AFTER_AUTH_URL,
DEFAULT_CLERK_SIGN_IN_URL,
DEFAULT_CLERK_SIGN_UP_URL,
hasRequiredValidClerkEnv,
canUseClerkClientComponents,
} from '@/lib/clerk-config';
import { resolveSignedInAppDestination } from '@/lib/public-auth-routing';

const ClerkSignInCard = dynamic(
() => import('@/components/auth/clerk-sign-in-card').then((module) => module.ClerkSignInCard),
{ ssr: false }
);

export default async function SignInPage() {
if (!hasRequiredValidClerkEnv()) {
if (!canUseClerkClientComponents()) {
return (
<main className="container grid min-h-screen gap-8 py-16 lg:grid-cols-[0.9fr_1.1fr] lg:items-center">
<section className="space-y-4">
Expand Down Expand Up @@ -60,12 +62,7 @@ export default async function SignInPage() {
</p>
</section>
<div className="flex justify-center lg:justify-end">
<SignIn
path={DEFAULT_CLERK_SIGN_IN_URL}
routing="path"
signUpUrl={DEFAULT_CLERK_SIGN_UP_URL}
fallbackRedirectUrl={DEFAULT_CLERK_AFTER_AUTH_URL}
/>
<ClerkSignInCard />
</div>
</main>
);
Expand Down
21 changes: 9 additions & 12 deletions app/(auth)/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { auth } from '@clerk/nextjs/server';
import { SignUp } from '@clerk/nextjs';
import dynamic from 'next/dynamic';
import { redirect } from 'next/navigation';

import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import {
DEFAULT_CLERK_AFTER_AUTH_URL,
DEFAULT_CLERK_SIGN_IN_URL,
DEFAULT_CLERK_SIGN_UP_URL,
hasRequiredValidClerkEnv,
canUseClerkClientComponents,
} from '@/lib/clerk-config';
import { resolveSignedInAppDestination } from '@/lib/public-auth-routing';

const ClerkSignUpCard = dynamic(
() => import('@/components/auth/clerk-sign-up-card').then((module) => module.ClerkSignUpCard),
{ ssr: false }
);

function getIntentCopy(intent: string | undefined) {
if (intent === 'pilot') {
return {
Expand All @@ -35,7 +37,7 @@ export default async function SignUpPage({
}: {
searchParams?: Record<string, string | string[] | undefined>;
}) {
if (!hasRequiredValidClerkEnv()) {
if (!canUseClerkClientComponents()) {
const intent = typeof searchParams?.intent === 'string' ? searchParams.intent : undefined;
const copy = getIntentCopy(intent);

Expand Down Expand Up @@ -90,12 +92,7 @@ export default async function SignUpPage({
</p>
</section>
<div className="flex justify-center lg:justify-end">
<SignUp
path={DEFAULT_CLERK_SIGN_UP_URL}
routing="path"
signInUrl={DEFAULT_CLERK_SIGN_IN_URL}
fallbackRedirectUrl={DEFAULT_CLERK_AFTER_AUTH_URL}
/>
<ClerkSignUpCard />
</div>
</main>
);
Expand Down
16 changes: 16 additions & 0 deletions components/auth/clerk-sign-in-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { SignIn } from '@clerk/nextjs';

import { DEFAULT_CLERK_AFTER_AUTH_URL, DEFAULT_CLERK_SIGN_IN_URL, DEFAULT_CLERK_SIGN_UP_URL } from '@/lib/clerk-config';

export function ClerkSignInCard() {
return (
<SignIn
path={DEFAULT_CLERK_SIGN_IN_URL}
routing="path"
signUpUrl={DEFAULT_CLERK_SIGN_UP_URL}
fallbackRedirectUrl={DEFAULT_CLERK_AFTER_AUTH_URL}
/>
);
}
16 changes: 16 additions & 0 deletions components/auth/clerk-sign-up-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { SignUp } from '@clerk/nextjs';

import { DEFAULT_CLERK_AFTER_AUTH_URL, DEFAULT_CLERK_SIGN_IN_URL, DEFAULT_CLERK_SIGN_UP_URL } from '@/lib/clerk-config';

export function ClerkSignUpCard() {
return (
<SignUp
path={DEFAULT_CLERK_SIGN_UP_URL}
routing="path"
signInUrl={DEFAULT_CLERK_SIGN_IN_URL}
fallbackRedirectUrl={DEFAULT_CLERK_AFTER_AUTH_URL}
/>
);
}
19 changes: 19 additions & 0 deletions lib/clerk-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,27 @@ export function hasRequiredValidClerkEnv(env: EnvMap = process.env) {
);
}

export function canUseClerkClientComponents(env: EnvMap = process.env) {
const publishableKey = env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY?.trim() ?? '';
if (!publishableKey || !isLikelyValidClerkPublishableKey(publishableKey)) {
return false;
}

// Localhost cannot use the production Clerk frontend API origin, so prefer the
// explicit auth-unavailable fallback instead of mounting broken widgets.
if (env.NODE_ENV !== 'production' && publishableKey.startsWith('pk_live_')) {
return false;
}

return true;
}

export function resolveClerkPublishableKey(env: EnvMap = process.env) {
const configured = env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY?.trim() ?? '';
if (configured && !canUseClerkClientComponents(env)) {
return CLERK_PREVIEW_FALLBACK_KEY;
}

if (configured && isLikelyValidClerkPublishableKey(configured)) {
return configured;
}
Expand Down
30 changes: 30 additions & 0 deletions tests/clerk-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import assert from 'node:assert/strict';
import test from 'node:test';

import {
canUseClerkClientComponents,
DEFAULT_CLERK_SIGN_IN_URL,
DEFAULT_CLERK_SIGN_UP_URL,
getClerkAuthUrls,
getClerkFrontendApiOrigin,
resolveClerkPublishableKey,
} from '../lib/clerk-config.ts';

test('getClerkAuthUrls normalizes env routes to stable base paths for Clerk path routing', () => {
Expand Down Expand Up @@ -36,3 +38,31 @@ test('getClerkFrontendApiOrigin returns null for invalid publishable keys', () =
null
);
});

test('canUseClerkClientComponents disables broken localhost live-key widgets during local development', () => {
assert.equal(
canUseClerkClientComponents({
NODE_ENV: 'development',
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_live_Y2xlcmsuY2FsbGJhY2tjbG9zZXIuY29tJA',
}),
false
);

assert.equal(
canUseClerkClientComponents({
NODE_ENV: 'development',
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_test_Y3VyaW91cy1yaGluby00NS5jbGVyay5hY2NvdW50cy5kZXYk',
}),
true
);
});

test('resolveClerkPublishableKey falls back to the preview key for local development with live Clerk keys', () => {
assert.equal(
resolveClerkPublishableKey({
NODE_ENV: 'development',
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_live_Y2xlcmsuY2FsbGJhY2tjbG9zZXIuY29tJA',
}),
'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k'
);
});
26 changes: 14 additions & 12 deletions tests/public-auth-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,29 @@ test('clerk auth surfaces use explicit path routing and fallback redirects', ()

assert.match(layout, /signInFallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(layout, /signUpFallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(signInPage, /routing="path"/);
assert.match(signInPage, /fallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(signInPage, /<SignIn/);
assert.match(signInPage, /<ClerkSignInCard/);
assert.match(signInPage, /Sign in to your CallbackCloser workspace/);
assert.match(signInPage, /path=\{DEFAULT_CLERK_SIGN_IN_URL\}/);
assert.match(signInPage, /signUpUrl=\{DEFAULT_CLERK_SIGN_UP_URL\}/);
assert.match(signInPage, /hasRequiredValidClerkEnv/);
assert.match(signInPage, /canUseClerkClientComponents/);
assert.match(signInPage, /Authentication is temporarily unavailable\./);
assert.match(signInPage, /resolveSignedInAppDestination/);
assert.match(signUpPage, /routing="path"/);
assert.match(signUpPage, /fallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(signUpPage, /<SignUp/);
assert.match(signUpPage, /<ClerkSignUpCard/);
assert.match(signUpPage, /Create your account and start your 14-day pilot/);
assert.match(signUpPage, /path=\{DEFAULT_CLERK_SIGN_UP_URL\}/);
assert.match(signUpPage, /signInUrl=\{DEFAULT_CLERK_SIGN_IN_URL\}/);
assert.match(signUpPage, /hasRequiredValidClerkEnv/);
assert.match(signUpPage, /canUseClerkClientComponents/);
assert.match(signUpPage, /Authentication is temporarily unavailable\./);
assert.match(signUpPage, /resolveSignedInAppDestination/);
assert.match(signUpPage, /Start 14-day pilot/);
assert.match(signUpPage, /we create your workspace, handle the setup for you/i);
assert.match(signUpPage, /do not need to configure the phone or messaging system yourself/i);
const signInCard = read('components/auth/clerk-sign-in-card.tsx');
const signUpCard = read('components/auth/clerk-sign-up-card.tsx');
assert.match(signInCard, /routing="path"/);
assert.match(signInCard, /fallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(signInCard, /path=\{DEFAULT_CLERK_SIGN_IN_URL\}/);
assert.match(signInCard, /signUpUrl=\{DEFAULT_CLERK_SIGN_UP_URL\}/);
assert.match(signUpCard, /routing="path"/);
assert.match(signUpCard, /fallbackRedirectUrl=\{DEFAULT_CLERK_AFTER_AUTH_URL\}/);
assert.match(signUpCard, /path=\{DEFAULT_CLERK_SIGN_UP_URL\}/);
assert.match(signUpCard, /signInUrl=\{DEFAULT_CLERK_SIGN_IN_URL\}/);
assert.match(pilotEntryPage, /resolvePublicPilotDestination/);
});

Expand Down
Loading