# Clerk Phase 0 + 0.5 Fix — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Fix the Clerk login/signup pages so they render correctly, match the existing `/login` page design, and are CSP-compliant for production.

**Architecture:** Additive only — restructure catch-all routes, strip nav/footer from auth pages, apply card-wrapper pattern matching existing login, add Clerk domains to CSP, thread nonce into ClerkProvider. Zero changes to `/login`, `/signup`, `lib/auth.ts`, or any Convex infrastructure.

**Tech Stack:** Next.js 14 App Router, `@clerk/nextjs` v5, Tailwind CSS, TypeScript strict mode. Working dir: `mawidi-site/` inside the `worktree-new-login` worktree.

---

## Task 0: Verify Starting State

**Step 1: Confirm working directory and branch**

```bash
git branch --show-current
# Expected: worktree-new-login
ls app/\[lang\]/clerk-login/
# Expected: page.tsx  (no [[...clerk]] subdirectory yet)
ls app/\[lang\]/clerk-signup/
# Expected: page.tsx
```

**Step 2: Confirm TypeScript baseline**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "error TS" | wc -l
# Note this number — we must not introduce new errors
```

---

## Task 1: Catch-All Route — clerk-login

Clerk's `<SignIn>` component requires a catch-all route to handle OAuth callbacks,
email verification links, and multi-step sign-in sub-routes. Without it, those flows
get a 404.

**Files:**

- Delete: `app/[lang]/clerk-login/page.tsx`
- Create: `app/[lang]/clerk-login/[[...clerk]]/page.tsx`

**Step 1: Create the new catch-all page**

Create `app/[lang]/clerk-login/[[...clerk]]/page.tsx` with this exact content:

```tsx
import { SignIn } from "@clerk/nextjs";
import Link from "next/link";
import Image from "next/image";
import { UI, type Lang } from "@/lib/config";

interface ClerkLoginPageProps {
  params: { lang: Lang };
}

export default function ClerkLoginPage({ params }: ClerkLoginPageProps) {
  const { lang } = params;
  const dir = UI[lang].dir;
  const isAr = lang === "ar";

  return (
    <div
      className="min-h-screen flex items-center justify-center bg-slate-50 px-4 py-12"
      dir={dir}
    >
      <div className="w-full max-w-md">
        {/* Logo — identical placement to existing /login page */}
        <div className="text-center mb-6">
          <Link href={`/${lang}`} className="inline-block">
            <Image
              src="/brand/logo-horizontal.jpeg"
              alt="Mawidi - موعدي"
              width={400}
              height={133}
              priority
              className="h-32 w-auto"
            />
          </Link>
        </div>

        {/* Card — identical to existing /login card */}
        <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8">
          <SignIn
            fallbackRedirectUrl={`/${lang}/dashboard`}
            signUpUrl={`/${lang}/clerk-signup`}
            appearance={{
              layout: {
                logoPlacement: "none",
                socialButtonsVariant: "blockButton",
              },
              variables: {
                colorPrimary: "#0F9972",
                colorText: "#1e293b",
                colorTextSecondary: "#64748b",
                colorBackground: "#ffffff",
                colorInputBackground: "#ffffff",
                colorInputText: "#1e293b",
                borderRadius: "0.5rem",
                fontFamily: isAr
                  ? "var(--font-ar), Cairo, system-ui"
                  : "var(--font-en), Inter, system-ui",
              },
              elements: {
                // Strip Clerk's card chrome — we wrap with our own card above
                card: "shadow-none border-0 p-0 w-full",
                // Hide Apple + Facebook — Google only, matching existing login
                socialButtonsBlockButton__apple: "hidden",
                socialButtonsBlockButton__facebook: "hidden",
                // Match existing login field styles
                formButtonPrimary:
                  "!bg-[#0F9972] hover:!bg-[#0D8761] font-medium transition-colors",
                socialButtonsBlockButton:
                  "!border-slate-300 hover:!bg-slate-50 transition-colors",
                formFieldInput:
                  "!border-slate-300 focus:!ring-2 focus:!ring-[#0F9972] focus:!border-[#0F9972]",
                footerActionLink: "!text-[#0F9972] hover:underline",
                identityPreviewEditButton: "!text-[#0F9972]",
                dividerRow: "!text-slate-500",
              },
            }}
          />
        </div>

        {/* Sign up link — matches existing /login page */}
        <p className="text-center text-sm text-slate-600 mt-6">
          {isAr ? "ليس لديك حساب؟" : "Don't have an account?"}{" "}
          <Link
            href={`/${lang}/clerk-signup`}
            className="text-[#0F9972] font-medium hover:underline"
          >
            {isAr ? "سجل الآن" : "Sign up"}
          </Link>
        </p>

        {/* Footer links */}
        <div className="flex items-center justify-center gap-4 text-xs text-slate-500 mt-4">
          <Link href={`/${lang}/privacy`} className="hover:text-[#0F9972]">
            {isAr ? "الخصوصية" : "Privacy"}
          </Link>
          <span>•</span>
          <Link href={`/${lang}/terms`} className="hover:text-[#0F9972]">
            {isAr ? "الشروط" : "Terms"}
          </Link>
        </div>
      </div>
    </div>
  );
}
```

**Step 2: Delete the old page**

```bash
rm app/\[lang\]/clerk-login/page.tsx
```

**Step 3: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "clerk-login"
# Expected: no output (no errors on this file)
```

