routes fix
extensive loging added
This commit is contained in:
parent
36315ea5cf
commit
22c274bb83
366
AGENTS.md
Normal file
366
AGENTS.md
Normal file
@ -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 <div>...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI library components - Named export (forwardRef constant)
|
||||||
|
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("rounded-lg", className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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 <button onClick={handleClick}>Click</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<div className="flex items-center gap-2 rounded-lg border p-4">
|
||||||
|
<Button className="bg-primary text-white hover:bg-primary/90">
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mobile App**: StyleSheet.create() at file bottom with theme system
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { theme } from '../styles/theme'
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
return <View style={styles.container}>...</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@ -1,39 +1,62 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from '@clerk/nextjs/server';
|
import { auth } from "@clerk/nextjs/server";
|
||||||
import { getDatabase } from '@/lib/database';
|
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
|
// POST - Mark goal as complete
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
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 });
|
|
||||||
}
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,119 +1,163 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from '@clerk/nextjs/server';
|
import { auth } from "@clerk/nextjs/server";
|
||||||
import { getDatabase } from '@/lib/database';
|
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
|
// GET - Get specific goal
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// PUT - Update goal
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
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 });
|
|
||||||
}
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// DELETE - Delete goal
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
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 });
|
|
||||||
}
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,95 +1,141 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from '@clerk/nextjs/server';
|
import { auth } from "@clerk/nextjs/server";
|
||||||
import { getDatabase } from '@/lib/database';
|
import { getDatabase } from "@/lib/database";
|
||||||
import { randomBytes } from 'crypto';
|
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<string | null> {
|
||||||
|
// 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
|
// GET - List user's fitness goals
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const userId = await getAuthenticatedUserId(req);
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url);
|
if (!userId) {
|
||||||
const targetUserId = searchParams.get('userId') || userId;
|
console.error("[Fitness Goals API] Authentication failed");
|
||||||
const status = searchParams.get('status'); // active, completed, all
|
console.error(
|
||||||
|
"[Fitness Goals API] Headers:",
|
||||||
const db = await getDatabase();
|
Object.fromEntries(req.headers.entries()),
|
||||||
|
);
|
||||||
// Fetch goals with optional status filter
|
return NextResponse.json(
|
||||||
const goals = await db.getFitnessGoalsByUserId(
|
{ error: "Unauthorized" },
|
||||||
targetUserId,
|
{ status: 401, headers: corsHeaders() },
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// POST - Create new fitness goal
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const userId = await getAuthenticatedUserId(req);
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await req.json();
|
if (!userId) {
|
||||||
const {
|
console.error("[Fitness Goals API] Authentication failed for POST");
|
||||||
goalType,
|
return NextResponse.json(
|
||||||
title,
|
{ error: "Unauthorized" },
|
||||||
description,
|
{ status: 401, headers: corsHeaders() },
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
apps/admin/src/app/api/test/route.ts
Normal file
5
apps/admin/src/app/api/test/route.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ message: "Test endpoint works!" });
|
||||||
|
}
|
||||||
47
apps/admin/src/lib/auth-helper.ts
Normal file
47
apps/admin/src/lib/auth-helper.ts
Normal file
@ -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 <token>
|
||||||
|
*/
|
||||||
|
export async function getAuthUserId(req: NextRequest): Promise<string | null> {
|
||||||
|
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<string> {
|
||||||
|
const userId = await getAuthUserId(req);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
@ -1,30 +1,43 @@
|
|||||||
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
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([
|
const isPublicRoute = createRouteMatcher([
|
||||||
"/sign-in(.*)",
|
"/sign-in(.*)",
|
||||||
"/sign-up(.*)",
|
"/sign-up(.*)",
|
||||||
"/api/webhooks(.*)",
|
"/api/webhooks(.*)",
|
||||||
"/api/attendance(.*)",
|
"/api/health(.*)",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Define routes that require authentication
|
// Define API routes that need auth but should be handled in the route itself
|
||||||
const isProtectedRoute = createRouteMatcher([
|
// This prevents auth.protect() from blocking before the route handler runs
|
||||||
"/",
|
const isApiRoute = createRouteMatcher(["/api/(.*)"]);
|
||||||
"/users(.*)",
|
|
||||||
"/analytics(.*)",
|
|
||||||
"/profile(.*)",
|
|
||||||
"/api/users(.*)",
|
|
||||||
"/api/profile(.*)",
|
|
||||||
"/api/payments(.*)",
|
|
||||||
"/api/notifications(.*)",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default clerkMiddleware(async (auth, req) => {
|
export default clerkMiddleware(async (auth, req) => {
|
||||||
// Protect all routes except public ones
|
// Log for debugging
|
||||||
if (!isPublicRoute(req)) {
|
const authHeader = req.headers.get("authorization");
|
||||||
await auth.protect();
|
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 = {
|
export const config = {
|
||||||
|
|||||||
@ -1,290 +1,392 @@
|
|||||||
import React, { useState, useCallback, useRef } from 'react';
|
import React, { useState, useCallback, useRef } from "react";
|
||||||
import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native';
|
import {
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
View,
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
Text,
|
||||||
import { theme } from '../../styles/theme';
|
StyleSheet,
|
||||||
import { GoalProgressCard } from '../../components/GoalProgressCard';
|
ScrollView,
|
||||||
import { GoalCreationModal } from '../../components/GoalCreationModal';
|
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 { useUser, useAuth } from "@clerk/clerk-expo";
|
||||||
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals';
|
import {
|
||||||
import { useFocusEffect } from 'expo-router';
|
fitnessGoalsService,
|
||||||
|
type FitnessGoal,
|
||||||
|
type CreateGoalData,
|
||||||
|
} from "../../services/fitnessGoals";
|
||||||
|
import { useFocusEffect } from "expo-router";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
|
||||||
export default function GoalsScreen() {
|
export default function GoalsScreen() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { getToken } = useAuth();
|
const { getToken } = useAuth();
|
||||||
const [goals, setGoals] = useState<FitnessGoal[]>([]);
|
const [goals, setGoals] = useState<FitnessGoal[]>([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const fabScale = useRef(new Animated.Value(1)).current;
|
const fabScale = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
const loadGoals = useCallback(async () => {
|
const loadGoals = useCallback(async () => {
|
||||||
if (!user?.id) return;
|
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 {
|
try {
|
||||||
const token = await getToken();
|
const parts = token.split(".");
|
||||||
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
|
if (parts.length === 3) {
|
||||||
setGoals(loadedGoals);
|
const payload = JSON.parse(atob(parts[1]));
|
||||||
} catch (error) {
|
console.log("Token issuer:", payload.iss);
|
||||||
console.error('Error loading goals:', error);
|
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(
|
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
|
||||||
useCallback(() => {
|
setGoals(loadedGoals);
|
||||||
loadGoals();
|
} catch (error) {
|
||||||
}, [loadGoals])
|
console.error("Error loading goals:", error);
|
||||||
);
|
}
|
||||||
|
}, [user?.id, getToken]);
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const clearClerkCache = async () => {
|
||||||
setRefreshing(true);
|
Alert.alert(
|
||||||
await loadGoals();
|
"Clear Clerk Cache",
|
||||||
setRefreshing(false);
|
"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) => {
|
for (const key of keysToDelete) {
|
||||||
const token = await getToken();
|
try {
|
||||||
await fitnessGoalsService.createGoal(newGoal, token);
|
await SecureStore.deleteItemAsync(key);
|
||||||
await loadGoals();
|
} catch (e) {
|
||||||
setIsModalVisible(false);
|
// Key might not exist
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<ScrollView
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.header}
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.headerTitle}>Fitness Goals</Text>
|
|
||||||
<Text style={styles.headerSubtitle}>
|
|
||||||
Track your fitness journey progress
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{/* Stats Summary */}
|
Alert.alert(
|
||||||
{goals.length > 0 && (
|
"Success",
|
||||||
<View style={styles.statsContainer}>
|
"Cache cleared! Please sign out and sign back in.",
|
||||||
<View style={styles.statCard}>
|
);
|
||||||
<Text style={styles.statValue}>{activeGoals.length}</Text>
|
} catch (error) {
|
||||||
<Text style={styles.statLabel}>Active</Text>
|
console.error("Error clearing cache:", error);
|
||||||
</View>
|
Alert.alert("Error", "Failed to clear cache");
|
||||||
<View style={styles.statCard}>
|
}
|
||||||
<Text style={styles.statValue}>{completedGoals.length}</Text>
|
},
|
||||||
<Text style={styles.statLabel}>Completed</Text>
|
},
|
||||||
</View>
|
],
|
||||||
<View style={styles.statCard}>
|
|
||||||
<Text style={styles.statValue}>
|
|
||||||
{activeGoals.length > 0
|
|
||||||
? Math.round(
|
|
||||||
activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
|
|
||||||
activeGoals.length
|
|
||||||
)
|
|
||||||
: 0}%
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.statLabel}>Avg Progress</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Active Goals */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>
|
|
||||||
Active Goals ({activeGoals.length})
|
|
||||||
</Text>
|
|
||||||
{activeGoals.length === 0 ? (
|
|
||||||
<View style={styles.emptyState}>
|
|
||||||
<Ionicons name="flag-outline" size={48} color="#d1d5db" />
|
|
||||||
<Text style={styles.emptyText}>No active goals yet</Text>
|
|
||||||
<Text style={styles.emptySubtext}>
|
|
||||||
Tap the + button to create your first goal
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
activeGoals.map((goal) => (
|
|
||||||
<GoalProgressCard
|
|
||||||
key={goal.id}
|
|
||||||
goal={goal}
|
|
||||||
onComplete={() => handleCompleteGoal(goal)}
|
|
||||||
onDelete={() => handleDeleteGoal(goal.id)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Completed Goals */}
|
|
||||||
{completedGoals.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>
|
|
||||||
Completed Goals ({completedGoals.length})
|
|
||||||
</Text>
|
|
||||||
{completedGoals.map((goal) => (
|
|
||||||
<GoalProgressCard
|
|
||||||
key={goal.id}
|
|
||||||
goal={goal}
|
|
||||||
onDelete={() => handleDeleteGoal(goal.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.footer} />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* Floating Action Button */}
|
|
||||||
<Animated.View style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => 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}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.fab}
|
|
||||||
>
|
|
||||||
<Ionicons name="add" size={28} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
{/* Create Goal Modal */}
|
|
||||||
<GoalCreationModal
|
|
||||||
visible={isModalVisible}
|
|
||||||
onClose={() => setIsModalVisible(false)}
|
|
||||||
onSubmit={handleCreateGoal}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LinearGradient colors={theme.gradients.primary} style={styles.header}>
|
||||||
|
<View style={styles.headerContent}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.headerTitle}>Fitness Goals</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>
|
||||||
|
Track your fitness journey progress
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={clearClerkCache}
|
||||||
|
style={styles.debugButton}
|
||||||
|
>
|
||||||
|
<Ionicons name="refresh-circle-outline" size={24} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</LinearGradient>
|
||||||
|
|
||||||
|
{/* Stats Summary */}
|
||||||
|
{goals.length > 0 && (
|
||||||
|
<View style={styles.statsContainer}>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statValue}>{activeGoals.length}</Text>
|
||||||
|
<Text style={styles.statLabel}>Active</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statValue}>{completedGoals.length}</Text>
|
||||||
|
<Text style={styles.statLabel}>Completed</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Text style={styles.statValue}>
|
||||||
|
{activeGoals.length > 0
|
||||||
|
? Math.round(
|
||||||
|
activeGoals.reduce(
|
||||||
|
(sum, g) => sum + (g.progress || 0),
|
||||||
|
0,
|
||||||
|
) / activeGoals.length,
|
||||||
|
)
|
||||||
|
: 0}
|
||||||
|
%
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.statLabel}>Avg Progress</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Active Goals */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
Active Goals ({activeGoals.length})
|
||||||
|
</Text>
|
||||||
|
{activeGoals.length === 0 ? (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Ionicons name="flag-outline" size={48} color="#d1d5db" />
|
||||||
|
<Text style={styles.emptyText}>No active goals yet</Text>
|
||||||
|
<Text style={styles.emptySubtext}>
|
||||||
|
Tap the + button to create your first goal
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
activeGoals.map((goal) => (
|
||||||
|
<GoalProgressCard
|
||||||
|
key={goal.id}
|
||||||
|
goal={goal}
|
||||||
|
onComplete={() => handleCompleteGoal(goal)}
|
||||||
|
onDelete={() => handleDeleteGoal(goal.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Completed Goals */}
|
||||||
|
{completedGoals.length > 0 && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
Completed Goals ({completedGoals.length})
|
||||||
|
</Text>
|
||||||
|
{completedGoals.map((goal) => (
|
||||||
|
<GoalProgressCard
|
||||||
|
key={goal.id}
|
||||||
|
goal={goal}
|
||||||
|
onDelete={() => handleDeleteGoal(goal.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.footer} />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Floating Action Button */}
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => 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}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={theme.gradients.primary}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.fab}
|
||||||
|
>
|
||||||
|
<Ionicons name="add" size={28} color="#fff" />
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Create Goal Modal */}
|
||||||
|
<GoalCreationModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
onClose={() => setIsModalVisible(false)}
|
||||||
|
onSubmit={handleCreateGoal}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: 24,
|
padding: 24,
|
||||||
paddingTop: 60,
|
paddingTop: 60,
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
borderBottomLeftRadius: theme.borderRadius.xl,
|
||||||
borderBottomRightRadius: theme.borderRadius.xl,
|
borderBottomRightRadius: theme.borderRadius.xl,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerContent: {
|
||||||
fontSize: theme.typography.fontSize['3xl'],
|
flexDirection: "row",
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
justifyContent: "space-between",
|
||||||
color: theme.colors.white,
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
headerSubtitle: {
|
debugButton: {
|
||||||
fontSize: theme.typography.fontSize.base,
|
padding: 8,
|
||||||
color: "rgba(255, 255, 255, 0.9)",
|
},
|
||||||
marginTop: 4,
|
headerTitle: {
|
||||||
},
|
fontSize: theme.typography.fontSize["3xl"],
|
||||||
statsContainer: {
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
flexDirection: "row",
|
color: theme.colors.white,
|
||||||
padding: 16,
|
},
|
||||||
gap: 12,
|
headerSubtitle: {
|
||||||
},
|
fontSize: theme.typography.fontSize.base,
|
||||||
statCard: {
|
color: "rgba(255, 255, 255, 0.9)",
|
||||||
flex: 1,
|
marginTop: 4,
|
||||||
backgroundColor: theme.colors.white,
|
},
|
||||||
padding: 16,
|
statsContainer: {
|
||||||
borderRadius: theme.borderRadius.xl,
|
flexDirection: "row",
|
||||||
alignItems: 'center',
|
padding: 16,
|
||||||
...theme.shadows.medium,
|
gap: 12,
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "rgba(59, 130, 246, 0.1)",
|
statCard: {
|
||||||
},
|
flex: 1,
|
||||||
statValue: {
|
backgroundColor: theme.colors.white,
|
||||||
fontSize: theme.typography.fontSize['2xl'],
|
padding: 16,
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
borderRadius: theme.borderRadius.xl,
|
||||||
color: theme.colors.primary,
|
alignItems: "center",
|
||||||
marginBottom: 4,
|
...theme.shadows.medium,
|
||||||
},
|
borderWidth: 1,
|
||||||
statLabel: {
|
borderColor: "rgba(59, 130, 246, 0.1)",
|
||||||
fontSize: 12,
|
},
|
||||||
color: "#6b7280",
|
statValue: {
|
||||||
fontWeight: "500",
|
fontSize: theme.typography.fontSize["2xl"],
|
||||||
},
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
section: {
|
color: theme.colors.primary,
|
||||||
padding: 20,
|
marginBottom: 4,
|
||||||
paddingTop: 10,
|
},
|
||||||
},
|
statLabel: {
|
||||||
sectionTitle: {
|
fontSize: 12,
|
||||||
fontSize: 18,
|
color: "#6b7280",
|
||||||
fontWeight: "600",
|
fontWeight: "500",
|
||||||
color: "#374151",
|
},
|
||||||
marginBottom: 12,
|
section: {
|
||||||
},
|
padding: 20,
|
||||||
emptyState: {
|
paddingTop: 10,
|
||||||
alignItems: "center",
|
},
|
||||||
paddingVertical: 40,
|
sectionTitle: {
|
||||||
},
|
fontSize: 18,
|
||||||
emptyText: {
|
fontWeight: "600",
|
||||||
fontSize: 16,
|
color: "#374151",
|
||||||
fontWeight: "500",
|
marginBottom: 12,
|
||||||
color: "#6b7280",
|
},
|
||||||
marginTop: 12,
|
emptyState: {
|
||||||
},
|
alignItems: "center",
|
||||||
emptySubtext: {
|
paddingVertical: 40,
|
||||||
fontSize: 14,
|
},
|
||||||
color: "#9ca3af",
|
emptyText: {
|
||||||
marginTop: 4,
|
fontSize: 16,
|
||||||
},
|
fontWeight: "500",
|
||||||
footer: {
|
color: "#6b7280",
|
||||||
height: 100,
|
marginTop: 12,
|
||||||
},
|
},
|
||||||
fabContainer: {
|
emptySubtext: {
|
||||||
position: "absolute",
|
fontSize: 14,
|
||||||
right: 20,
|
color: "#9ca3af",
|
||||||
bottom: 110, // Adjusted for tab bar height
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
fab: {
|
footer: {
|
||||||
width: 64,
|
height: 100,
|
||||||
height: 64,
|
},
|
||||||
borderRadius: 32,
|
fabContainer: {
|
||||||
justifyContent: "center",
|
position: "absolute",
|
||||||
alignItems: "center",
|
right: 20,
|
||||||
...theme.shadows.glow,
|
bottom: 110, // Adjusted for tab bar height
|
||||||
},
|
},
|
||||||
|
fab: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
...theme.shadows.glow,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,12 +2,26 @@ import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo";
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { View, Text } from "react-native";
|
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
|
// Token cache for Clerk
|
||||||
const tokenCache = {
|
const tokenCache = {
|
||||||
async getToken(key: string) {
|
async getToken(key: string) {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error("Error getting token:", err);
|
console.error("Error getting token:", err);
|
||||||
return null;
|
return null;
|
||||||
@ -15,6 +29,10 @@ const tokenCache = {
|
|||||||
},
|
},
|
||||||
async saveToken(key: string, value: string) {
|
async saveToken(key: string, value: string) {
|
||||||
try {
|
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);
|
return SecureStore.setItemAsync(key, value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error saving token:", err);
|
console.error("Error saving token:", err);
|
||||||
@ -24,8 +42,71 @@ const tokenCache = {
|
|||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
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) {
|
if (!publishableKey) {
|
||||||
|
console.log("❌ [RootLayout] No publishable key found!");
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||||
<Text>Missing Clerk Publishable Key</Text>
|
<Text>Missing Clerk Publishable Key</Text>
|
||||||
@ -38,6 +119,23 @@ export default function RootLayout() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cacheCleared) {
|
||||||
|
console.log("⏳ [RootLayout] Waiting for cache to clear...");
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||||
|
<Text>Clearing old authentication cache...</Text>
|
||||||
|
<Text style={{ marginTop: 8, fontSize: 12, color: "#666" }}>
|
||||||
|
Check the terminal for logs...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"🚀 [RootLayout] Rendering ClerkProvider with key:",
|
||||||
|
publishableKey?.substring(0, 20) + "...",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
|
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
|
||||||
<ClerkLoaded>
|
<ClerkLoaded>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user