From f04e85a6bf35ed6bea1ee898e629ba9979172686 Mon Sep 17 00:00:00 2001 From: dhollifield Date: Sun, 23 Nov 2025 15:43:22 -0600 Subject: [PATCH] added signup-login-logout functionality --- .gitignore | 1 + backend/main.py | 145 ++- docker-compose.yml | 2 +- frontend/App.tsx | 158 ++- frontend/package-lock.json | 1141 +++++++++++++++++++ frontend/package.json | 5 + frontend/src/components/DashboardScreen.tsx | 38 + frontend/src/components/LoginScreen.tsx | 230 ++++ frontend/src/components/SignupScreen.tsx | 373 ++++++ frontend/src/config/firebase.ts | 21 + frontend/src/services/authService.ts | 174 +++ frontend/src/utils/passwordValidation.ts | 57 + package-lock.json | 6 + 13 files changed, 2300 insertions(+), 51 deletions(-) create mode 100644 frontend/src/components/DashboardScreen.tsx create mode 100644 frontend/src/components/LoginScreen.tsx create mode 100644 frontend/src/components/SignupScreen.tsx create mode 100644 frontend/src/config/firebase.ts create mode 100644 frontend/src/services/authService.ts create mode 100644 frontend/src/utils/passwordValidation.ts create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 7b4ff1d..db81cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ firebase-debug.log .firebase/ serviceAccountKey.json firebase-admin-sdk.json +firebase-service-account.json # Jupyter Notebooks .ipynb_checkpoints/ diff --git a/backend/main.py b/backend/main.py index 01eee81..be5c583 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,39 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Header from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel -from typing import Dict +from typing import Dict, Optional +import firebase_admin +from firebase_admin import credentials, auth as firebase_auth +from sqlalchemy import create_engine, Column, String, DateTime, text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import sessionmaker, declarative_base +from datetime import datetime +import uuid +import os + +# Initialize Firebase Admin SDK +try: + firebase_admin.get_app() +except ValueError: + # Initialize with service account credentials + cred = credentials.Certificate('firebase-service-account.json') + firebase_admin.initialize_app(cred) + +# Database setup +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://app_user:app_password@postgres:5432/app_db") +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# User model +class User(Base): + __tablename__ = "user" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + firebase_uid = Column(String(128), unique=True, nullable=False) + email = Column(String(254), nullable=True) + display_name = Column(String(100), nullable=True) + created_at = Column(DateTime, server_default=text('CURRENT_TIMESTAMP')) app = FastAPI(title="Backend API", version="1.0.0") @@ -18,6 +50,16 @@ class HealthResponse(BaseModel): message: str service: str +class UserInitRequest(BaseModel): + id_token: str + +class UserResponse(BaseModel): + id: str + firebase_uid: str + email: Optional[str] + display_name: Optional[str] + created_at: str + @app.get("/") async def root() -> Dict[str, str]: return {"message": "Backend is running!"} @@ -28,4 +70,101 @@ async def health_check() -> HealthResponse: status="connected", message="Backend is operational", service="FastAPI Backend" - ) \ No newline at end of file + ) + +@app.get("/users/me", response_model=UserResponse) +async def get_current_user(authorization: Optional[str] = Header(None)): + """ + Get the current user's data from the database using their Firebase ID token. + Expects: Authorization: Bearer + """ + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing or invalid authorization header") + + id_token = authorization.split("Bearer ")[1] + db = SessionLocal() + + try: + # Verify the Firebase ID token + try: + decoded_token = firebase_auth.verify_id_token(id_token) + firebase_uid = decoded_token['uid'] + except Exception as e: + raise HTTPException(status_code=401, detail=f"Invalid Firebase token: {str(e)}") + + # Fetch user from database + user = db.query(User).filter(User.firebase_uid == firebase_uid).first() + + if not user: + raise HTTPException(status_code=404, detail="User not found in database") + + return UserResponse( + id=str(user.id), + firebase_uid=user.firebase_uid, + email=user.email, + display_name=user.display_name, + created_at=user.created_at.isoformat() if user.created_at else "" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to fetch user: {str(e)}") + finally: + db.close() + +@app.post("/users/init", response_model=UserResponse) +async def initialize_user(request: UserInitRequest): + """ + Initialize a new user in the database after Firebase signup. + Verifies the Firebase ID token and creates a user record in Postgres. + """ + db = SessionLocal() + try: + # Verify the Firebase ID token + try: + decoded_token = firebase_auth.verify_id_token(request.id_token) + firebase_uid = decoded_token['uid'] + email = decoded_token.get('email') + except Exception as e: + raise HTTPException(status_code=401, detail=f"Invalid Firebase token: {str(e)}") + + # Check if user already exists + existing_user = db.query(User).filter(User.firebase_uid == firebase_uid).first() + if existing_user: + # User already exists, return existing user data + return UserResponse( + id=str(existing_user.id), + firebase_uid=existing_user.firebase_uid, + email=existing_user.email, + display_name=existing_user.display_name, + created_at=existing_user.created_at.isoformat() if existing_user.created_at else "" + ) + + # Create new user in database + new_user = User( + firebase_uid=firebase_uid, + email=email, + display_name=email.split('@')[0] if email else None # Use email prefix as default display name + ) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + return UserResponse( + id=str(new_user.id), + firebase_uid=new_user.firebase_uid, + email=new_user.email, + display_name=new_user.display_name, + created_at=new_user.created_at.isoformat() if new_user.created_at else "" + ) + + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to create user: {str(e)}") + finally: + db.close() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0e8abaf..b5415a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: postgres: image: postgres:17-alpine ports: - - "5432:5432" + - "5433:5432" environment: - POSTGRES_USER=app_user - POSTGRES_PASSWORD=app_password diff --git a/frontend/App.tsx b/frontend/App.tsx index 4c4b4e0..0e874e2 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -1,62 +1,126 @@ import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; -import { useEffect, useState } from 'react'; -import axios from 'axios'; +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import React from 'react'; +import { TouchableOpacity, Text } from 'react-native'; +import LoginScreen from './src/components/LoginScreen'; +import SignupScreen from './src/components/SignupScreen'; +import DashboardScreen from './src/components/DashboardScreen'; +import { signUpUser, signInUser, resetPassword, logoutUser } from './src/services/authService'; -const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000'; +export type RootStackParamList = { + Login: undefined; + Signup: undefined; + Dashboard: undefined; +}; + +const Stack = createNativeStackNavigator(); export default function App() { - const [connectionStatus, setConnectionStatus] = useState(''); - const [isConnected, setIsConnected] = useState(false); + const handleLogin = async (email: string, password: string) => { + try { + // Sign in user with Firebase and fetch from database + const userData = await signInUser(email, password); + console.log('User logged in successfully:', userData); + + // Navigation will happen automatically after successful login + } catch (error: any) { + // Error is already formatted by authService + throw error; + } + }; - useEffect(() => { - checkBackendConnection(); - }, []); + const handleSignup = async (email: string, password: string) => { + try { + // Sign up user with Firebase and initialize in database + const userData = await signUpUser(email, password); + console.log('User signed up successfully:', userData); + + // Navigation will happen automatically after successful signup + // The user is already auto-logged in by Firebase's createUserWithEmailAndPassword + } catch (error: any) { + // Error is already formatted by authService + throw error; + } + }; - const checkBackendConnection = async () => { + const handlePasswordReset = async (email: string) => { try { - const response = await axios.get(`${API_URL}/api/health`); - if (response.data.status === 'connected') { - setConnectionStatus('You are connected to the backend!'); - setIsConnected(true); + await resetPassword(email); + } catch (error: any) { + // Error is already formatted by authService + throw error; + } + }; + + const handleLogout = async () => { + try { + await logoutUser(); + console.log('User logged out successfully'); + // Navigate to login after logout + if (navigationRef.current) { + navigationRef.current.reset({ + index: 0, + routes: [{ name: 'Login' }], + }); } - } catch (error) { - setConnectionStatus('Unable to connect to backend'); - setIsConnected(false); + } catch (error: any) { + console.error('Logout failed:', error); + throw error; } }; + const navigationRef = React.useRef(null); + return ( - - Hello World - - {connectionStatus} - + + + + {(props) => ( + + )} + + + + {(props) => } + + + ( + + Log Out + + ), + }} + component={DashboardScreen} + /> + - + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - marginBottom: 20, - }, - status: { - fontSize: 18, - marginTop: 10, - }, - connected: { - color: 'green', - }, - disconnected: { - color: 'red', - }, -}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index df1079a..3eb214b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,12 +9,17 @@ "version": "1.0.0", "dependencies": { "@expo/metro-runtime": "~6.1.2", + "@react-navigation/native": "^7.1.19", + "@react-navigation/native-stack": "^7.6.2", "axios": "^1.12.2", "expo": "~54.0.12", "expo-status-bar": "~3.0.8", + "firebase": "^12.5.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", + "react-native-safe-area-context": "^5.6.2", + "react-native-screens": "^4.18.0", "react-native-web": "^0.21.0" }, "devDependencies": { @@ -2238,6 +2243,645 @@ "node": ">=8" } }, + "node_modules/@firebase/ai": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.5.0.tgz", + "integrity": "sha512-OXv/jZLRjV9jTejWA4KOvW8gM1hNsLvQSCPwKhi2CEfe0Nap3rM6z+Ial0PGqXga0WgzhpypEvJOFvaAUFX3kg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.19", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", + "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", + "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.5.tgz", + "integrity": "sha512-zyNY77xJOGwcuB+xCxF8z8lSiHvD4ox7BCsqLEHEvgqQoRjxFZ0fkROR6NV5QyXmCqRLodMM8J5d2EStOocWIw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.5.tgz", + "integrity": "sha512-lVG/nRnXaot0rQSZazmTNqy83ti9O3+kdwoaE0d5wahRIWNoDirbIMcGVjDDgdmf4IE6FYreWOMh0L3DV1475w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.5", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", + "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2583,6 +3227,70 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@react-native/assets-registry": { "version": "0.81.4", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz", @@ -2830,6 +3538,123 @@ } } }, + "node_modules/@react-navigation/core": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.0.tgz", + "integrity": "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.1", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.1.tgz", + "integrity": "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.19", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.19.tgz", + "integrity": "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.13.0", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.6.2.tgz", + "integrity": "sha512-CB6chGNLwJYiyOeyCNUKx33yT7XJSwRZIeKHf4S1vs+Oqu3u9zMnvGUIsesNgbgX0xy16gBqYsrWgr0ZczBTtA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.8.1", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.19", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/native/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz", + "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3807,6 +4632,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3820,6 +4658,34 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4010,6 +4876,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4851,11 +5726,29 @@ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4902,6 +5795,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -4944,6 +5846,42 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.5.0.tgz", + "integrity": "sha512-Ak8JcpH7FL6kiv0STwkv5+3CYEROO9iFWSx7OCZVvc4kIIABAIyAGs1mPGaHRxGUIApFZdMCXA7baq17uS6Mow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.5.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.5", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.5", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", @@ -5254,6 +6192,12 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5271,6 +6215,12 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6222,6 +7172,12 @@ "node": ">=8" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6243,6 +7199,12 @@ "node": ">=4" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7223,6 +8185,30 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7244,6 +8230,24 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -7302,6 +8306,18 @@ "react": "^19.1.0" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7372,6 +8388,30 @@ "react-native": "*" } }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", + "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz", + "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz", @@ -7813,6 +8853,15 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sf-symbols-typescript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.1.0.tgz", + "integrity": "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7864,6 +8913,21 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -7918,6 +8982,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7974,6 +9047,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -8336,6 +9418,12 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8487,6 +9575,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8532,6 +9638,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -8540,6 +9652,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", @@ -8548,6 +9666,29 @@ "node": ">=8" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", diff --git a/frontend/package.json b/frontend/package.json index 43b8bf4..9b0fc5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,12 +10,17 @@ }, "dependencies": { "@expo/metro-runtime": "~6.1.2", + "@react-navigation/native": "^7.1.19", + "@react-navigation/native-stack": "^7.6.2", "axios": "^1.12.2", "expo": "~54.0.12", "expo-status-bar": "~3.0.8", + "firebase": "^12.5.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", + "react-native-safe-area-context": "^5.6.2", + "react-native-screens": "^4.18.0", "react-native-web": "^0.21.0" }, "devDependencies": { diff --git a/frontend/src/components/DashboardScreen.tsx b/frontend/src/components/DashboardScreen.tsx new file mode 100644 index 0000000..12c0ad0 --- /dev/null +++ b/frontend/src/components/DashboardScreen.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; + +export default function DashboardScreen() { + return ( + + Dashboard + Welcome to Code Coven! + Your mood tracking and music recommendation dashboard will appear here. + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#ffffff', + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + title: { + fontSize: 32, + fontWeight: 'bold', + marginBottom: 10, + color: '#333333', + }, + subtitle: { + fontSize: 20, + marginBottom: 20, + color: '#666666', + }, + message: { + fontSize: 16, + textAlign: 'center', + color: '#999999', + }, +}); diff --git a/frontend/src/components/LoginScreen.tsx b/frontend/src/components/LoginScreen.tsx new file mode 100644 index 0000000..5d591d9 --- /dev/null +++ b/frontend/src/components/LoginScreen.tsx @@ -0,0 +1,230 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + ActivityIndicator, + Alert, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../../App'; + +type LoginScreenNavigationProp = NativeStackNavigationProp; + +interface LoginScreenProps { + onLogin: (email: string, password: string) => Promise; + onPasswordReset: (email: string) => Promise; +} + +export default function LoginScreen({ onLogin, onPasswordReset }: LoginScreenProps) { + const navigation = useNavigation(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleLogin = async () => { + // Clear previous errors + setError(''); + + // Validate email + if (!email || !email.includes('@')) { + setError('Please enter a valid email address'); + return; + } + + // Validate password + if (!password) { + setError('Please enter your password'); + return; + } + + setLoading(true); + try { + await onLogin(email, password); + // Login successful - navigate to dashboard + navigation.replace('Dashboard'); + } catch (err: any) { + setError(err.message || 'Login failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleForgotPassword = async () => { + // Validate email + if (!email || !email.includes('@')) { + setError('Please enter your email address to reset your password'); + return; + } + + setLoading(true); + setError(''); + + try { + await onPasswordReset(email); + Alert.alert( + 'Password Reset Email Sent', + `A password reset link has been sent to ${email}. Please check your inbox.`, + [{ text: 'OK' }] + ); + } catch (err: any) { + setError(err.message || 'Failed to send password reset email.'); + } finally { + setLoading(false); + } + }; + + const navigateToSignup = () => { + navigation.navigate('Signup'); + }; + + return ( + + Welcome Back + Sign in to continue + + {/* Email Input */} + + Email + + + + {/* Password Input */} + + Password + + + + {/* Forgot Password Link */} + + Forgot Password? + + + {/* Error Message */} + {error && {error}} + + {/* Login Button */} + + {loading ? ( + + ) : ( + Sign In + )} + + + {/* Sign Up Link */} + + Don't have an account? + + Sign Up + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + padding: 20, + backgroundColor: '#ffffff', + justifyContent: 'center', + }, + title: { + fontSize: 32, + fontWeight: 'bold', + marginBottom: 10, + textAlign: 'center', + }, + subtitle: { + fontSize: 16, + color: '#666666', + marginBottom: 30, + textAlign: 'center', + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + color: '#333333', + }, + input: { + borderWidth: 1, + borderColor: '#cccccc', + borderRadius: 8, + padding: 12, + fontSize: 16, + backgroundColor: '#f9f9f9', + }, + forgotPassword: { + color: '#007AFF', + fontSize: 14, + textAlign: 'right', + marginBottom: 20, + }, + errorMessage: { + color: '#ff4444', + fontSize: 14, + textAlign: 'center', + marginBottom: 10, + padding: 10, + backgroundColor: '#fff0f0', + borderRadius: 8, + }, + button: { + backgroundColor: '#007AFF', + padding: 16, + borderRadius: 8, + alignItems: 'center', + marginTop: 10, + }, + buttonDisabled: { + backgroundColor: '#cccccc', + }, + buttonText: { + color: '#ffffff', + fontSize: 18, + fontWeight: '600', + }, + signupContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 20, + }, + signupText: { + fontSize: 14, + color: '#666666', + }, + signupLink: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, +}); diff --git a/frontend/src/components/SignupScreen.tsx b/frontend/src/components/SignupScreen.tsx new file mode 100644 index 0000000..551db66 --- /dev/null +++ b/frontend/src/components/SignupScreen.tsx @@ -0,0 +1,373 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + ActivityIndicator, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../../App'; +import { validatePassword, validatePasswordMatch } from '../utils/passwordValidation'; + +type SignupScreenNavigationProp = NativeStackNavigationProp; + +interface SignupScreenProps { + onSignup: (email: string, password: string) => Promise; +} + +export default function SignupScreen({ onSignup }: SignupScreenProps) { + const navigation = useNavigation(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [acceptedTerms, setAcceptedTerms] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const passwordValidation = validatePassword(password); + const passwordsMatch = validatePasswordMatch(password, confirmPassword); + + const handleSignup = async () => { + // Clear previous errors + setError(''); + + // Validate email + if (!email || !email.includes('@')) { + setError('Please enter a valid email address'); + return; + } + + // Validate password + if (!passwordValidation.isValid) { + setError('Please fix password requirements'); + return; + } + + // Validate password match + if (!passwordsMatch) { + setError('Passwords do not match'); + return; + } + + // Validate terms acceptance + if (!acceptedTerms) { + setError('You must accept the Terms of Service'); + return; + } + + setLoading(true); + try { + await onSignup(email, password); + // Signup successful - navigate to dashboard + // User is already auto-logged in via Firebase createUserWithEmailAndPassword + navigation.replace('Dashboard'); + } catch (err: any) { + setError(err.message || 'Signup failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + const getStrengthColor = () => { + switch (passwordValidation.strength) { + case 'weak': + return '#ff4444'; + case 'medium': + return '#ffaa00'; + case 'strong': + return '#00cc00'; + default: + return '#cccccc'; + } + }; + + return ( + + Create Account + + {/* Email Input */} + + Email + + + + {/* Password Input */} + + Password + + + {/* Password Strength Indicator */} + {password.length > 0 && ( + + + r).length / 6) * 100}%`, + backgroundColor: getStrengthColor(), + }, + ]} + /> + + + {passwordValidation.strength.charAt(0).toUpperCase() + passwordValidation.strength.slice(1)} + + + )} + + {/* Password Requirements */} + {password.length > 0 && ( + + {Object.entries({ + minLength: '8-12 characters', + hasUppercase: 'One uppercase letter', + hasLowercase: 'One lowercase letter', + hasNumber: 'One number', + hasSpecialChar: 'One special character', + }).map(([key, label]) => ( + + {passwordValidation.requirements[key as keyof typeof passwordValidation.requirements] ? '✓' : '○'} {label} + + ))} + + )} + + + {/* Confirm Password Input */} + + Confirm Password + + {confirmPassword.length > 0 && !passwordsMatch && ( + Passwords do not match + )} + {confirmPassword.length > 0 && passwordsMatch && ( + ✓ Passwords match + )} + + + {/* Terms of Service */} + setAcceptedTerms(!acceptedTerms)} + > + + {acceptedTerms && } + + + I accept the Terms of Service + + + + {/* Error Message */} + {error && {error}} + + {/* Signup Button */} + + {loading ? ( + + ) : ( + Sign Up + )} + + + {/* Login Link */} + + Already have an account? + navigation.navigate('Login')} disabled={loading}> + Sign In + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + padding: 20, + backgroundColor: '#ffffff', + justifyContent: 'center', + }, + title: { + fontSize: 32, + fontWeight: 'bold', + marginBottom: 30, + textAlign: 'center', + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + color: '#333333', + }, + input: { + borderWidth: 1, + borderColor: '#cccccc', + borderRadius: 8, + padding: 12, + fontSize: 16, + backgroundColor: '#f9f9f9', + }, + strengthContainer: { + marginTop: 8, + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + strengthBar: { + flex: 1, + height: 6, + backgroundColor: '#e0e0e0', + borderRadius: 3, + overflow: 'hidden', + }, + strengthFill: { + height: '100%', + borderRadius: 3, + }, + strengthText: { + fontSize: 12, + fontWeight: '600', + width: 60, + }, + requirementsContainer: { + marginTop: 8, + paddingLeft: 4, + }, + requirement: { + fontSize: 13, + marginVertical: 2, + }, + requirementMet: { + color: '#00cc00', + }, + requirementUnmet: { + color: '#999999', + }, + errorText: { + color: '#ff4444', + fontSize: 13, + marginTop: 4, + }, + successText: { + color: '#00cc00', + fontSize: 13, + marginTop: 4, + }, + checkboxContainer: { + flexDirection: 'row', + alignItems: 'center', + marginVertical: 20, + }, + checkbox: { + width: 24, + height: 24, + borderWidth: 2, + borderColor: '#cccccc', + borderRadius: 4, + marginRight: 10, + justifyContent: 'center', + alignItems: 'center', + }, + checkboxChecked: { + backgroundColor: '#007AFF', + borderColor: '#007AFF', + }, + checkmark: { + color: '#ffffff', + fontSize: 16, + fontWeight: 'bold', + }, + checkboxLabel: { + fontSize: 14, + color: '#333333', + }, + link: { + color: '#007AFF', + textDecorationLine: 'underline', + }, + errorMessage: { + color: '#ff4444', + fontSize: 14, + textAlign: 'center', + marginBottom: 10, + padding: 10, + backgroundColor: '#fff0f0', + borderRadius: 8, + }, + button: { + backgroundColor: '#007AFF', + padding: 16, + borderRadius: 8, + alignItems: 'center', + marginTop: 10, + }, + buttonDisabled: { + backgroundColor: '#cccccc', + }, + buttonText: { + color: '#ffffff', + fontSize: 18, + fontWeight: '600', + }, + loginContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 20, + }, + loginText: { + fontSize: 14, + color: '#666666', + }, + loginLink: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, +}); diff --git a/frontend/src/config/firebase.ts b/frontend/src/config/firebase.ts new file mode 100644 index 0000000..580a86d --- /dev/null +++ b/frontend/src/config/firebase.ts @@ -0,0 +1,21 @@ +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; + +// Firebase configuration +// TODO: Replace with your actual Firebase project credentials +const firebaseConfig = { + apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY || '', + authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN || '', + projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID || '', + storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET || '', + messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || '', + appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID || '', +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); + +// Initialize Firebase Authentication +export const auth = getAuth(app); + +export default app; diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts new file mode 100644 index 0000000..0d04a6a --- /dev/null +++ b/frontend/src/services/authService.ts @@ -0,0 +1,174 @@ +import { + createUserWithEmailAndPassword, + deleteUser, + signInWithEmailAndPassword, + sendPasswordResetEmail, + signOut +} from 'firebase/auth'; +import { auth } from '../config/firebase'; +import axios from 'axios'; + +const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000'; + +export interface UserData { + id: string; + firebase_uid: string; + email: string | null; + display_name: string | null; + created_at: string; +} + +/** + * Sign up a new user with Firebase and initialize their record in the database. + * Implements rollback logic: if database initialization fails, the Firebase user is deleted. + */ +export const signUpUser = async (email: string, password: string): Promise => { + let firebaseUser = null; + + try { + // Step 1: Create user in Firebase + const userCredential = await createUserWithEmailAndPassword(auth, email, password); + firebaseUser = userCredential.user; + + // Step 2: Get Firebase ID token + const idToken = await firebaseUser.getIdToken(); + + // Step 3: Initialize user in database + try { + const response = await axios.post(`${API_URL}/users/init`, { + id_token: idToken, + }); + + // Success! Return user data + return response.data as UserData; + + } catch (dbError: any) { + // Database initialization failed - ROLLBACK: Delete Firebase user + console.error('Database initialization failed, rolling back Firebase user:', dbError); + + if (firebaseUser) { + try { + await deleteUser(firebaseUser); + console.log('Successfully deleted Firebase user after database failure'); + } catch (deleteError) { + console.error('Failed to delete Firebase user during rollback:', deleteError); + } + } + + // Re-throw error with better message + throw new Error( + dbError.response?.data?.detail || + 'Failed to initialize user account. Please try again.' + ); + } + + } catch (error: any) { + // Handle Firebase auth errors + if (error.code) { + switch (error.code) { + case 'auth/email-already-in-use': + throw new Error('This email is already registered. Please sign in instead.'); + case 'auth/invalid-email': + throw new Error('Invalid email address.'); + case 'auth/weak-password': + throw new Error('Password is too weak. Please use a stronger password.'); + case 'auth/network-request-failed': + throw new Error('Network error. Please check your connection and try again.'); + default: + throw new Error(error.message || 'Signup failed. Please try again.'); + } + } + + // Re-throw if it's already a formatted error from database rollback + throw error; + } +}; + +/** + * Sign in an existing user with Firebase. + */ +export const signInUser = async (email: string, password: string): Promise => { + try { + // Step 1: Sign in with Firebase + const userCredential = await signInWithEmailAndPassword(auth, email, password); + const firebaseUser = userCredential.user; + + // Step 2: Get user data from database using Firebase ID token + const idToken = await firebaseUser.getIdToken(); + + try { + const response = await axios.get(`${API_URL}/users/me`, { + headers: { + Authorization: `Bearer ${idToken}`, + }, + }); + + return response.data as UserData; + } catch (dbError: any) { + console.error('Failed to fetch user data from database:', dbError); + throw new Error( + dbError.response?.data?.detail || + 'Failed to load user data. Please try again.' + ); + } + } catch (error: any) { + // Handle Firebase auth errors + if (error.code) { + switch (error.code) { + case 'auth/invalid-email': + throw new Error('Invalid email address.'); + case 'auth/user-disabled': + throw new Error('This account has been disabled.'); + case 'auth/user-not-found': + throw new Error('No account found with this email.'); + case 'auth/wrong-password': + throw new Error('Incorrect password.'); + case 'auth/invalid-credential': + throw new Error('Invalid email or password.'); + case 'auth/network-request-failed': + throw new Error('Network error. Please check your connection and try again.'); + case 'auth/too-many-requests': + throw new Error('Too many failed login attempts. Please try again later.'); + default: + throw new Error(error.message || 'Login failed. Please try again.'); + } + } + + // Re-throw if it's already a formatted error + throw error; + } +}; + +/** + * Send a password reset email to the user. + */ +export const resetPassword = async (email: string): Promise => { + try { + await sendPasswordResetEmail(auth, email); + } catch (error: any) { + if (error.code) { + switch (error.code) { + case 'auth/invalid-email': + throw new Error('Invalid email address.'); + case 'auth/user-not-found': + throw new Error('No account found with this email.'); + case 'auth/network-request-failed': + throw new Error('Network error. Please check your connection and try again.'); + default: + throw new Error(error.message || 'Failed to send password reset email.'); + } + } + throw error; + } +}; + +/** + * Sign out the current user from Firebase. + */ +export const logoutUser = async (): Promise => { + try { + await signOut(auth); + } catch (error: any) { + throw new Error(error.message || 'Failed to log out. Please try again.'); + } +}; diff --git a/frontend/src/utils/passwordValidation.ts b/frontend/src/utils/passwordValidation.ts new file mode 100644 index 0000000..8986367 --- /dev/null +++ b/frontend/src/utils/passwordValidation.ts @@ -0,0 +1,57 @@ +export interface PasswordValidation { + isValid: boolean; + strength: 'weak' | 'medium' | 'strong'; + errors: string[]; + requirements: { + minLength: boolean; + maxLength: boolean; + hasUppercase: boolean; + hasLowercase: boolean; + hasNumber: boolean; + hasSpecialChar: boolean; + }; +} + +export const validatePassword = (password: string): PasswordValidation => { + const requirements = { + minLength: password.length >= 8, + maxLength: password.length <= 12, + hasUppercase: /[A-Z]/.test(password), + hasLowercase: /[a-z]/.test(password), + hasNumber: /\d/.test(password), + hasSpecialChar: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password), + }; + + const errors: string[] = []; + if (!requirements.minLength) errors.push('Password must be at least 8 characters'); + if (!requirements.maxLength) errors.push('Password must be at most 12 characters'); + if (!requirements.hasUppercase) errors.push('Password must include an uppercase letter'); + if (!requirements.hasLowercase) errors.push('Password must include a lowercase letter'); + if (!requirements.hasNumber) errors.push('Password must include a number'); + if (!requirements.hasSpecialChar) errors.push('Password must include a special character'); + + const isValid = Object.values(requirements).every(req => req === true); + + // Calculate strength based on met requirements + const metRequirements = Object.values(requirements).filter(req => req === true).length; + let strength: 'weak' | 'medium' | 'strong'; + + if (metRequirements <= 2) { + strength = 'weak'; + } else if (metRequirements <= 4) { + strength = 'medium'; + } else { + strength = 'strong'; + } + + return { + isValid, + strength, + errors, + requirements, + }; +}; + +export const validatePasswordMatch = (password: string, confirmPassword: string): boolean => { + return password === confirmPassword && password.length > 0; +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3d4606b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Code-Coven", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}