**Step 4: Commit**

```bash
git add app/\[lang\]/clerk-login/
git commit -m "feat(auth): restructure clerk-login as catch-all route with card-wrapper design"
```

---

## Task 2: Catch-All Route — clerk-signup

Same pattern as Task 1 for the signup page.

**Files:**

- Delete: `app/[lang]/clerk-signup/page.tsx`
- Create: `app/[lang]/clerk-signup/[[...clerk]]/page.tsx`

**Step 1: Create the new catch-all page**

Create `app/[lang]/clerk-signup/[[...clerk]]/page.tsx`:

```tsx
import { SignUp } from "@clerk/nextjs";
import Link from "next/link";
import Image from "next/image";
import { UI, type Lang } from "@/lib/config";

interface ClerkSignupPageProps {
  params: { lang: Lang };
}

export default function ClerkSignupPage({ params }: ClerkSignupPageProps) {
  const { lang } = params;
  const dir = UI[lang].dir;
  const isAr = lang === "ar";

  return (
    <div
      className="min-h-screen flex items-center justify-center bg-slate-50 px-4 py-12"
      dir={dir}
    >
      <div className="w-full max-w-md">
        {/* Logo */}
        <div className="text-center mb-6">
          <Link href={`/${lang}`} className="inline-block">
            <Image
              src="/brand/logo-horizontal.jpeg"
              alt="Mawidi - موعدي"
              width={400}
              height={133}
              priority
              className="h-32 w-auto"
            />
          </Link>
        </div>

        {/* Card */}
        <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8">
          <SignUp
            fallbackRedirectUrl={`/${lang}/dashboard`}
            signInUrl={`/${lang}/clerk-login`}
            appearance={{
              layout: {
                logoPlacement: "none",
                socialButtonsVariant: "blockButton",
              },
              variables: {
                colorPrimary: "#0F9972",
                colorText: "#1e293b",
                colorTextSecondary: "#64748b",
                colorBackground: "#ffffff",
                colorInputBackground: "#ffffff",
                colorInputText: "#1e293b",
                borderRadius: "0.5rem",
                fontFamily: isAr
                  ? "var(--font-ar), Cairo, system-ui"
                  : "var(--font-en), Inter, system-ui",
              },
              elements: {
                card: "shadow-none border-0 p-0 w-full",
                socialButtonsBlockButton__apple: "hidden",
                socialButtonsBlockButton__facebook: "hidden",
                formButtonPrimary:
                  "!bg-[#0F9972] hover:!bg-[#0D8761] font-medium transition-colors",
                socialButtonsBlockButton:
                  "!border-slate-300 hover:!bg-slate-50 transition-colors",
                formFieldInput:
                  "!border-slate-300 focus:!ring-2 focus:!ring-[#0F9972] focus:!border-[#0F9972]",
                footerActionLink: "!text-[#0F9972] hover:underline",
                identityPreviewEditButton: "!text-[#0F9972]",
                dividerRow: "!text-slate-500",
              },
            }}
          />
        </div>

        {/* Sign in link */}
        <p className="text-center text-sm text-slate-600 mt-6">
          {isAr ? "لديك حساب بالفعل؟" : "Already have an account?"}{" "}
          <Link
            href={`/${lang}/clerk-login`}
            className="text-[#0F9972] font-medium hover:underline"
          >
            {isAr ? "سجل الدخول" : "Sign in"}
          </Link>
        </p>

        {/* Footer links */}
        <div className="flex items-center justify-center gap-4 text-xs text-slate-500 mt-4">
          <Link href={`/${lang}/privacy`} className="hover:text-[#0F9972]">
            {isAr ? "الخصوصية" : "Privacy"}
          </Link>
          <span>•</span>
          <Link href={`/${lang}/terms`} className="hover:text-[#0F9972]">
            {isAr ? "الشروط" : "Terms"}
          </Link>
        </div>
      </div>
    </div>
  );
}
```

