diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6402712
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,366 @@
+# Agent Development Guide for FitAI
+
+This document provides essential guidelines for AI coding agents working in the FitAI codebase.
+
+## Project Overview
+
+FitAI is a monorepo containing a fitness AI platform with:
+
+- **Admin App** (`apps/admin`): Next.js 16 web application for gym management
+- **Mobile App** (`apps/mobile`): React Native/Expo mobile app for clients
+- **Shared Packages**: Database (`@fitai/database`) and utilities (`@fitai/shared`)
+
+**Tech Stack**: TypeScript, Next.js 16, React 19, React Native, Expo 54, Drizzle ORM, Clerk Auth, TanStack Query, Tailwind CSS
+
+## Build, Lint, and Test Commands
+
+### From Repository Root (`/home/echo/dev/prototype`)
+
+```bash
+# Development
+npm run dev # Start both admin and mobile dev servers
+npm run dev:admin # Start admin dev server (Next.js)
+npm run dev:mobile # Start mobile dev server (Expo)
+
+# Build
+npm run build # Build both apps
+npm run build:admin # Build admin app
+npm run build:mobile # Build mobile app
+
+# Linting
+npm run lint # Lint both apps
+npm run lint:admin # Lint admin app
+npm run lint:mobile # Lint mobile app
+
+# Type Checking
+npm run typecheck # Check types in both apps
+npm run typecheck:admin # Check types in admin
+npm run typecheck:mobile # Check types in mobile
+
+# Testing
+npm run test # Run all tests
+npm run test:admin # Run admin tests
+npm run test:mobile # Run mobile tests
+```
+
+### Admin App (`apps/admin`)
+
+```bash
+cd apps/admin
+
+# Development
+npm run dev # Start Next.js dev server (port 3000)
+
+# Testing
+npm test # Run all tests
+npx jest # Run all tests
+npx jest path/to/file.test.ts # Run specific test file
+npx jest --testPathPattern=drizzle # Run tests matching pattern
+npx jest src/lib/database/__tests__/drizzle.test.ts # Run specific test
+
+# Build & Lint
+npm run build # Build production bundle
+npm run lint # Run ESLint
+npm run typecheck # Type check without emitting
+```
+
+### Mobile App (`apps/mobile`)
+
+```bash
+cd apps/mobile
+
+# Development
+npm start # Start Expo dev server
+npm run android # Start on Android emulator
+npm run ios # Start on iOS simulator
+
+# Testing
+npm test # Run all tests
+npx jest path/to/file.test.ts # Run specific test file
+npx jest --testPathPattern=component # Run tests matching pattern
+
+# Build & Lint
+npm run build # Build with Expo
+npm run lint # Run ESLint
+npm run typecheck # Type check
+```
+
+## Code Style Guidelines
+
+### Import Organization
+
+**Order**: External libraries → Internal imports, grouped logically
+
+```typescript
+// External: React, Next.js, React Native
+import { NextRequest, NextResponse } from "next/server";
+import React from "react";
+
+// External: Third-party libraries
+import { auth, clerkClient } from "@clerk/nextjs/server";
+import bcrypt from "bcryptjs";
+
+// Monorepo packages
+import { db, sql } from "@fitai/database";
+
+// Internal: Path aliases or relative imports
+import { getDatabase } from "@/lib/database";
+import type { User } from "./types";
+```
+
+**Key Rules**:
+
+- Use `import type { ... }` for type-only imports
+- Admin app uses path aliases: `@/*` → `./src/*`
+- Mobile app uses relative paths or `@/*` alias
+- No automatic import sorting; manual grouping by category
+
+### Component Structure & Naming
+
+**Components**: PascalCase function components with named exports
+
+```typescript
+// Feature components - Named export (function)
+export function UserManagement() {
+ return
...
+}
+
+// UI library components - Named export (forwardRef constant)
+const Card = React.forwardRef(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+Card.displayName = "Card"
+export { Card }
+```
+
+**Naming Conventions**:
+
+- Components: `UserManagement`, `GoalProgressCard`
+- Functions: `handleEditUser`, `getGreeting`
+- Custom hooks: `useUser`, `useAuth`
+- API routes: Named exports `GET`, `POST`, `PUT`, `DELETE`
+
+### Type Definitions
+
+**Interfaces** for object shapes and React props; **Types** for unions and aliases
+
+```typescript
+// Props interfaces (ComponentNameProps pattern)
+interface UserManagementProps {
+ userId: string;
+ onUpdate?: () => void;
+}
+
+// Domain interfaces
+interface User {
+ id: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+}
+
+// Type aliases and unions
+type UserRole = "admin" | "trainer" | "client";
+type Status = "active" | "inactive" | "pending";
+
+// No I prefix or T prefix
+```
+
+### Function Patterns
+
+**Arrow functions** for components and handlers; **function declarations** for utilities
+
+```typescript
+// Component functions
+export function UserCard() {
+ const handleClick = () => { /* ... */ } // Arrow function for handlers
+ return Click
+}
+
+// Exported utility functions
+export async function setUserRole(userId: string, role: UserRole) {
+ // ... implementation
+}
+
+// Always use async/await (never .then() chains)
+const fetchData = async () => {
+ try {
+ const response = await fetch(url)
+ const data = await response.json()
+ return data
+ } catch (error) {
+ console.error('Failed to fetch:', error)
+ }
+}
+```
+
+### Error Handling
+
+**API Routes** (Next.js):
+
+```typescript
+export async function POST(request: NextRequest) {
+ try {
+ const { email, password } = await request.json();
+
+ // Early returns for validation
+ if (!email || !password) {
+ return NextResponse.json(
+ { error: "Email and password are required" },
+ { status: 400 },
+ );
+ }
+
+ // ... logic
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Login error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
+}
+```
+
+**React Components**:
+
+```typescript
+// Admin: Use try-catch with console.error
+const handleSave = async () => {
+ try {
+ await updateUser(userId, data);
+ } catch (error) {
+ console.error("Failed to update user:", error);
+ // Show error to user (toast, alert, etc.)
+ }
+};
+
+// Mobile: Use Alert for confirmations
+const handleDelete = () => {
+ Alert.alert("Delete Goal", "Are you sure?", [
+ { text: "Cancel", style: "cancel" },
+ { text: "Delete", style: "destructive", onPress: onDelete },
+ ]);
+};
+```
+
+### Comments & Documentation
+
+Use JSDoc for public APIs; inline comments for complex logic only
+
+```typescript
+/**
+ * Set a user's role in Clerk public metadata
+ *
+ * @param userId - Clerk user ID
+ * @param role - Role to assign (admin, trainer, or client)
+ * @returns Updated user object
+ *
+ * @example
+ * await setUserRole('user_abc123', 'admin')
+ */
+export async function setUserRole(userId: string, role: UserRole) {
+ // Implementation
+}
+
+// Inline comments for clarification
+// Optimistically update local state so grid reflects changes immediately
+setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, ...updates } : u)));
+```
+
+### File Naming Conventions
+
+```
+apps/admin/src/
+├── app/
+│ ├── api/*/route.ts # API routes (lowercase route.ts)
+│ └── */page.tsx # Pages (lowercase page.tsx)
+├── components/
+│ ├── ui/button.tsx # UI primitives (kebab-case)
+│ └── users/UserManagement.tsx # Feature components (PascalCase)
+├── lib/
+│ ├── database/index.ts # Utilities (kebab-case)
+│ └── clerk-helpers.ts # Helpers (kebab-case)
+
+apps/mobile/src/
+├── app/
+│ └── (tabs)/index.tsx # Routes (lowercase)
+├── components/
+│ └── GoalProgressCard.tsx # All components (PascalCase)
+└── services/
+ └── fitnessGoals.ts # Services (camelCase)
+```
+
+### Styling
+
+**Admin App**: Tailwind CSS with utility classes
+
+```typescript
+
+
+ Click me
+
+
+```
+
+**Mobile App**: StyleSheet.create() at file bottom with theme system
+
+```typescript
+import { theme } from '../styles/theme'
+
+export function MyComponent() {
+ return ...
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: theme.colors.background,
+ padding: theme.spacing.md,
+ },
+})
+```
+
+## ESLint Rules (Admin)
+
+- `@typescript-eslint/no-unused-vars`: error
+- `@typescript-eslint/no-explicit-any`: warn (allowed but discouraged)
+- `prefer-const`: error
+- `no-var`: error
+- Extends: `next/core-web-vitals`, `@typescript-eslint/recommended`
+
+## TypeScript Configuration
+
+- **Strict mode enabled**: All strict type checking options on
+- **Path aliases**: `@/*` maps to `./src/*`
+- **Module resolution**: `bundler` (admin), `node` (mobile)
+- **Target**: ES5 (admin), ESNext (mobile)
+- Always provide explicit return types for exported functions
+
+## Testing Best Practices
+
+- Tests in `__tests__/` directories or `*.test.ts` files
+- Use `@jest-environment node` comment for Node.js API tests
+- Admin: Jest + ts-jest + @testing-library/react
+- Mobile: Jest + react-native preset + @testing-library/react-native
+- Test file naming: `component.test.ts` or `feature.test.tsx`
+- Always test error cases and edge cases
+
+## Key Patterns
+
+1. **State Management**: Multiple `useState` declarations grouped together
+2. **Destructuring**: Props in function signature, responses inline
+3. **Type Safety**: Explicit return types, const assertions for readonly arrays
+4. **Database**: Factory pattern with singleton (`getDatabase()`)
+5. **Forms**: React Hook Form + Zod validation
+6. **Data Fetching**: TanStack Query for server state
+7. **Authentication**: Clerk for both admin and mobile (different packages)
+
+## Node & Package Manager
+
+- **Node**: >=18.0.0
+- **Package Manager**: npm (>=9.0.0)
+- Use `npm install` (not yarn or pnpm)
diff --git a/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts
index 28a8504..87d4e68 100644
--- a/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts
+++ b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts
@@ -1,39 +1,62 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { auth } from '@clerk/nextjs/server';
-import { getDatabase } from '@/lib/database';
+import { NextRequest, NextResponse } from "next/server";
+import { auth } from "@clerk/nextjs/server";
+import { getDatabase } from "@/lib/database";
+
+// Helper to add CORS headers
+function corsHeaders() {
+ return {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ };
+}
+
+// OPTIONS - Handle preflight requests
+export async function OPTIONS() {
+ return NextResponse.json({}, { headers: corsHeaders() });
+}
// POST - Mark goal as complete
export async function POST(
- req: NextRequest,
- { params }: { params: Promise<{ id: string }> }
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> },
) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = await params;
- const db = await getDatabase();
-
- // Verify goal exists and user owns it
- const existingGoal = await db.getFitnessGoalById(id);
- if (!existingGoal) {
- return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
- }
- if (existingGoal.userId !== userId) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
-
- // Mark as completed
- const completedGoal = await db.completeGoal(id);
-
- return NextResponse.json(completedGoal);
- } catch (error) {
- console.error('Error completing fitness goal:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ try {
+ const { userId } = await auth();
+ if (!userId) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ const { id } = await params;
+ const db = await getDatabase();
+
+ // Verify goal exists and user owns it
+ const existingGoal = await db.getFitnessGoalById(id);
+ if (!existingGoal) {
+ return NextResponse.json(
+ { error: "Goal not found" },
+ { status: 404, headers: corsHeaders() },
+ );
+ }
+ if (existingGoal.userId !== userId) {
+ return NextResponse.json(
+ { error: "Forbidden" },
+ { status: 403, headers: corsHeaders() },
+ );
+ }
+
+ // Mark as completed
+ const completedGoal = await db.completeGoal(id);
+
+ return NextResponse.json(completedGoal, { headers: corsHeaders() });
+ } catch (error) {
+ console.error("Error completing fitness goal:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
diff --git a/apps/admin/src/app/api/fitness-goals/[id]/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/route.ts
index d0f6fb5..701c2a7 100644
--- a/apps/admin/src/app/api/fitness-goals/[id]/route.ts
+++ b/apps/admin/src/app/api/fitness-goals/[id]/route.ts
@@ -1,119 +1,163 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { auth } from '@clerk/nextjs/server';
-import { getDatabase } from '@/lib/database';
+import { NextRequest, NextResponse } from "next/server";
+import { auth } from "@clerk/nextjs/server";
+import { getDatabase } from "@/lib/database";
+
+// Helper to add CORS headers
+function corsHeaders() {
+ return {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ };
+}
+
+// OPTIONS - Handle preflight requests
+export async function OPTIONS() {
+ return NextResponse.json({}, { headers: corsHeaders() });
+}
// GET - Get specific goal
export async function GET(
- req: NextRequest,
- { params }: { params: Promise<{ id: string }> }
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> },
) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = await params;
- const db = await getDatabase();
-
- const goal = await db.getFitnessGoalById(id);
-
- if (!goal) {
- return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
- }
-
- // Verify ownership
- if (goal.userId !== userId) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
-
- return NextResponse.json(goal);
- } catch (error) {
- console.error('Error fetching fitness goal:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ try {
+ const { userId } = await auth();
+ if (!userId) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ const { id } = await params;
+ const db = await getDatabase();
+
+ const goal = await db.getFitnessGoalById(id);
+
+ if (!goal) {
+ return NextResponse.json(
+ { error: "Goal not found" },
+ { status: 404, headers: corsHeaders() },
+ );
+ }
+
+ // Verify ownership
+ if (goal.userId !== userId) {
+ return NextResponse.json(
+ { error: "Forbidden" },
+ { status: 403, headers: corsHeaders() },
+ );
+ }
+
+ return NextResponse.json(goal, { headers: corsHeaders() });
+ } catch (error) {
+ console.error("Error fetching fitness goal:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
// PUT - Update goal
export async function PUT(
- req: NextRequest,
- { params }: { params: Promise<{ id: string }> }
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> },
) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = await params;
- const db = await getDatabase();
-
- // Verify goal exists and user owns it
- const existingGoal = await db.getFitnessGoalById(id);
- if (!existingGoal) {
- return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
- }
- if (existingGoal.userId !== userId) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
-
- const updates = await req.json();
-
- // Don't allow changing userId or id
- delete updates.userId;
- delete updates.id;
- delete updates.createdAt;
-
- const updatedGoal = await db.updateFitnessGoal(id, updates);
-
- return NextResponse.json(updatedGoal);
- } catch (error) {
- console.error('Error updating fitness goal:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ try {
+ const { userId } = await auth();
+ if (!userId) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ const { id } = await params;
+ const db = await getDatabase();
+
+ // Verify goal exists and user owns it
+ const existingGoal = await db.getFitnessGoalById(id);
+ if (!existingGoal) {
+ return NextResponse.json(
+ { error: "Goal not found" },
+ { status: 404, headers: corsHeaders() },
+ );
+ }
+ if (existingGoal.userId !== userId) {
+ return NextResponse.json(
+ { error: "Forbidden" },
+ { status: 403, headers: corsHeaders() },
+ );
+ }
+
+ const updates = await req.json();
+
+ // Don't allow changing userId or id
+ delete updates.userId;
+ delete updates.id;
+ delete updates.createdAt;
+
+ const updatedGoal = await db.updateFitnessGoal(id, updates);
+
+ return NextResponse.json(updatedGoal, { headers: corsHeaders() });
+ } catch (error) {
+ console.error("Error updating fitness goal:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
// DELETE - Delete goal
export async function DELETE(
- req: NextRequest,
- { params }: { params: Promise<{ id: string }> }
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> },
) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = await params;
- const db = await getDatabase();
-
- // Verify goal exists and user owns it
- const existingGoal = await db.getFitnessGoalById(id);
- if (!existingGoal) {
- return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
- }
- if (existingGoal.userId !== userId) {
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
- }
-
- const deleted = await db.deleteFitnessGoal(id);
-
- if (deleted) {
- return NextResponse.json({ success: true });
- } else {
- return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 });
- }
- } catch (error) {
- console.error('Error deleting fitness goal:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ try {
+ const { userId } = await auth();
+ if (!userId) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ const { id } = await params;
+ const db = await getDatabase();
+
+ // Verify goal exists and user owns it
+ const existingGoal = await db.getFitnessGoalById(id);
+ if (!existingGoal) {
+ return NextResponse.json(
+ { error: "Goal not found" },
+ { status: 404, headers: corsHeaders() },
+ );
+ }
+ if (existingGoal.userId !== userId) {
+ return NextResponse.json(
+ { error: "Forbidden" },
+ { status: 403, headers: corsHeaders() },
+ );
+ }
+
+ const deleted = await db.deleteFitnessGoal(id);
+
+ if (deleted) {
+ return NextResponse.json({ success: true }, { headers: corsHeaders() });
+ } else {
+ return NextResponse.json(
+ { error: "Failed to delete goal" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
+ } catch (error) {
+ console.error("Error deleting fitness goal:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
diff --git a/apps/admin/src/app/api/fitness-goals/route.ts b/apps/admin/src/app/api/fitness-goals/route.ts
index c82c3f3..ab82724 100644
--- a/apps/admin/src/app/api/fitness-goals/route.ts
+++ b/apps/admin/src/app/api/fitness-goals/route.ts
@@ -1,95 +1,141 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { auth } from '@clerk/nextjs/server';
-import { getDatabase } from '@/lib/database';
-import { randomBytes } from 'crypto';
+import { NextRequest, NextResponse } from "next/server";
+import { auth } from "@clerk/nextjs/server";
+import { getDatabase } from "@/lib/database";
+import { randomBytes } from "crypto";
+
+// Helper to add CORS headers
+function corsHeaders() {
+ return {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ };
+}
+
+// Helper to get user ID from auth (works with both web sessions and mobile tokens)
+async function getAuthenticatedUserId(
+ req: NextRequest,
+): Promise {
+ // The auth() function from Clerk should handle Bearer tokens automatically
+ // when called within an API route that's processed by clerkMiddleware
+ const { userId } = await auth();
+ return userId;
+}
+
+// OPTIONS - Handle preflight requests
+export async function OPTIONS() {
+ return NextResponse.json({}, { headers: corsHeaders() });
+}
// GET - List user's fitness goals
export async function GET(req: NextRequest) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
+ try {
+ const userId = await getAuthenticatedUserId(req);
- const { searchParams } = new URL(req.url);
- const targetUserId = searchParams.get('userId') || userId;
- const status = searchParams.get('status'); // active, completed, all
-
- const db = await getDatabase();
-
- // Fetch goals with optional status filter
- const goals = await db.getFitnessGoalsByUserId(
- targetUserId,
- status && status !== 'all' ? status : undefined
- );
-
- return NextResponse.json(goals);
- } catch (error) {
- console.error('Error fetching fitness goals:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ if (!userId) {
+ console.error("[Fitness Goals API] Authentication failed");
+ console.error(
+ "[Fitness Goals API] Headers:",
+ Object.fromEntries(req.headers.entries()),
+ );
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ console.log("[Fitness Goals API] Authenticated user:", userId);
+
+ const { searchParams } = new URL(req.url);
+ const targetUserId = searchParams.get("userId") || userId;
+ const status = searchParams.get("status"); // active, completed, all
+
+ const db = await getDatabase();
+
+ // Fetch goals with optional status filter
+ const goals = await db.getFitnessGoalsByUserId(
+ targetUserId,
+ status && status !== "all" ? status : undefined,
+ );
+
+ console.log(
+ `[Fitness Goals API] Found ${goals.length} goals for user ${targetUserId}`,
+ );
+ return NextResponse.json(goals, { headers: corsHeaders() });
+ } catch (error) {
+ console.error("[Fitness Goals API] Error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
// POST - Create new fitness goal
export async function POST(req: NextRequest) {
- try {
- const { userId } = await auth();
- if (!userId) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
+ try {
+ const userId = await getAuthenticatedUserId(req);
- const body = await req.json();
- const {
- goalType,
- title,
- description,
- targetValue,
- currentValue,
- unit,
- targetDate,
- priority,
- notes,
- fitnessProfileId
- } = body;
-
- // Validation
- if (!goalType || !title) {
- return NextResponse.json(
- { error: 'goalType and title are required' },
- { status: 400 }
- );
- }
-
- const db = await getDatabase();
-
- // Create the goal
- const goal = await db.createFitnessGoal({
- id: randomBytes(16).toString('hex'),
- userId,
- fitnessProfileId: fitnessProfileId || undefined,
- goalType,
- title,
- description: description || undefined,
- targetValue: targetValue || undefined,
- currentValue: currentValue || 0,
- unit: unit || undefined,
- startDate: new Date(),
- targetDate: targetDate ? new Date(targetDate) : undefined,
- status: 'active',
- progress: 0,
- priority: priority || 'medium',
- notes: notes || undefined
- });
-
- return NextResponse.json(goal, { status: 201 });
- } catch (error) {
- console.error('Error creating fitness goal:', error);
- return NextResponse.json(
- { error: 'Internal server error' },
- { status: 500 }
- );
+ if (!userId) {
+ console.error("[Fitness Goals API] Authentication failed for POST");
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401, headers: corsHeaders() },
+ );
}
+
+ const body = await req.json();
+ const {
+ goalType,
+ title,
+ description,
+ targetValue,
+ currentValue,
+ unit,
+ targetDate,
+ priority,
+ notes,
+ fitnessProfileId,
+ } = body;
+
+ // Validation
+ if (!goalType || !title) {
+ return NextResponse.json(
+ { error: "goalType and title are required" },
+ { status: 400, headers: corsHeaders() },
+ );
+ }
+
+ const db = await getDatabase();
+
+ // Create the goal
+ const goal = await db.createFitnessGoal({
+ id: randomBytes(16).toString("hex"),
+ userId,
+ fitnessProfileId: fitnessProfileId || undefined,
+ goalType,
+ title,
+ description: description || undefined,
+ targetValue: targetValue || undefined,
+ currentValue: currentValue || 0,
+ unit: unit || undefined,
+ startDate: new Date(),
+ targetDate: targetDate ? new Date(targetDate) : undefined,
+ status: "active",
+ progress: 0,
+ priority: priority || "medium",
+ notes: notes || undefined,
+ });
+
+ console.log(
+ `[Fitness Goals API] Created goal ${goal.id} for user ${userId}`,
+ );
+ return NextResponse.json(goal, { status: 201, headers: corsHeaders() });
+ } catch (error) {
+ console.error("[Fitness Goals API] Error creating goal:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, headers: corsHeaders() },
+ );
+ }
}
diff --git a/apps/admin/src/app/api/test/route.ts b/apps/admin/src/app/api/test/route.ts
new file mode 100644
index 0000000..1681868
--- /dev/null
+++ b/apps/admin/src/app/api/test/route.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json({ message: "Test endpoint works!" });
+}
diff --git a/apps/admin/src/lib/auth-helper.ts b/apps/admin/src/lib/auth-helper.ts
new file mode 100644
index 0000000..2547096
--- /dev/null
+++ b/apps/admin/src/lib/auth-helper.ts
@@ -0,0 +1,47 @@
+import { auth, currentUser } from "@clerk/nextjs/server";
+import { NextRequest } from "next/server";
+
+/**
+ * Get authenticated user ID from request
+ * Handles both session-based auth (web) and Bearer token auth (mobile)
+ *
+ * For mobile apps using Clerk Expo, tokens should be passed as:
+ * Authorization: Bearer
+ */
+export async function getAuthUserId(req: NextRequest): Promise {
+ try {
+ // Clerk's auth() should handle both cookies and Bearer tokens automatically
+ // when the request is properly formatted
+ const { userId } = await auth();
+
+ if (userId) {
+ console.log("✓ Authenticated user:", userId);
+ return userId;
+ }
+
+ console.log("✗ No authentication found");
+
+ // Log headers for debugging
+ const authHeader = req.headers.get("authorization");
+ console.log("Authorization header:", authHeader ? "Present" : "Missing");
+
+ return null;
+ } catch (error) {
+ console.error("Authentication error:", error);
+ return null;
+ }
+}
+
+/**
+ * Simplified version that just uses Clerk's built-in auth
+ * This should work with both session cookies and Bearer tokens
+ */
+export async function requireAuth(req: NextRequest): Promise {
+ const userId = await getAuthUserId(req);
+
+ if (!userId) {
+ throw new Error("Unauthorized");
+ }
+
+ return userId;
+}
diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts
index 0ae9460..83e7761 100644
--- a/apps/admin/src/middleware.ts
+++ b/apps/admin/src/middleware.ts
@@ -1,30 +1,43 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
-// Define routes that should be publicly accessible
+// Define routes that should be publicly accessible (no auth required)
const isPublicRoute = createRouteMatcher([
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks(.*)",
- "/api/attendance(.*)",
+ "/api/health(.*)",
]);
-// Define routes that require authentication
-const isProtectedRoute = createRouteMatcher([
- "/",
- "/users(.*)",
- "/analytics(.*)",
- "/profile(.*)",
- "/api/users(.*)",
- "/api/profile(.*)",
- "/api/payments(.*)",
- "/api/notifications(.*)",
-]);
+// Define API routes that need auth but should be handled in the route itself
+// This prevents auth.protect() from blocking before the route handler runs
+const isApiRoute = createRouteMatcher(["/api/(.*)"]);
export default clerkMiddleware(async (auth, req) => {
- // Protect all routes except public ones
- if (!isPublicRoute(req)) {
- await auth.protect();
+ // Log for debugging
+ const authHeader = req.headers.get("authorization");
+ if (authHeader) {
+ console.log(
+ "[Middleware] Authorization header present:",
+ authHeader.substring(0, 20) + "...",
+ );
}
+
+ // Don't protect public routes
+ if (isPublicRoute(req)) {
+ console.log("[Middleware] Public route, skipping auth");
+ return;
+ }
+
+ // For API routes, let the route handler check auth
+ // This allows API routes to handle both web sessions and mobile Bearer tokens
+ if (isApiRoute(req)) {
+ console.log("[Middleware] API route, auth will be checked in handler");
+ return;
+ }
+
+ // For all other routes (web pages), enforce authentication
+ console.log("[Middleware] Protected route, requiring auth");
+ await auth.protect();
});
export const config = {
diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx
index a264f63..b36f5e3 100644
--- a/apps/mobile/src/app/(tabs)/goals.tsx
+++ b/apps/mobile/src/app/(tabs)/goals.tsx
@@ -1,290 +1,392 @@
-import React, { useState, useCallback, useRef } from 'react';
-import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native';
-import { Ionicons } from '@expo/vector-icons';
-import { LinearGradient } from 'expo-linear-gradient';
-import { theme } from '../../styles/theme';
-import { GoalProgressCard } from '../../components/GoalProgressCard';
-import { GoalCreationModal } from '../../components/GoalCreationModal';
+import React, { useState, useCallback, useRef } from "react";
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ RefreshControl,
+ TouchableOpacity,
+ Animated,
+ Alert,
+} from "react-native";
+import { Ionicons } from "@expo/vector-icons";
+import { LinearGradient } from "expo-linear-gradient";
+import { theme } from "../../styles/theme";
+import { GoalProgressCard } from "../../components/GoalProgressCard";
+import { GoalCreationModal } from "../../components/GoalCreationModal";
import { useUser, useAuth } from "@clerk/clerk-expo";
-import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals';
-import { useFocusEffect } from 'expo-router';
+import {
+ fitnessGoalsService,
+ type FitnessGoal,
+ type CreateGoalData,
+} from "../../services/fitnessGoals";
+import { useFocusEffect } from "expo-router";
+import * as SecureStore from "expo-secure-store";
export default function GoalsScreen() {
- const { user } = useUser();
- const { getToken } = useAuth();
- const [goals, setGoals] = useState([]);
- const [refreshing, setRefreshing] = useState(false);
- const [isModalVisible, setIsModalVisible] = useState(false);
- const fabScale = useRef(new Animated.Value(1)).current;
+ const { user } = useUser();
+ const { getToken } = useAuth();
+ const [goals, setGoals] = useState([]);
+ const [refreshing, setRefreshing] = useState(false);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const fabScale = useRef(new Animated.Value(1)).current;
- const loadGoals = useCallback(async () => {
- if (!user?.id) return;
+ const loadGoals = useCallback(async () => {
+ if (!user?.id) return;
+ try {
+ const token = await getToken();
+ console.log(
+ "Token obtained:",
+ token ? "Yes (" + token.substring(0, 20) + "...)" : "No",
+ );
+ console.log("User ID:", user.id);
+
+ // Decode and log token details for debugging
+ if (token) {
try {
- const token = await getToken();
- const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
- setGoals(loadedGoals);
- } catch (error) {
- console.error('Error loading goals:', error);
+ const parts = token.split(".");
+ if (parts.length === 3) {
+ const payload = JSON.parse(atob(parts[1]));
+ console.log("Token issuer:", payload.iss);
+ console.log(
+ "Token kid from header:",
+ JSON.parse(atob(parts[0])).kid,
+ );
+ }
+ } catch (e) {
+ console.log("Could not decode token");
}
- }, [user?.id]); // Removed getToken from dependencies
+ }
- useFocusEffect(
- useCallback(() => {
- loadGoals();
- }, [loadGoals])
- );
+ const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
+ setGoals(loadedGoals);
+ } catch (error) {
+ console.error("Error loading goals:", error);
+ }
+ }, [user?.id, getToken]);
- const onRefresh = async () => {
- setRefreshing(true);
- await loadGoals();
- setRefreshing(false);
- };
+ const clearClerkCache = async () => {
+ Alert.alert(
+ "Clear Clerk Cache",
+ "This will clear all cached Clerk tokens. You will need to sign out and sign back in.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Clear Cache",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ // Clear all possible Clerk token keys
+ const keysToDelete = [
+ "__clerk_client_jwt",
+ "__clerk_db_jwt",
+ "__clerk_client_uat",
+ "__clerk_session_id",
+ "__clerk_refresh_token",
+ "__clerk_session_jwt",
+ ];
- const handleCreateGoal = async (newGoal: CreateGoalData) => {
- const token = await getToken();
- await fitnessGoalsService.createGoal(newGoal, token);
- await loadGoals();
- setIsModalVisible(false);
- };
-
- const handleCompleteGoal = async (goal: FitnessGoal) => {
- const token = await getToken();
- await fitnessGoalsService.completeGoal(goal.id, token);
- await loadGoals();
- };
-
- const handleDeleteGoal = async (goalId: string) => {
- const token = await getToken();
- await fitnessGoalsService.deleteGoal(goalId, token);
- await loadGoals();
- };
-
- const activeGoals = goals.filter(g => g.status === 'active');
- const completedGoals = goals.filter(g => g.status === 'completed');
-
- return (
-
-
+ for (const key of keysToDelete) {
+ try {
+ await SecureStore.deleteItemAsync(key);
+ } catch (e) {
+ // Key might not exist
}
- >
-
-
- Fitness Goals
-
- Track your fitness journey progress
-
-
-
+ }
- {/* Stats Summary */}
- {goals.length > 0 && (
-
-
- {activeGoals.length}
- Active
-
-
- {completedGoals.length}
- Completed
-
-
-
- {activeGoals.length > 0
- ? Math.round(
- activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
- activeGoals.length
- )
- : 0}%
-
- Avg Progress
-
-
- )}
-
- {/* Active Goals */}
-
-
- Active Goals ({activeGoals.length})
-
- {activeGoals.length === 0 ? (
-
-
- No active goals yet
-
- Tap the + button to create your first goal
-
-
- ) : (
- activeGoals.map((goal) => (
- handleCompleteGoal(goal)}
- onDelete={() => handleDeleteGoal(goal.id)}
- />
- ))
- )}
-
-
- {/* Completed Goals */}
- {completedGoals.length > 0 && (
-
-
- Completed Goals ({completedGoals.length})
-
- {completedGoals.map((goal) => (
- handleDeleteGoal(goal.id)}
- />
- ))}
-
- )}
-
-
-
-
- {/* Floating Action Button */}
-
- setIsModalVisible(true)}
- onPressIn={() => {
- Animated.spring(fabScale, {
- toValue: 0.9,
- friction: 8,
- tension: 100,
- useNativeDriver: true,
- }).start();
- }}
- onPressOut={() => {
- Animated.spring(fabScale, {
- toValue: 1,
- friction: 8,
- tension: 100,
- useNativeDriver: true,
- }).start();
- }}
- activeOpacity={0.9}
- >
-
-
-
-
-
-
- {/* Create Goal Modal */}
- setIsModalVisible(false)}
- onSubmit={handleCreateGoal}
- />
-
+ Alert.alert(
+ "Success",
+ "Cache cleared! Please sign out and sign back in.",
+ );
+ } catch (error) {
+ console.error("Error clearing cache:", error);
+ Alert.alert("Error", "Failed to clear cache");
+ }
+ },
+ },
+ ],
);
+ };
+
+ useFocusEffect(
+ useCallback(() => {
+ loadGoals();
+ }, [loadGoals]),
+ );
+
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await loadGoals();
+ setRefreshing(false);
+ };
+
+ const handleCreateGoal = async (newGoal: CreateGoalData) => {
+ const token = await getToken();
+ await fitnessGoalsService.createGoal(newGoal, token);
+ await loadGoals();
+ setIsModalVisible(false);
+ };
+
+ const handleCompleteGoal = async (goal: FitnessGoal) => {
+ const token = await getToken();
+ await fitnessGoalsService.completeGoal(goal.id, token);
+ await loadGoals();
+ };
+
+ const handleDeleteGoal = async (goalId: string) => {
+ const token = await getToken();
+ await fitnessGoalsService.deleteGoal(goalId, token);
+ await loadGoals();
+ };
+
+ const activeGoals = goals.filter((g) => g.status === "active");
+ const completedGoals = goals.filter((g) => g.status === "completed");
+
+ return (
+
+
+ }
+ >
+
+
+
+ Fitness Goals
+
+ Track your fitness journey progress
+
+
+
+
+
+
+
+
+ {/* Stats Summary */}
+ {goals.length > 0 && (
+
+
+ {activeGoals.length}
+ Active
+
+
+ {completedGoals.length}
+ Completed
+
+
+
+ {activeGoals.length > 0
+ ? Math.round(
+ activeGoals.reduce(
+ (sum, g) => sum + (g.progress || 0),
+ 0,
+ ) / activeGoals.length,
+ )
+ : 0}
+ %
+
+ Avg Progress
+
+
+ )}
+
+ {/* Active Goals */}
+
+
+ Active Goals ({activeGoals.length})
+
+ {activeGoals.length === 0 ? (
+
+
+ No active goals yet
+
+ Tap the + button to create your first goal
+
+
+ ) : (
+ activeGoals.map((goal) => (
+ handleCompleteGoal(goal)}
+ onDelete={() => handleDeleteGoal(goal.id)}
+ />
+ ))
+ )}
+
+
+ {/* Completed Goals */}
+ {completedGoals.length > 0 && (
+
+
+ Completed Goals ({completedGoals.length})
+
+ {completedGoals.map((goal) => (
+ handleDeleteGoal(goal.id)}
+ />
+ ))}
+
+ )}
+
+
+
+
+ {/* Floating Action Button */}
+
+ setIsModalVisible(true)}
+ onPressIn={() => {
+ Animated.spring(fabScale, {
+ toValue: 0.9,
+ friction: 8,
+ tension: 100,
+ useNativeDriver: true,
+ }).start();
+ }}
+ onPressOut={() => {
+ Animated.spring(fabScale, {
+ toValue: 1,
+ friction: 8,
+ tension: 100,
+ useNativeDriver: true,
+ }).start();
+ }}
+ activeOpacity={0.9}
+ >
+
+
+
+
+
+
+ {/* Create Goal Modal */}
+ setIsModalVisible(false)}
+ onSubmit={handleCreateGoal}
+ />
+
+ );
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: theme.colors.background,
- },
- scrollContent: {
- paddingBottom: 20,
- },
- header: {
- padding: 24,
- paddingTop: 60,
- paddingBottom: 24,
- marginBottom: 10,
- borderBottomLeftRadius: theme.borderRadius.xl,
- borderBottomRightRadius: theme.borderRadius.xl,
- },
- headerTitle: {
- fontSize: theme.typography.fontSize['3xl'],
- fontWeight: theme.typography.fontWeight.bold,
- color: theme.colors.white,
- },
- headerSubtitle: {
- fontSize: theme.typography.fontSize.base,
- color: "rgba(255, 255, 255, 0.9)",
- marginTop: 4,
- },
- statsContainer: {
- flexDirection: "row",
- padding: 16,
- gap: 12,
- },
- statCard: {
- flex: 1,
- backgroundColor: theme.colors.white,
- padding: 16,
- borderRadius: theme.borderRadius.xl,
- alignItems: 'center',
- ...theme.shadows.medium,
- borderWidth: 1,
- borderColor: "rgba(59, 130, 246, 0.1)",
- },
- statValue: {
- fontSize: theme.typography.fontSize['2xl'],
- fontWeight: theme.typography.fontWeight.bold,
- color: theme.colors.primary,
- marginBottom: 4,
- },
- statLabel: {
- fontSize: 12,
- color: "#6b7280",
- fontWeight: "500",
- },
- section: {
- padding: 20,
- paddingTop: 10,
- },
- sectionTitle: {
- fontSize: 18,
- fontWeight: "600",
- color: "#374151",
- marginBottom: 12,
- },
- emptyState: {
- alignItems: "center",
- paddingVertical: 40,
- },
- emptyText: {
- fontSize: 16,
- fontWeight: "500",
- color: "#6b7280",
- marginTop: 12,
- },
- emptySubtext: {
- fontSize: 14,
- color: "#9ca3af",
- marginTop: 4,
- },
- footer: {
- height: 100,
- },
- fabContainer: {
- position: "absolute",
- right: 20,
- bottom: 110, // Adjusted for tab bar height
- },
- fab: {
- width: 64,
- height: 64,
- borderRadius: 32,
- justifyContent: "center",
- alignItems: "center",
- ...theme.shadows.glow,
- },
+ container: {
+ flex: 1,
+ backgroundColor: theme.colors.background,
+ },
+ scrollContent: {
+ paddingBottom: 20,
+ },
+ header: {
+ padding: 24,
+ paddingTop: 60,
+ paddingBottom: 24,
+ marginBottom: 10,
+ borderBottomLeftRadius: theme.borderRadius.xl,
+ borderBottomRightRadius: theme.borderRadius.xl,
+ },
+ headerContent: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+ debugButton: {
+ padding: 8,
+ },
+ headerTitle: {
+ fontSize: theme.typography.fontSize["3xl"],
+ fontWeight: theme.typography.fontWeight.bold,
+ color: theme.colors.white,
+ },
+ headerSubtitle: {
+ fontSize: theme.typography.fontSize.base,
+ color: "rgba(255, 255, 255, 0.9)",
+ marginTop: 4,
+ },
+ statsContainer: {
+ flexDirection: "row",
+ padding: 16,
+ gap: 12,
+ },
+ statCard: {
+ flex: 1,
+ backgroundColor: theme.colors.white,
+ padding: 16,
+ borderRadius: theme.borderRadius.xl,
+ alignItems: "center",
+ ...theme.shadows.medium,
+ borderWidth: 1,
+ borderColor: "rgba(59, 130, 246, 0.1)",
+ },
+ statValue: {
+ fontSize: theme.typography.fontSize["2xl"],
+ fontWeight: theme.typography.fontWeight.bold,
+ color: theme.colors.primary,
+ marginBottom: 4,
+ },
+ statLabel: {
+ fontSize: 12,
+ color: "#6b7280",
+ fontWeight: "500",
+ },
+ section: {
+ padding: 20,
+ paddingTop: 10,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: "#374151",
+ marginBottom: 12,
+ },
+ emptyState: {
+ alignItems: "center",
+ paddingVertical: 40,
+ },
+ emptyText: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: "#6b7280",
+ marginTop: 12,
+ },
+ emptySubtext: {
+ fontSize: 14,
+ color: "#9ca3af",
+ marginTop: 4,
+ },
+ footer: {
+ height: 100,
+ },
+ fabContainer: {
+ position: "absolute",
+ right: 20,
+ bottom: 110, // Adjusted for tab bar height
+ },
+ fab: {
+ width: 64,
+ height: 64,
+ borderRadius: 32,
+ justifyContent: "center",
+ alignItems: "center",
+ ...theme.shadows.glow,
+ },
});
diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx
index 69181f4..65b4023 100644
--- a/apps/mobile/src/app/_layout.tsx
+++ b/apps/mobile/src/app/_layout.tsx
@@ -2,12 +2,26 @@ import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo";
import { Stack } from "expo-router";
import * as SecureStore from "expo-secure-store";
import { View, Text } from "react-native";
+import { useEffect, useState } from "react";
+
+console.log("========================================");
+console.log("🚀 _layout.tsx loaded at:", new Date().toISOString());
+console.log(
+ "📦 EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY:",
+ process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY,
+);
+console.log("========================================");
// Token cache for Clerk
const tokenCache = {
async getToken(key: string) {
try {
- return SecureStore.getItemAsync(key);
+ const value = await SecureStore.getItemAsync(key);
+ console.log(`[TokenCache] Getting key: ${key}, exists: ${!!value}`);
+ if (value && value.length > 50) {
+ console.log(`[TokenCache] Value preview: ${value.substring(0, 50)}...`);
+ }
+ return value;
} catch (err) {
console.error("Error getting token:", err);
return null;
@@ -15,6 +29,10 @@ const tokenCache = {
},
async saveToken(key: string, value: string) {
try {
+ console.log(`[TokenCache] Saving key: ${key}`);
+ if (value && value.length > 50) {
+ console.log(`[TokenCache] Value preview: ${value.substring(0, 50)}...`);
+ }
return SecureStore.setItemAsync(key, value);
} catch (err) {
console.error("Error saving token:", err);
@@ -24,8 +42,71 @@ const tokenCache = {
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
+ const [cacheCleared, setCacheCleared] = useState(false);
+
+ console.log(
+ "🔑 [RootLayout] Component rendering with publishableKey:",
+ publishableKey,
+ );
+
+ // TEMPORARY: Clear Clerk cache on app start to fix instance mismatch
+ useEffect(() => {
+ console.log("⚡ [RootLayout] useEffect triggered - starting cache cleanup");
+
+ const clearOldClerkCache = async () => {
+ try {
+ console.log("🔍 [RootLayout] Checking for old Clerk tokens...");
+
+ const keysToCheck = [
+ "__clerk_client_jwt",
+ "__clerk_db_jwt",
+ "__clerk_client_uat",
+ "__clerk_session_id",
+ "__clerk_refresh_token",
+ "__clerk_session_jwt",
+ ];
+
+ for (const key of keysToCheck) {
+ try {
+ const value = await SecureStore.getItemAsync(key);
+ if (value) {
+ console.log(`📌 [RootLayout] Found token at ${key}`);
+ console.log(
+ `📝 [RootLayout] Token preview: ${value.substring(0, 80)}...`,
+ );
+
+ // Check if it's from the old instance
+ if (value.includes("pleasing-pheasant-20")) {
+ console.log(`🗑️ [RootLayout] DELETING old token from ${key}`);
+ await SecureStore.deleteItemAsync(key);
+ } else if (value.includes("needed-elephant-64")) {
+ console.log(
+ `✅ [RootLayout] Token is from correct instance (needed-elephant-64)`,
+ );
+ } else {
+ console.log(
+ `⚠️ [RootLayout] Token doesn't match known instances`,
+ );
+ }
+ }
+ } catch (e) {
+ console.log(`❌ [RootLayout] Error checking key ${key}:`, e);
+ }
+ }
+
+ console.log("✅ [RootLayout] Old token cleanup complete");
+ setCacheCleared(true);
+ } catch (error) {
+ console.error("❌ [RootLayout] Error clearing old cache:", error);
+ setCacheCleared(true); // Continue anyway
+ }
+ };
+
+ clearOldClerkCache();
+ }, []);
if (!publishableKey) {
+ console.log("❌ [RootLayout] No publishable key found!");
return (
Missing Clerk Publishable Key
@@ -38,6 +119,23 @@ export default function RootLayout() {
);
}
+ if (!cacheCleared) {
+ console.log("⏳ [RootLayout] Waiting for cache to clear...");
+ return (
+
+ Clearing old authentication cache...
+
+ Check the terminal for logs...
+
+
+ );
+ }
+
+ console.log(
+ "🚀 [RootLayout] Rendering ClerkProvider with key:",
+ publishableKey?.substring(0, 20) + "...",
+ );
+
return (