**Step 2: Delete the old page**

```bash
rm app/\[lang\]/clerk-signup/page.tsx
```

**Step 3: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "clerk-signup"
# Expected: no output
```

**Step 4: Commit**

```bash
git add app/\[lang\]/clerk-signup/
git commit -m "feat(auth): restructure clerk-signup as catch-all route with card-wrapper design"
```

---

## Task 3: Hide Nav/Footer on Clerk Auth Pages

The existing `/login` and `/signup` pages don't show nav/footer because... actually they
DO inherit nav/footer from `LayoutContent`. But the clerk pages look cleaner without it
(distraction-free auth UX). Add them to `ROUTES_WITHOUT_NAV_FOOTER`.

**Files:**

- Modify: `components/LayoutContent.tsx` lines 27-31

**Step 1: Edit `ROUTES_WITHOUT_NAV_FOOTER`**

Find this block in `components/LayoutContent.tsx`:

```typescript
const ROUTES_WITHOUT_NAV_FOOTER = [
  "/dashboard", // Main user dashboard (e.g., /en/dashboard, /ar/dashboard)
  "/staff/dashboard", // Staff admin dashboard
  "/user/dashboard", // User-specific dashboard
] as const;
```

Replace with:

```typescript
const ROUTES_WITHOUT_NAV_FOOTER = [
  "/dashboard", // Main user dashboard (e.g., /en/dashboard, /ar/dashboard)
  "/staff/dashboard", // Staff admin dashboard
  "/user/dashboard", // User-specific dashboard
  "/clerk-login", // Clerk auth pages — distraction-free, no nav/footer
  "/clerk-signup",
] as const;
```

**Step 2: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "LayoutContent"
# Expected: no output
```

**Step 3: Commit**

```bash
git add components/LayoutContent.tsx
git commit -m "feat(auth): hide nav/footer on clerk-login and clerk-signup pages"
```

---

## Task 4: Layout + Metadata Files

Add `layout.tsx` to both clerk auth directories so they get correct page titles and
`noindex` meta (prevents search engines from indexing auth pages). Matches the pattern
used by the existing `app/[lang]/login/layout.tsx`.

**Files:**

- Create: `app/[lang]/clerk-login/layout.tsx`
- Create: `app/[lang]/clerk-signup/layout.tsx`

**Step 1: Create `app/[lang]/clerk-login/layout.tsx`**

```tsx
import type { Metadata } from "next";
import type { Lang } from "@/lib/config";
import { buildPageMetadata } from "@/lib/seo";

export function generateMetadata({
  params,
}: {
  params: { lang: Lang };
}): Metadata {
  const isAr = params.lang === "ar";
  return buildPageMetadata({
    lang: params.lang,
    title: isAr ? "تسجيل الدخول | موعدي" : "Login | Mawidi",
    description: isAr
      ? "سجل الدخول إلى حسابك في موعدي."
      : "Log in to your Mawidi account.",
    path: "/clerk-login",
    noindex: true,
  });
}

export default function ClerkLoginLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return children;
}
```

**Step 2: Create `app/[lang]/clerk-signup/layout.tsx`**

```tsx
import type { Metadata } from "next";
import type { Lang } from "@/lib/config";
import { buildPageMetadata } from "@/lib/seo";

export function generateMetadata({
  params,
}: {
  params: { lang: Lang };
}): Metadata {
  const isAr = params.lang === "ar";
  return buildPageMetadata({
    lang: params.lang,
    title: isAr ? "إنشاء حساب | موعدي" : "Sign Up | Mawidi",
    description: isAr
      ? "أنشئ حسابك في موعدي وابدأ اليوم."
      : "Create your Mawidi account and get started today.",
    path: "/clerk-signup",
    noindex: true,
  });
}

export default function ClerkSignupLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return children;
}
```

**Step 3: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep -E "clerk-login|clerk-signup"
# Expected: no output
```

**Step 4: Commit**

```bash
git add app/\[lang\]/clerk-login/layout.tsx app/\[lang\]/clerk-signup/layout.tsx
git commit -m "feat(auth): add noindex layout metadata for clerk-login and clerk-signup"
```

---

## Task 5: CSP — Add Clerk Domains

Clerk's widget loads scripts, makes API calls, loads images, and renders iframes from
`*.clerk.accounts.dev`. These must be in the CSP or production will block the widget.

In dev, CSP is report-only (no enforcement) — this fix is essential for production.

**Files:**

- Modify: `middleware.ts` function `buildCsp()` (lines 71-87)

**Step 1: Edit `buildCsp()` in `middleware.ts`**

Find the exact current lines:

```typescript
`script-src 'self' 'nonce-${nonce}'${isDev ? " 'unsafe-eval'" : ""} https://www.google.com https://www.gstatic.com https://js.stripe.com https://unpkg.com https://elevenlabs.io https://*.elevenlabs.io`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
"img-src 'self' data: https://js.stripe.com https://storage.googleapis.com https://elevenlabs.io https://*.elevenlabs.io",
"font-src 'self' https://fonts.gstatic.com",
"frame-src 'self' https://www.google.com https://js.stripe.com https://hooks.stripe.com",
"connect-src 'self' https://*.supabase.co https://api.stripe.com https://api.elevenlabs.io https://api.us.elevenlabs.io https://elevenlabs.io https://*.elevenlabs.io wss://api.elevenlabs.io wss://api.us.elevenlabs.io wss://livekit.rtc.elevenlabs.io https://www.google.com https://www.gstatic.com",
```

Replace with (Clerk domains added at end of each relevant directive):

```typescript
`script-src 'self' 'nonce-${nonce}'${isDev ? " 'unsafe-eval'" : ""} https://www.google.com https://www.gstatic.com https://js.stripe.com https://unpkg.com https://elevenlabs.io https://*.elevenlabs.io https://*.clerk.accounts.dev`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
"img-src 'self' data: https://js.stripe.com https://storage.googleapis.com https://elevenlabs.io https://*.elevenlabs.io https://*.clerk.accounts.dev",
"font-src 'self' https://fonts.gstatic.com",
"frame-src 'self' https://www.google.com https://js.stripe.com https://hooks.stripe.com https://*.clerk.accounts.dev",
"connect-src 'self' https://*.supabase.co https://api.stripe.com https://api.elevenlabs.io https://api.us.elevenlabs.io https://elevenlabs.io https://*.elevenlabs.io wss://api.elevenlabs.io wss://api.us.elevenlabs.io wss://livekit.rtc.elevenlabs.io https://www.google.com https://www.gstatic.com https://*.clerk.accounts.dev https://clerk.shared.global",
```

**Step 2: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "middleware"
# Expected: no output
```

**Step 3: Lint**

```bash
npm run lint -- middleware.ts 2>&1 | tail -5
# Expected: no errors
```

**Step 4: Commit**

```bash
git add middleware.ts
git commit -m "fix(csp): add Clerk domains to script-src, img-src, frame-src, connect-src"
```

---

## Task 6: Thread Nonce into ClerkProvider

Clerk v5 accepts a `nonce` prop on `<ClerkProvider>` to allow its inline styles through
the CSP without needing `unsafe-inline`. The nonce is already computed in `app/layout.tsx`
via `getCspNonce()` — just pass it through.

**Files:**

- Modify: `app/layout.tsx` — the `<ClerkProvider>` render in the `RootLayout` function

**Step 1: Find and update the ClerkProvider line**

In `app/layout.tsx`, find:

```tsx
        {CLERK_ENABLED ? (
          <ClerkProvider>
```

Replace with:

```tsx
        {CLERK_ENABLED ? (
          <ClerkProvider nonce={nonce}>
```

That's the only change. `nonce` is already defined 2 lines above via `const nonce = getCspNonce()`.

**Step 2: Verify TypeScript**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "layout"
# Expected: no output
```

**Step 3: Commit**

```bash
git add app/layout.tsx
git commit -m "fix(csp): thread CSP nonce into ClerkProvider for inline style compliance"
```

---

## Task 7: Verification

**Step 1: Full TypeScript check**

```bash
npx tsc --noEmit 2>&1 | grep -v node_modules | grep "error TS"
# Expected: same count as Task 0 baseline (no new errors introduced)
```

**Step 2: Lint check**

```bash
npm run lint 2>&1 | tail -10
# Expected: no errors on any changed files
```

**Step 3: Browser verification — EN login**

Navigate to `http://localhost:9000/en/clerk-login`. Confirm:

- [ ] No nav bar, no footer (clean auth page)
- [ ] Mawidi logo centered above white card
- [ ] White card (`bg-white rounded-xl shadow-sm border`)
- [ ] Google social button only (no Apple, no Facebook)
- [ ] Email field with green focus ring
- [ ] Green "Continue" button
- [ ] "Don't have an account? Sign up" link below card
- [ ] Privacy + Terms links below
- [ ] Zero Clerk catch-all errors in browser console
- [ ] Zero CSP blocking errors in browser console (INFO warnings OK in dev)

**Step 4: Browser verification — AR login**

Navigate to `http://localhost:9000/ar/clerk-login`. Confirm:

- [ ] RTL layout (`dir="rtl"`)
- [ ] Arabic font (Cairo)
- [ ] Same card/logo/button structure

**Step 5: Browser verification — signup**

Navigate to `http://localhost:9000/en/clerk-signup`. Confirm same visual checklist.

**Step 6: Cross-link navigation**

- On clerk-login, click "Sign up" → lands on `/en/clerk-signup` ✓
- On clerk-signup, click "Sign in" → lands on `/en/clerk-login` ✓

**Step 7: Confirm `/login` is untouched**

Navigate to `http://localhost:9000/en/login`. Confirm the existing NextAuth login page
loads exactly as before (no regression).

---

## Final Commit Summary

After all tasks pass verification:

```bash
git log --oneline -7
# Should show 6 commits:
# fix(csp): thread CSP nonce into ClerkProvider for inline style compliance
# fix(csp): add Clerk domains to script-src, img-src, frame-src, connect-src
# feat(auth): add noindex layout metadata for clerk-login and clerk-signup
# feat(auth): hide nav/footer on clerk-login and clerk-signup pages
# feat(auth): restructure clerk-signup as catch-all route with card-wrapper design
# feat(auth): restructure clerk-login as catch-all route with card-wrapper design
```

## What Is NOT Changed (Guard Rail)

- `app/[lang]/login/` — untouched
- `app/[lang]/signup/` — untouched
- `lib/auth.ts` — untouched (NextAuth stays)
- `lib/data-source.ts` — untouched
- Any `convex/` files — untouched
- `middleware.ts` auth logic — only CSP `buildCsp()` function edited
