diff --git a/.env b/.env index a96b28f..a3b6ac6 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ -EXPO_PUBLIC_FIREBASE_API_KEY=AIzaSyBAyoohTHVBz3bZFmRiDb6s80D2hBeMSfs -EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=visuwallet.firebaseapp.com -EXPO_PUBLIC_FIREBASE_DATABASE_URL=https://visuwallet-default-rtdb.firebaseio.com -EXPO_PUBLIC_FIREBASE_PROJECT_ID=visuwallet -EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET=visuwallet.firebasestorage.app -EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=410749935369 -EXPO_PUBLIC_FIREBASE_APP_ID=1:410749935369:web:5d6e1ac0ff306ea26cfdd8 -EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID=G-KFVKWGVXD2 \ No newline at end of file +EXPO_PUBLIC_FIREBASE_API_KEY=AIzaSyCizNbDECCLXJyBimg1Q2UFalrguFdQ614 +EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=pre-taws.firebaseapp.com +EXPO_PUBLIC_FIREBASE_DATABASE_URL=https://pre-taws-default-rtdb.firebaseio.com +EXPO_PUBLIC_FIREBASE_PROJECT_ID=pre-taws +EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET=pre-taws.firebasestorage.app +EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=787398838709 +EXPO_PUBLIC_FIREBASE_APP_ID=1:787398838709:web:1e6ebc76530cfa47b9b1fc +EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID=G-51P1KVVG6H diff --git a/App.tsx b/App.tsx index cee74b2..b2a7e4d 100644 --- a/App.tsx +++ b/App.tsx @@ -1,27 +1,22 @@ // App.tsx // App.tsx -import 'global.css'; -import { useState } from 'react'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; - -import Home from 'components/Home'; -import Inicio from 'components/Inicio'; -import Formulario from 'components/Formulario'; -import DetalleCuenta from 'components/DetalleCuenta'; -import Estadisticas from 'components/Estadisticas'; -import ChartsScreen from 'components/ChartsScreen'; -import Perfil from 'components/Perfil'; -import Login from 'components/Auth/Login'; -import Signup from 'components/Auth/Signup'; +import "global.css"; +import { useState } from "react"; +import { SafeAreaProvider } from "react-native-safe-area-context"; + +import Home from "components/Home"; +import Inicio from "components/Inicio"; +import Formulario from "components/Formulario"; +import DetalleCuenta from "components/DetalleCuenta"; +import Estadisticas from "components/Estadisticas"; +import ChartsScreen from "components/ChartsScreen"; import { Cuenta } from './types'; -export default function App() { +function AppContent() { // Estado para controlar qué pantalla mostrar - const [currentScreen, setCurrentScreen] = useState< - 'login' | 'signup' | 'home' | 'inicio' | 'formulario' | 'detalleCuenta' | 'estadisticas' | 'charts' | 'perfil' - >('login'); + const [currentScreen, setCurrentScreen] = useState<'home' | 'inicio' | 'formulario' | 'detalleCuenta' | 'estadisticas' | 'charts'>('home'); // Estado para guardar la cuenta seleccionada const [selectedAccount, setSelectedAccount] = useState(null); @@ -39,8 +34,18 @@ export default function App() { setCurrentScreen('detalleCuenta'); }; + if (isLoading) { + return ( + + {/* loading state */} + + ); + } + + const isLoggedIn = !!user; + return ( - + {/* Renderizar la pantalla según el estado */} {currentScreen === 'login' && ( @@ -51,7 +56,9 @@ export default function App() { navigateTo('home')} onBack={() => navigateTo('login')} /> )} - {currentScreen === 'home' && ( + {!isLoggedIn ? ( + setCurrentScreen('home')} /> + ) : currentScreen === 'home' && ( navigateTo('inicio')} onPressAccount={navigateToAccountDetail} @@ -90,10 +97,6 @@ export default function App() { onPressPerfil={() => navigateTo('perfil')} /> )} - - {currentScreen === 'perfil' && ( - navigateTo('home')} /> - )} ); -} +} \ No newline at end of file diff --git a/README.firestoredb.md b/README.firestoredb.md new file mode 100644 index 0000000..032dec8 --- /dev/null +++ b/README.firestoredb.md @@ -0,0 +1,61 @@ +# Firestore Data Model for VisuWallet + +## Colecciones y documentos + +- `usuarios/{uid}` + - `nombre`: string + - `telefono`: string + - `providerId`: string (opcional) + - `createdAt`, `updatedAt`: timestamp + - Subcolección: `cuentas/{cuentaId}` + - `nombre`: string + - `numero`: string + - `tipo`: string + - `moneda`: string + - `saldoInicial`: number + - `notas`: string (opcional) + - `ownerUid`: string (igual a `uid`) + - `createdAt`, `updatedAt`: timestamp + +- `transacciones/{txId}` + - Datos del formulario: + - `monto`: number + - `tipo`: string + - `categoria`: string + - `fecha`: timestamp + - `descripcion`: string (opcional) + - Referencias: + - `refUsuario`: reference → `usuarios/{uid}` + - `refCuenta`: reference → `usuarios/{uid}/cuentas/{cuentaId}` + - Campos denormalizados para consultas: + - `ownerUid`: string + - `cuentaId`: string + - `cuentaNumero`: string (opcional) + - Auditoría: + - `createdAt`, `updatedAt`: timestamp + - `createdByUid`: string + +## Consultas recomendadas + +- Por usuario: `where('ownerUid','==',uid).orderBy('fecha','desc')` +- Por cuenta: `where('ownerUid','==',uid).where('cuentaId','==',cuentaId).orderBy('fecha','desc')` +- Por rango de fechas: `where('ownerUid','==',uid).where('fecha','>=',start).where('fecha','<=',end).orderBy('fecha')` + +## Índices sugeridos + +Ver `firestore.indexes.json` incluido en el proyecto. + +## Reglas de seguridad + +Ver `firestore.rules` incluido en el proyecto. En resumen: + +- `usuarios` y sus `cuentas` solo son accesibles por su dueño (`request.auth.uid`). +- `transacciones` solo pueden ser leídas/escritas por el dueño (`ownerUid`). +- En `create` de `transacciones` se valida que las referencias apunten al usuario y cuenta correctos. + +## Buenas prácticas + +- Usar subcolecciones para listas que crecen (cuentas por usuario). +- Mantener denormalización mínima en `transacciones` (`ownerUid`, `cuentaId`, `cuentaNumero`) para consultas rápidas. +- Acompañar referencias (`refUsuario`, `refCuenta`) con IDs planos para filtros. +- Añadir timestamps con `serverTimestamp()` y auditar con `createdByUid`. diff --git a/components/AddCuenta.tsx b/components/AddCuenta.tsx index 2bc85cd..7916697 100644 --- a/components/AddCuenta.tsx +++ b/components/AddCuenta.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { Modal, View, Text, TextInput, TouchableOpacity, Alert, Pressable } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { createCuentaEmbedded } from '../firebase/firestoreService'; +import { db } from 'utils/firebase.js'; +import { collection, addDoc, serverTimestamp } from 'firebase/firestore'; interface Props { visible: boolean; @@ -10,6 +11,7 @@ interface Props { } export default function AddCuenta({ visible, onClose, onSaved }: Props) { + const { user } = useAuth(); const [tipo, setTipo] = useState<'corriente' | 'ahorros'>('corriente'); const [numero, setNumero] = useState(''); const [saldo, setSaldo] = useState('0'); @@ -34,14 +36,23 @@ export default function AddCuenta({ visible, onClose, onSaved }: Props) { } const parsedSaldo = Number(saldo) || 0; + const payload = { + tipo, + numero, + saldo: parsedSaldo, + cedula, + propietario, + email: email || null, + createdAt: serverTimestamp(), + } as any; setSaving(true); try { - const cuenta = await createCuentaEmbedded({ nombre: propietario || 'Cuenta', balance: parsedSaldo, tipo, numero }); - console.log('Cuenta embebida creada', cuenta); + const ref = await addDoc(collection(db, 'cuentas'), payload); + console.log('Cuenta creada', ref.id, payload); reset(); onClose(); - onSaved?.(cuenta.id); + onSaved?.(ref.id); } catch (e) { console.warn('Failed to save embedded account to user doc', e); // Fallback: just log and close diff --git a/components/ChartsScreen.tsx b/components/ChartsScreen.tsx index 7093317..65b33d5 100644 --- a/components/ChartsScreen.tsx +++ b/components/ChartsScreen.tsx @@ -78,6 +78,31 @@ export default function ChartsScreen({ onBack, onPressAdd, onPressHome, onPressE setCuentas([]); } +<<<<<<< HEAD + // Cargar transacciones + const snap = await getDocs(collection(db, 'transacciones')); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.type ?? data.tipo ?? 'expense', + categoria: data.category ?? data.categoria ?? '', + monto: Number(data.amount ?? data.monto ?? 0), + fecha: data.date ?? data.fecha ?? (data.createdAt ? data.createdAt.toDate().toString() : ''), + account: data.account ?? data.cuenta ?? '', + accountId: data.accountId ?? '', + } as TransaccionConCuenta; + }); + + // Ordenar por fecha descendente + docs.sort((a, b) => { + const ta = a.fecha ? new Date(a.fecha).getTime() : 0; + const tb = b.fecha ? new Date(b.fecha).getTime() : 0; + return tb - ta; + }); + + setTodasLasTransacciones(docs); +======= // Cargar transacciones desde la colección top-level `transacciones` try { const auth = getAuth(); @@ -104,6 +129,7 @@ export default function ChartsScreen({ onBack, onPressAdd, onPressHome, onPressE console.warn('Error cargando transacciones', err); setTodasLasTransacciones([]); } +>>>>>>> origin/main } catch (error) { console.warn('Error cargando datos:', error); setTodasLasTransacciones([]); diff --git a/components/DetalleCuenta.tsx b/components/DetalleCuenta.tsx index bc62196..8dff8c1 100644 --- a/components/DetalleCuenta.tsx +++ b/components/DetalleCuenta.tsx @@ -26,16 +26,31 @@ export default function DetalleCuenta({ cuenta, onBack }: DetalleCuentaProps) { useEffect(() => { const loadForAccount = async () => { try { - // Load recent transactions for the current user and filter locally - const auth = getAuth(); - const user = auth.currentUser; - if (!user) { - setTransactions([]); - return; - } - - const txs = await getRecentTransactionsForUser(user.uid, 200); - const docs = txs.map((d: any) => ({ + // If cuenta.numero is available, query by accountId field in Firestore + if (cuenta && (cuenta as any).numero) { + const accNum = (cuenta as any).numero; + // query registro where accountId == accNum ordered by date desc + const q = query(collection(db, 'registro'), where('accountId', '==', accNum), orderBy('date', 'desc')); + const snap = await getDocs(q); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.type ?? data.tipo ?? 'expense', + categoria: data.category ?? data.categoria ?? '', + monto: Number(data.amount ?? data.monto ?? 0), + fecha: data.date ?? data.fecha ?? (data.createdAt ? data.createdAt.toDate().toString() : ''), + } as Transaccion; + }); + setTransactions(docs); + return; + } + + // Fallback: load all and filter by name/id (best-effort) + const snap = await getDocs(collection(db, 'registro')); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { id: d.id, tipo: d.tipo ?? d.type ?? d.tipo ?? 'expense', categoria: d.categoria ?? d.category ?? '', @@ -89,6 +104,23 @@ export default function DetalleCuenta({ cuenta, onBack }: DetalleCuentaProps) { + {/* Detalles de la cuenta */} + + DETALLES DE LA CUENTA + + Número + {(cuenta as any).numero || '—'} + + + Tipo + {(cuenta as any).tipo ? ((cuenta as any).tipo as string).toUpperCase() : '—'} + + + Balance + ${cuenta.balance.toFixed(2)} + + + ({ - id: d.id, - tipo: d.tipo ?? d.type ?? 'expense', - categoria: d.categoria ?? d.category ?? '', - monto: Number(d.monto ?? d.amount ?? 0), - fecha: d.fecha ?? d.date ?? (d.createdAt ? (d.createdAt.toDate ? d.createdAt.toDate().toString() : d.createdAt) : ''), - account: d.account ?? d.cuenta ?? '', - accountId: d.cuentaId ?? d.accountId ?? '', - } as TransaccionConCuenta)); - docs.sort((a: TransaccionConCuenta, b: TransaccionConCuenta) => { - const ta = a.fecha ? new Date(a.fecha).getTime() : 0; - const tb = b.fecha ? new Date(b.fecha).getTime() : 0; - return tb - ta; - }); - setTodasLasTransacciones(docs); - } - } catch (err) { - console.warn('Error cargando transacciones', err); - setTodasLasTransacciones([]); - } + // Cargar transacciones (patrón de Home.tsx) + const snap = await getDocs(collection(db, 'registro')); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.type ?? data.tipo ?? 'expense', + categoria: data.category ?? data.categoria ?? '', + monto: Number(data.amount ?? data.monto ?? 0), + fecha: data.date ?? data.fecha ?? (data.createdAt ? data.createdAt.toDate().toString() : ''), + account: data.account ?? data.cuenta ?? '', + accountId: data.accountId ?? '', + } as TransaccionConCuenta; + }); + + // Ordenar por fecha descendente + docs.sort((a, b) => { + const ta = a.fecha ? new Date(a.fecha).getTime() : 0; + const tb = b.fecha ? new Date(b.fecha).getTime() : 0; + return tb - ta; + }); + + setTodasLasTransacciones(docs); } catch (error) { console.warn('Error cargando datos:', error); setTodasLasTransacciones([]); diff --git a/components/Formulario.tsx b/components/Formulario.tsx index 4e90e8b..6d781c3 100644 --- a/components/Formulario.tsx +++ b/components/Formulario.tsx @@ -14,10 +14,8 @@ import { SafeAreaView } from "react-native-safe-area-context"; import AsyncStorage from '@react-native-async-storage/async-storage'; import Categorias from "./Categorias"; -import { db } from "utils/firebase.js"; +import { db, ensureAnonymousSignIn } from "utils/firebase.js"; import { collection, addDoc, getDocs, query, where, deleteDoc, doc, serverTimestamp } from "firebase/firestore"; -import { getCuentasFromUserDoc, addTransaccionAndUpdateBalance } from '../firebase/firestoreService'; -import { getAuth } from 'firebase/auth'; interface FormularioProps { @@ -28,7 +26,6 @@ export default function Formulario({onBack}:FormularioProps) { const [type, setType] = useState<"expense" | "income" | "transfer">("expense"); const [amount, setAmount] = useState(""); const [account, setAccount] = useState("Cuenta transaccional"); - const [accountId, setAccountId] = useState(null); const [category, setCategory] = useState(null); const [dateTime, setDateTime] = useState(new Date().toString()); const [labels, setLabels] = useState(""); // could be an array later @@ -117,16 +114,20 @@ export default function Formulario({onBack}:FormularioProps) { const loadAccounts = async () => { try { - const accsRaw = await getCuentasFromUserDoc(); - const accs = accsRaw.map((d: any) => ({ - id: d.id, - tipo: d.tipo || 'corriente', - numero: d.numero || '', - saldo: d.balance ? Number(d.balance) : Number(d.saldo ?? 0), - cedula: d.cedula || '', - propietario: d.nombre || d.propietario || d.titular || '', - email: d.email || '', - })); + const snapAcc = await getDocs(collection(db, 'cuentas')); + const accs = snapAcc.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.tipo || 'corriente', + numero: data.numero || '', + saldo: data.saldo ? Number(data.saldo) : 0, + cedula: data.cedula || '', + propietario: data.propietario || data.titular || '', + email: data.email || '', + }; + }); + // sort by propietario then numero accs.sort((a,b) => (a.propietario || '').localeCompare(b.propietario || '') || (a.numero || '').localeCompare(b.numero || '')); setAccounts(accs); } catch (e) { @@ -169,43 +170,13 @@ export default function Formulario({onBack}:FormularioProps) { const saveRecord = async (rec?: typeof record) => { const toSave = rec ?? record; try { - // If accountId is set use the service to write transaction and update embedded balance - if (accountId) { - await addTransaccionAndUpdateBalance(accountId, { - tipo: toSave.type ?? toSave.type ?? 'expense', - monto: Number(toSave.amount ?? toSave.monto ?? 0), - categoria: toSave.category ?? toSave.category ?? '', - fecha: toSave.date ?? new Date().toISOString(), - descripcion: toSave.details ?? toSave.note ?? '', - }); - Alert.alert('Success', 'Record saved successfully!'); - console.log('Record saved via service:', toSave); - onBack(); - return; - } - - // fallback: write to top-level `transacciones` (legacy `registro` is deprecated) - const auth = getAuth(); - const user = auth.currentUser; - const txDoc = { - cuentaId: accountId ?? '', - ownerUid: user ? user.uid : null, - createdByUid: user ? user.uid : null, - tipo: toSave.type ?? 'expense', - categoria: toSave.category ?? '', - monto: Number(toSave.amount) || 0, - fecha: toSave.date ?? new Date().toISOString(), - descripcion: toSave.details ?? '', - createdAt: serverTimestamp(), - } as any; - - await addDoc(collection(db, 'transacciones'), txDoc); + await addDoc(collection(db, 'registro'), { ...toSave }); Alert.alert('Success', 'Record saved successfully!'); console.log('Record saved:', toSave); onBack(); // ← : llamar a la función onBack } catch (e: unknown) { console.error(e); - Alert.alert('Error', 'Failed to save record.'); + Alert.alert('Error', 'Failed to save transaction.'); } }; const createAgain = () => { @@ -238,6 +209,7 @@ export default function Formulario({onBack}:FormularioProps) { const recToSave = { account, + accountCode, category: category ?? '', date: dateTime, details: note, @@ -367,7 +339,6 @@ export default function Formulario({onBack}:FormularioProps) { onPress={() => { const display = c.propietario ? `${c.propietario} — ${c.numero || ''}` : (c.numero || 'Cuenta'); setAccount(display); - setAccountId(c.id); handleChange('account', display); setShowAccountsModal(false); }} diff --git a/components/GraficoBalance.tsx b/components/GraficoBalance.tsx index 01f2b73..dd12cc1 100644 --- a/components/GraficoBalance.tsx +++ b/components/GraficoBalance.tsx @@ -5,17 +5,87 @@ interface GraficoBalanceProps { onPressShowMore?: () => void; } -export default function GraficoBalance({ balance, onPressShowMore }: GraficoBalanceProps) { - // Generar datos de ejemplo (últimos 7 días) - const daysAgo = 7; - const balances = Array.from({ length: daysAgo }, (_, i) => ({ - day: i + 1, - value: Math.max(0, balance - (Math.random() * balance * 0.3)) - })); - - const maxBalance = Math.max(...balances.map(b => b.value), balance); - const minBalance = Math.min(...balances.map(b => b.value), 0); - const range = maxBalance - minBalance || 1; +export default function GraficoBalance({ balance, transacciones = [], onPressShowMore }: GraficoBalanceProps) { + // Generar datos de últimos 7 días basados en transacciones reales + const generarDatos7Dias = () => { + const hoy = new Date(); + const datos: { value: number; label: string; dataPointText: string }[] = []; + + // Calcular balance hacia atrás desde hoy + const hace7Dias = new Date(hoy); + hace7Dias.setDate(hace7Dias.getDate() - 6); + + // Calcular balance inicial (hace 7 días) + let balanceInicial = balance; + for (let i = 0; i < 7; i++) { + const fecha = new Date(hoy); + fecha.setDate(fecha.getDate() - i); + const fechaStr = fecha.toISOString().split('T')[0]; + + const transaccionesDia = transacciones.filter(t => { + const ts = Date.parse(t.fecha as any); + if (isNaN(ts)) return false; + const fechaTransaccion = new Date(ts).toISOString().split('T')[0]; + return fechaTransaccion === fechaStr; + }); + + const ingresosdia = transaccionesDia + .filter(t => t.tipo === 'income') + .reduce((sum, t) => sum + t.monto, 0); + + const gastosdia = transaccionesDia + .filter(t => t.tipo === 'expense' || t.tipo === 'transfer') + .reduce((sum, t) => sum + t.monto, 0); + + balanceInicial -= (ingresosdia - gastosdia); + } + + // Ahora calcular hacia adelante para construir la serie + let balanceAcumulado = balanceInicial; + for (let i = 0; i < 7; i++) { + const fecha = new Date(hace7Dias); + fecha.setDate(fecha.getDate() + i); + const fechaStr = fecha.toISOString().split('T')[0]; + + const transaccionesDia = transacciones.filter(t => { + const ts = Date.parse(t.fecha as any); + if (isNaN(ts)) return false; + const fechaTransaccion = new Date(ts).toISOString().split('T')[0]; + return fechaTransaccion === fechaStr; + }); + + const ingresosdia = transaccionesDia + .filter(t => t.tipo === 'income') + .reduce((sum, t) => sum + t.monto, 0); + + const gastosdia = transaccionesDia + .filter(t => t.tipo === 'expense' || t.tipo === 'transfer') + .reduce((sum, t) => sum + t.monto, 0); + + balanceAcumulado += (ingresosdia - gastosdia); + + datos.push({ + value: balanceAcumulado, + label: fecha.toLocaleDateString('es-ES', { weekday: 'short' }).charAt(0).toUpperCase(), + dataPointText: `$${(balanceAcumulado / 1000).toFixed(1)}k` + }); + } + + return datos; + }; + + const datos = generarDatos7Dias(); + const balanceInicial = datos[0]?.value || 0; + const balanceFinal = balance; + const cambio = balanceInicial !== 0 + ? ((balanceFinal - balanceInicial) / Math.abs(balanceInicial)) * 100 + : 0; + const cambioFormateado = cambio.toFixed(1); + + // Calcular máximo y mínimo para el gráfico + const valoresY = datos.map(d => d.value); + const maxValor = Math.max(...valoresY); + const minValor = Math.min(...valoresY); return ( diff --git a/components/Home.tsx b/components/Home.tsx index 044c815..739a979 100644 --- a/components/Home.tsx +++ b/components/Home.tsx @@ -14,12 +14,12 @@ import AddCuenta from './AddCuenta'; import { mockTransactions } from "../datosPrueba"; import { db } from "utils/firebase.js"; -import { collection, getDocs, doc, getDoc } from "firebase/firestore"; -import { getCuentasFromUserDoc, getRecentTransactionsForUser } from '../firebase/firestoreService'; -import { getAuth } from 'firebase/auth'; -import { Transaccion, Cuenta } from "../types"; +import { collection, getDocs } from "firebase/firestore"; +import { Transaccion } from "../types"; import TransactionDetails from './TransactionDetails'; +import { Cuenta } from "../types"; + interface HomeProps { onPressAdd: () => void; onPressAccount: (cuenta: Cuenta) => void; @@ -31,7 +31,7 @@ interface HomeProps { export default function Home({ onPressAdd, onPressAccount, onPressEstadisticas, onPressCharts, onPressHome, onPressPerfil }: HomeProps) { const insets = useSafeAreaInsets(); - const [nombreUsuario, setNombreUsuario] = useState(''); + const nombreUsuario = "Sebas"; const [accounts, setAccounts] = useState<{id:string; nombre:string; balance:number}[]>([]); const [transactions, setTransactions] = useState(mockTransactions); const balanceTotal = accounts.reduce((s, a) => s + (a.balance || 0), 0); @@ -42,56 +42,38 @@ export default function Home({ onPressAdd, onPressAccount, onPressEstadisticas, // Load transactions and accounts from Firestore const loadAll = async () => { try { - // transactions: use new transacciones collection via service - try { - const auth = getAuth(); - const user = auth.currentUser; - if (user) { - // load user profile (displayName) from users/{uid} - try { - const userRef = doc(db, 'users', user.uid); - const userSnap = await getDoc(userRef); - if (userSnap.exists()) { - const ud: any = userSnap.data(); - const nameFromDoc = ud.displayName || ud.nombre || ''; - const fallback = user.displayName || (user.email ? user.email.split('@')[0] : '') || 'Usuario'; - setNombreUsuario(nameFromDoc || fallback); - } else { - const fallback = user.displayName || (user.email ? user.email.split('@')[0] : '') || 'Usuario'; - setNombreUsuario(fallback); - } - } catch (err) { - console.warn('Failed to load user profile', err); - } - const txs = await getRecentTransactionsForUser(user.uid, 50); - const docs = txs.map((d: any) => ({ - id: d.id, - tipo: d.tipo ?? d.type ?? d.tipo ?? 'expense', - categoria: d.categoria ?? d.category ?? '', - monto: Number(d.monto ?? d.amount ?? 0), - fecha: d.fecha ?? d.date ?? (d.createdAt ? (d.createdAt.toDate ? d.createdAt.toDate().toString() : d.createdAt) : ''), - } as Transaccion)); - docs.sort((a,b) => { - const ta = a.fecha ? new Date(a.fecha).getTime() : 0; - const tb = b.fecha ? new Date(b.fecha).getTime() : 0; - return tb - ta; - }); - setTransactions(docs.slice(0,4)); - } - } catch (err) { - console.warn('Failed to load transactions from service', err); - } - - // accounts: read embedded cuentas from users/{uid} - try { - const accs = await getCuentasFromUserDoc(); - // ensure shape matches Cuenta interface (id, nombre, balance) - const mapped = accs.map((c: any) => ({ id: c.id, nombre: c.nombre || c.propietario || 'Cuenta', balance: Number(c.balance ?? c.saldo ?? 0), numero: c.numero ?? '' })); - setAccounts(mapped); - } catch (err) { - console.warn('Failed to load embedded cuentas from user doc', err); - setAccounts([]); - } + // transactions + const snap = await getDocs(collection(db, 'registro')); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.type ?? data.tipo ?? 'expense', + categoria: data.category ?? data.categoria ?? '', + monto: Number(data.amount ?? data.monto ?? 0), + fecha: data.date ?? data.fecha ?? (data.createdAt ? data.createdAt.toDate().toString() : ''), + } as Transaccion; + }); + docs.sort((a,b) => { + const ta = a.fecha ? new Date(a.fecha).getTime() : 0; + const tb = b.fecha ? new Date(b.fecha).getTime() : 0; + return tb - ta; + }); + setAllTransactions(docs); // Guardar todas las transacciones + setTransactions(docs.slice(0,4)); // Solo las primeras 4 para mostrar + + // accounts + const snapAcc = await getDocs(collection(db, 'cuentas')); + const accs = snapAcc.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + nombre: data.propietario ?? data.nombre ?? `Cuenta ${d.id}`, + balance: Number(data.saldo ?? data.balance ?? 0), + numero: data.numero ?? '', + }; + }); + setAccounts(accs); } catch (e) { console.warn('Failed to load data from Firestore, using fallbacks', e); // leave transactions as mocks and accounts empty @@ -123,6 +105,9 @@ export default function Home({ onPressAdd, onPressAccount, onPressEstadisticas, Hola, {nombreUsuario} + + Auth: {user?.providerData?.[0]?.providerId ?? 'anonymous'} + @@ -134,24 +119,40 @@ export default function Home({ onPressAdd, onPressAccount, onPressEstadisticas, - + {/* Grid de cuentas */} - - {accounts.map((cuenta) => ( - onPressAccount(cuenta)} - /> - ))} + {accounts.length === 0 ? ( + + + + No tienes cuentas registradas + + setShowAddCuenta(true)} + className="bg-blue-500 rounded-lg px-6 py-3" + activeOpacity={0.8} + > + Crear primera cuenta + + + ) : ( + + {accounts.map((cuenta) => ( + onPressAccount(cuenta)} + /> + ))} setShowAddCuenta(true)} diff --git a/components/Inicio.tsx b/components/Inicio.tsx index 7309666..3ce4379 100644 --- a/components/Inicio.tsx +++ b/components/Inicio.tsx @@ -2,6 +2,8 @@ import React from "react"; import { Text, TouchableOpacity, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; +import { useAuth } from "../contexts/AuthContext"; +import { signInWithGoogle } from "../utils/firebase"; interface InicioProps { onPressManual: () => void; // ← NUEVA PROP @@ -9,7 +11,15 @@ interface InicioProps { } export default function Inicio({ onPressManual, onBack }: InicioProps) { // ← RECIBIR PROPS - + const { user } = useAuth(); + + const handleGoogleSignIn = async () => { + try { + await signInWithGoogle(); + } catch (e) { + // optionally show a toast + } + }; return ( + {/* Google Sign-In */} + + + {user?.providerData?.[0]?.providerId === 'google.com' ? 'Conectado con Google' : 'Iniciar sesión con Google'} + + + void; +}; + +export default function Login({ onContinue }: LoginProps) { + const { user } = useAuth(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [mode, setMode] = useState<'login' | 'signup'>('login'); + const [displayName, setDisplayName] = useState(''); + const [phone, setPhone] = useState(''); + const [error, setError] = useState(null); + const [showPassword, setShowPassword] = useState(false); + + // Anonymous login removed per policy; users must authenticate. + + const handleEmailAuth = async () => { + setError(null); + try { + if (mode === 'login') { + await signInWithEmail(email.trim(), password); + } else { + const nameTrim = displayName.trim(); + const phoneTrim = phone.trim(); + if (!nameTrim) { setError('El nombre es obligatorio'); return; } + if (!phoneTrim) { setError('El número de celular es obligatorio'); return; } + const u = await signUpWithEmail(email.trim(), password); + try { + await upsertUser({ uid: u.uid, email: u.email ?? undefined, displayName: nameTrim, phone: phoneTrim, providerId: 'password' }); + } catch {} + } + onContinue(); + } catch (e: any) { + setError(e?.message || 'Error de autenticación'); + } + }; + + return ( + + + + + + + VisuWallet + {mode==='login' ? 'Bienvenido de vuelta' : 'Crear una nueva cuenta'} + + + + + setMode('login')} className={`flex-1 rounded-lg py-2 items-center justify-center ${mode==='login' ? 'bg-neutral-700' : ''}`}> + Ingresar + + setMode('signup')} className={`flex-1 rounded-lg py-2 items-center justify-center ${mode==='signup' ? 'bg-neutral-700' : ''}`}> + Crear cuenta + + + + + Email + + + + Contraseña + + + setShowPassword(!showPassword)} className="px-3"> + + + + + {error && ( + + {error} + + )} + + + {mode==='login' ? 'Entrar' : 'Registrarse'} + + + {mode==='signup' && ( + <> + + Nombre + + + + Número de celular + + + + )} + + + {/* Anonymous access removed */} + + + ); +} + + diff --git a/components/TransaccionList.tsx b/components/TransaccionList.tsx index 89c5e2b..f4910ce 100644 --- a/components/TransaccionList.tsx +++ b/components/TransaccionList.tsx @@ -5,8 +5,6 @@ import TransaccionItem from './ItemTransaccion'; import { Transaccion } from '../types'; import { db } from 'utils/firebase.js'; import { collection, getDocs } from 'firebase/firestore'; -import { getRecentTransactionsForUser } from '../firebase/firestoreService'; -import { getAuth } from 'firebase/auth'; interface Props { visible: boolean; @@ -19,23 +17,23 @@ export default function TransaccionList({ visible, onClose }: Props) { useEffect(() => { const load = async () => { try { - const auth = getAuth(); - const user = auth.currentUser; - if (!user) return; - const snap = await getRecentTransactionsForUser(user.uid, 200); - const docs = snap.map((d: any) => ({ - id: d.id, - tipo: d.tipo ?? d.type ?? 'expense', - categoria: d.categoria ?? d.category ?? '', - monto: Number(d.monto ?? d.amount ?? 0), - fecha: d.fecha ?? d.date ?? (d.createdAt ? (d.createdAt.toDate ? d.createdAt.toDate().toString() : d.createdAt) : ''), - } as Transaccion)); + const snap = await getDocs(collection(db, 'registro')); + const docs = snap.docs.map(d => { + const data = d.data() as any; + return { + id: d.id, + tipo: data.type ?? data.tipo ?? 'expense', + categoria: data.category ?? data.categoria ?? '', + monto: Number(data.amount ?? data.monto ?? 0), + fecha: data.date ?? data.fecha ?? (data.createdAt ? data.createdAt.toDate().toString() : ''), + } as Transaccion; + }); docs.sort((a,b) => { const ta = a.fecha ? new Date(a.fecha).getTime() : 0; const tb = b.fecha ? new Date(b.fecha).getTime() : 0; return tb - ta; }); - setTransactions(docs); + setTransactions(mapped); } catch (e) { console.warn('Failed to load transactions for list', e); } diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx new file mode 100644 index 0000000..12a9cae --- /dev/null +++ b/contexts/AuthContext.tsx @@ -0,0 +1,44 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { auth } from '../utils/firebase'; +import { upsertUser } from '../services/firestore'; + +type AuthContextValue = { + user: User | null; + isLoading: boolean; +}; + +const AuthContext = createContext({ user: null, isLoading: true }); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Ensure there's at least an anonymous session + // Anonymous sign-in removed; user must sign in explicitly. + + const unsub = onAuthStateChanged(auth, async (u) => { + setUser(u); + setIsLoading(false); + if (u) { + try { + await upsertUser({ + uid: u.uid, + email: u.email ?? undefined, + displayName: u.displayName ?? undefined, + photoURL: u.photoURL ?? undefined, + providerId: u.providerData?.[0]?.providerId ?? 'anonymous', + }); + } catch (err) { + // swallow errors to avoid blocking the app + } + } + }); + return () => unsub(); + }, []); + + return {children}; +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..26fbfbe --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,31 @@ +{ + "indexes": [ + { + "collectionGroup": "transacciones", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "ownerUid", "order": "ASCENDING" }, + { "fieldPath": "fecha", "order": "DESCENDING" } + ] + }, + { + "collectionGroup": "transacciones", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "ownerUid", "order": "ASCENDING" }, + { "fieldPath": "cuentaId", "order": "ASCENDING" }, + { "fieldPath": "fecha", "order": "DESCENDING" } + ] + }, + { + "collectionGroup": "transacciones", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "ownerUid", "order": "ASCENDING" }, + { "fieldPath": "categoria", "order": "ASCENDING" }, + { "fieldPath": "fecha", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules index 1402f5f..79d14c4 100644 --- a/firestore.rules +++ b/firestore.rules @@ -2,24 +2,38 @@ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { - match /users/{uid} { + // Usuarios profile + match /usuarios/{uid} { allow read, write: if request.auth != null && request.auth.uid == uid; + + // Subcollection cuentas under user + match /cuentas/{cuentaId} { + allow read, write: if request.auth != null && request.auth.uid == uid; + + // Optional: basic field validations on create/update + function isValidCuenta() { + return request.resource.data.keys().hasAll(['nombre','numero']) && + (request.resource.data.ownerUid == uid); + } + + allow create: if isValidCuenta(); + allow update: if isValidCuenta(); + } } + // Transacciones top-level match /transacciones/{txId} { - allow create: if request.auth != null - && request.auth.uid == request.resource.data.createdByUid - && request.resource.data.ownerUid == request.auth.uid - && (request.resource.data.cuentaId in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.cuentasIds); + // Read/write only by owner + allow read, write: if request.auth != null && request.auth.uid == resource.data.ownerUid; - allow get, list: if request.auth != null && request.auth.uid == resource.data.ownerUid; - allow update, delete: if false; - } + // Create must set ownerUid to caller and refs to caller's path + allow create: if request.auth != null + && request.resource.data.ownerUid == request.auth.uid + && request.resource.data.refUsuario.path == '/databases/' + __database__ + '/documents/usuarios/' + request.auth.uid + && request.resource.data.refCuenta.path.matches('^/databases/' + __database__ + '/documents/usuarios/' + request.auth.uid + '/cuentas/.+$'); - // optional legacy collection read (if you keep registro) - match /registro/{docId} { - allow read: if request.auth != null && resource.data.ownerUid == request.auth.uid; - allow create: if request.auth != null && request.resource.data.createdByUid == request.auth.uid; + // Update keeps ownership invariant + allow update: if request.auth != null && request.auth.uid == resource.data.ownerUid; } } } diff --git a/package-lock.json b/package-lock.json index 2277079..c7b147b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,8 @@ "@react-navigation/native": "^7.1.21", "@react-navigation/native-stack": "^7.7.0", "expo": "^54.0.0", - "expo-auth-session": "~7.0.8", - "expo-build-properties": "~1.0.9", - "expo-crypto": "~15.0.7", - "expo-dev-client": "~6.0.17", + "expo-linear-gradient": "~15.0.7", "expo-status-bar": "~3.0.8", - "expo-web-browser": "~15.0.9", "firebase": "^12.5.0", "nativewind": "latest", "react": "19.1.0", @@ -1556,6 +1552,7 @@ "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==", "license": "MIT", + "peer": true, "dependencies": { "deepmerge": "^3.2.0", "hoist-non-react-statics": "^3.3.0" @@ -3566,9 +3563,10 @@ "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==" }, "node_modules/@firebase/app": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", - "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.5.tgz", + "integrity": "sha512-zyNY77xJOGwcuB+xCxF8z8lSiHvD4ox7BCsqLEHEvgqQoRjxFZ0fkROR6NV5QyXmCqRLodMM8J5d2EStOocWIw==", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -3627,9 +3625,10 @@ "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==" }, "node_modules/@firebase/app-compat": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", - "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.5.tgz", + "integrity": "sha512-lVG/nRnXaot0rQSZazmTNqy83ti9O3+kdwoaE0d5wahRIWNoDirbIMcGVjDDgdmf4IE6FYreWOMh0L3DV1475w==", + "peer": true, "dependencies": { "@firebase/app": "0.14.6", "@firebase/component": "0.7.0", @@ -3644,27 +3643,10 @@ "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==" - }, - "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==", - "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" - } + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "peer": true }, - "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "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==", @@ -4339,6 +4321,18 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4825,9 +4819,10 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@react-native-async-storage/async-storage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", - "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "peer": true, "dependencies": { "merge-options": "^3.0.4" }, @@ -5216,6 +5211,14 @@ "integrity": "sha512-tgeMMuYYJt3Aar5IIk3kyfL9zMvGsv5d7KsVT/2auri+hEH/L2M1i8X67ne4JjMWZqENYIGY1WuI4oPEL1H/xA==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "canvaskit-wasm": "0.40.0", + "react-reconciler": "0.31.0" + }, + "bin": { + "setup-skia-web": "scripts/setup-canvaskit.js" + }, "peerDependencies": { "ajv": "^8.0.0" } @@ -5394,6 +5397,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -5445,7 +5454,7 @@ "version": "19.1.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", - "devOptional": true, + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7732,9 +7741,195 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, - "license": "MIT" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -9252,97 +9447,6 @@ "react-native": "*" } }, - "node_modules/expo-asset/node_modules/@expo/image-utils": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.7.tgz", - "integrity": "sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w==", - "license": "MIT", - "dependencies": { - "@expo/spawn-async": "^1.7.2", - "chalk": "^4.0.0", - "getenv": "^2.0.0", - "jimp-compact": "0.16.1", - "parse-png": "^2.1.0", - "resolve-from": "^5.0.0", - "resolve-global": "^1.0.0", - "semver": "^7.6.0", - "temp-dir": "~2.0.0", - "unique-string": "~2.0.0" - } - }, - "node_modules/expo-asset/node_modules/getenv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expo-asset/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-auth-session": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-7.0.8.tgz", - "integrity": "sha512-kpo2Jva+6uVjk6TmNqWAoqTnULXZaEVa9l4uf8JH32uDMt/iZQhM0fauy7Ww+y910Euhv5djCP7cPj8KWv6cmQ==", - "license": "MIT", - "dependencies": { - "expo-application": "~7.0.7", - "expo-constants": "~18.0.8", - "expo-crypto": "~15.0.7", - "expo-linking": "~8.0.8", - "expo-web-browser": "~15.0.7", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-auth-session/node_modules/expo-application": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.7.tgz", - "integrity": "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-build-properties": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-1.0.9.tgz", - "integrity": "sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.11.0", - "semver": "^7.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-build-properties/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/expo-constants": { "version": "18.0.10", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz", @@ -9479,971 +9583,188 @@ "node": ">=6" } }, - "node_modules/expo-constants/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-constants/node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/expo-crypto": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.7.tgz", - "integrity": "sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-dev-client": { - "version": "6.0.17", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.17.tgz", - "integrity": "sha512-zVilIum3sqXFbhYhPT6TuxR3ddH/IfHL82FiOTqJUiYaTQqun1I6ogSvU1djhY1eXUYhfYIBieQNWMVjXPxMvw==", - "license": "MIT", - "dependencies": { - "expo-dev-launcher": "6.0.17", - "expo-dev-menu": "7.0.16", - "expo-dev-menu-interface": "2.0.0", - "expo-manifests": "~1.0.8", - "expo-updates-interface": "~2.0.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-dev-launcher": { - "version": "6.0.17", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.17.tgz", - "integrity": "sha512-riLxFXaw6Nvgb27TiQtUvoHkW/zTz0aO7M+qxDBBaEbJMJSFl51KSwOJJBTItVQIE9f9jB8x5L1CfLw81/McZw==", - "license": "MIT", - "dependencies": { - "expo-dev-menu": "7.0.16", - "expo-manifests": "~1.0.8" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-dev-menu": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.16.tgz", - "integrity": "sha512-/kjTjk5tcZV0ixYnV3JyzPXKlMimpBNYaDo4XxBbRFIkTf/vmb/9e1BTR2nALnoa/D3MRwtR43gZYT+W/wfKXw==", - "license": "MIT", - "dependencies": { - "expo-dev-menu-interface": "2.0.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-dev-menu-interface": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", - "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-file-system": { - "version": "19.0.17", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.17.tgz", - "integrity": "sha512-WwaS01SUFrxBnExn87pg0sCTJjZpf2KAOzfImG0o8yhkU7fbYpihpl/oocXBEsNbj58a8hVt1Y4CVV5c1tzu/g==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo-font": { - "version": "14.0.9", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz", - "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==", - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-json-utils": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", - "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", - "license": "MIT" - }, - "node_modules/expo-keep-awake": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz", - "integrity": "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, - "node_modules/expo-linking": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz", - "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", - "license": "MIT", - "dependencies": { - "expo-constants": "~18.0.8", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-manifests": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.8.tgz", - "integrity": "sha512-nA5PwU2uiUd+2nkDWf9e71AuFAtbrb330g/ecvuu52bmaXtN8J8oiilc9BDvAX0gg2fbtOaZdEdjBYopt1jdlQ==", - "license": "MIT", - "dependencies": { - "@expo/config": "~12.0.8", - "expo-json-utils": "~0.15.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-manifests/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/expo-manifests/node_modules/@expo/config": { - "version": "12.0.10", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.10.tgz", - "integrity": "sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "@expo/config-plugins": "~54.0.2", - "@expo/config-types": "^54.0.8", - "@expo/json-file": "^10.0.7", - "deepmerge": "^4.3.1", - "getenv": "^2.0.0", - "glob": "^10.4.2", - "require-from-string": "^2.0.2", - "resolve-from": "^5.0.0", - "resolve-workspace-root": "^2.0.0", - "semver": "^7.6.0", - "slugify": "^1.3.4", - "sucrase": "3.35.0" - } - }, - "node_modules/expo-manifests/node_modules/@expo/config-plugins": { - "version": "54.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.2.tgz", - "integrity": "sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg==", - "license": "MIT", - "dependencies": { - "@expo/config-types": "^54.0.8", - "@expo/json-file": "~10.0.7", - "@expo/plist": "^0.4.7", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", - "getenv": "^2.0.0", - "glob": "^10.4.2", - "resolve-from": "^5.0.0", - "semver": "^7.5.4", - "slash": "^3.0.0", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/expo-manifests/node_modules/@expo/config-types": { - "version": "54.0.8", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.8.tgz", - "integrity": "sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A==", - "license": "MIT" - }, - "node_modules/expo-manifests/node_modules/@expo/json-file": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz", - "integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "json5": "^2.2.3" - } - }, - "node_modules/expo-manifests/node_modules/@expo/plist": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz", - "integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.2.3", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/expo-manifests/node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/expo-manifests/node_modules/getenv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expo-manifests/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-manifests/node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/expo-modules-autolinking": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.21.tgz", - "integrity": "sha512-pOtPDLln3Ju8DW1zRW4OwZ702YqZ8g+kM/tEY1sWfv22kWUtxkvK+ytRDRpRdnKEnC28okbhWqeMnmVkSFzP6Q==", - "license": "MIT", - "dependencies": { - "@expo/spawn-async": "^1.7.2", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "require-from-string": "^2.0.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "expo-modules-autolinking": "bin/expo-modules-autolinking.js" - } - }, - "node_modules/expo-modules-core": { - "version": "3.0.25", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.25.tgz", - "integrity": "sha512-0P8PT8UV6c5/+p8zeVM/FXvBgn/ErtGcMaasqUgbzzBUg94ktbkIrij9t9reGCrir03BYt/Bcpv+EQtYC8JOug==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-server": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.4.tgz", - "integrity": "sha512-IN06r3oPxFh3plSXdvBL7dx0x6k+0/g0bgxJlNISs6qL5Z+gyPuWS750dpTzOeu37KyBG0RcyO9cXUKzjYgd4A==", - "license": "MIT", - "engines": { - "node": ">=20.16.0" - } - }, - "node_modules/expo-status-bar": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz", - "integrity": "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==", - "license": "MIT", - "dependencies": { - "react-native-is-edge-to-edge": "^1.2.1" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-updates-interface": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", - "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-web-browser": { - "version": "15.0.9", - "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.9.tgz", - "integrity": "sha512-Dj8kNFO+oXsxqCDNlUT/GhOrJnm10kAElH++3RplLydogFm5jTzXYWDEeNIDmV+F+BzGYs+sIhxiBf7RyaxXZg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/expo/node_modules/@expo/cli": { - "version": "54.0.16", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.16.tgz", - "integrity": "sha512-hY/OdRaJMs5WsVPuVSZ+RLH3VObJmL/pv5CGCHEZHN2PxZjSZSdctyKV8UcFBXTF0yIKNAJ9XLs1dlNYXHh4Cw==", - "license": "MIT", - "dependencies": { - "@0no-co/graphql.web": "^1.0.8", - "@expo/code-signing-certificates": "^0.0.5", - "@expo/config": "~12.0.10", - "@expo/config-plugins": "~54.0.2", - "@expo/devcert": "^1.1.2", - "@expo/env": "~2.0.7", - "@expo/image-utils": "^0.8.7", - "@expo/json-file": "^10.0.7", - "@expo/mcp-tunnel": "~0.1.0", - "@expo/metro": "~54.1.0", - "@expo/metro-config": "~54.0.9", - "@expo/osascript": "^2.3.7", - "@expo/package-manager": "^1.9.8", - "@expo/plist": "^0.4.7", - "@expo/prebuild-config": "^54.0.6", - "@expo/schema-utils": "^0.1.7", - "@expo/spawn-async": "^1.7.2", - "@expo/ws-tunnel": "^1.0.1", - "@expo/xcpretty": "^4.3.0", - "@react-native/dev-middleware": "0.81.5", - "@urql/core": "^5.0.6", - "@urql/exchange-retry": "^1.3.0", - "accepts": "^1.3.8", - "arg": "^5.0.2", - "better-opn": "~3.0.2", - "bplist-creator": "0.1.0", - "bplist-parser": "^0.3.1", - "chalk": "^4.0.0", - "ci-info": "^3.3.0", - "compression": "^1.7.4", - "connect": "^3.7.0", - "debug": "^4.3.4", - "env-editor": "^0.4.1", - "expo-server": "^1.0.4", - "freeport-async": "^2.0.0", - "getenv": "^2.0.0", - "glob": "^10.4.2", - "lan-network": "^0.1.6", - "minimatch": "^9.0.0", - "node-forge": "^1.3.1", - "npm-package-arg": "^11.0.0", - "ora": "^3.4.0", - "picomatch": "^3.0.1", - "pretty-bytes": "^5.6.0", - "pretty-format": "^29.7.0", - "progress": "^2.0.3", - "prompts": "^2.3.2", - "qrcode-terminal": "0.11.0", - "require-from-string": "^2.0.2", - "requireg": "^0.2.2", - "resolve": "^1.22.2", - "resolve-from": "^5.0.0", - "resolve.exports": "^2.0.3", - "semver": "^7.6.0", - "send": "^0.19.0", - "slugify": "^1.3.4", - "source-map-support": "~0.5.21", - "stacktrace-parser": "^0.1.10", - "structured-headers": "^0.4.1", - "tar": "^7.4.3", - "terminal-link": "^2.1.1", - "undici": "^6.18.2", - "wrap-ansi": "^7.0.0", - "ws": "^8.12.1" - }, - "bin": { - "expo-internal": "build/bin/cli" - }, - "peerDependencies": { - "expo": "*", - "expo-router": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "expo-router": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/config": { - "version": "12.0.10", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.10.tgz", - "integrity": "sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "@expo/config-plugins": "~54.0.2", - "@expo/config-types": "^54.0.8", - "@expo/json-file": "^10.0.7", - "deepmerge": "^4.3.1", - "getenv": "^2.0.0", - "glob": "^10.4.2", - "require-from-string": "^2.0.2", - "resolve-from": "^5.0.0", - "resolve-workspace-root": "^2.0.0", - "semver": "^7.6.0", - "slugify": "^1.3.4", - "sucrase": "3.35.0" - } - }, - "node_modules/expo/node_modules/@expo/config-plugins": { - "version": "54.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.2.tgz", - "integrity": "sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg==", - "license": "MIT", - "dependencies": { - "@expo/config-types": "^54.0.8", - "@expo/json-file": "~10.0.7", - "@expo/plist": "^0.4.7", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", - "getenv": "^2.0.0", - "glob": "^10.4.2", - "resolve-from": "^5.0.0", - "semver": "^7.5.4", - "slash": "^3.0.0", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/expo/node_modules/@expo/config-types": { - "version": "54.0.8", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.8.tgz", - "integrity": "sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A==", - "license": "MIT" - }, - "node_modules/expo/node_modules/@expo/env": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.7.tgz", - "integrity": "sha512-BNETbLEohk3HQ2LxwwezpG8pq+h7Fs7/vAMP3eAtFT1BCpprLYoBBFZH7gW4aqGfqOcVP4Lc91j014verrYNGg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", - "getenv": "^2.0.0" - } - }, - "node_modules/expo/node_modules/@expo/image-utils": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.7.tgz", - "integrity": "sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w==", - "license": "MIT", - "dependencies": { - "@expo/spawn-async": "^1.7.2", - "chalk": "^4.0.0", - "getenv": "^2.0.0", - "jimp-compact": "0.16.1", - "parse-png": "^2.1.0", - "resolve-from": "^5.0.0", - "resolve-global": "^1.0.0", - "semver": "^7.6.0", - "temp-dir": "~2.0.0", - "unique-string": "~2.0.0" - } - }, - "node_modules/expo/node_modules/@expo/json-file": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz", - "integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "json5": "^2.2.3" - } - }, - "node_modules/expo/node_modules/@expo/osascript": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.7.tgz", - "integrity": "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ==", - "license": "MIT", - "dependencies": { - "@expo/spawn-async": "^1.7.2", - "exec-async": "^2.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/expo/node_modules/@expo/package-manager": { - "version": "1.9.8", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.8.tgz", - "integrity": "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA==", - "license": "MIT", - "dependencies": { - "@expo/json-file": "^10.0.7", - "@expo/spawn-async": "^1.7.2", - "chalk": "^4.0.0", - "npm-package-arg": "^11.0.0", - "ora": "^3.4.0", - "resolve-workspace-root": "^2.0.0" - } - }, - "node_modules/expo/node_modules/@expo/plist": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz", - "integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.2.3", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/expo/node_modules/@expo/prebuild-config": { - "version": "54.0.6", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.6.tgz", - "integrity": "sha512-xowuMmyPNy+WTNq+YX0m0EFO/Knc68swjThk4dKivgZa8zI1UjvFXOBIOp8RX4ljCXLzwxQJM5oBBTvyn+59ZA==", - "license": "MIT", - "dependencies": { - "@expo/config": "~12.0.10", - "@expo/config-plugins": "~54.0.2", - "@expo/config-types": "^54.0.8", - "@expo/image-utils": "^0.8.7", - "@expo/json-file": "^10.0.7", - "@react-native/normalize-colors": "0.81.5", - "debug": "^4.3.1", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "xml2js": "0.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo/node_modules/@react-native/normalize-colors": { - "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", - "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", - "license": "MIT" - }, - "node_modules/expo/node_modules/@urql/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", - "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", - "license": "MIT", - "dependencies": { - "@0no-co/graphql.web": "^1.0.13", - "wonka": "^6.3.2" - } - }, - "node_modules/expo/node_modules/@urql/exchange-retry": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", - "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", - "license": "MIT", - "dependencies": { - "@urql/core": "^5.1.2", - "wonka": "^6.3.2" - }, - "peerDependencies": { - "@urql/core": "^5.0.0" - } - }, - "node_modules/expo/node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/expo/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expo/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/expo/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/expo/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/expo/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/expo/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/expo/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/expo/node_modules/getenv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expo/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "license": "MIT", - "dependencies": { - "chalk": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/log-symbols/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/expo/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/expo-constants/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/expo/node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/expo-constants/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, "engines": { - "node": ">= 18" + "node": ">=8.0" } }, - "node_modules/expo/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "node_modules/expo-crypto": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.7.tgz", + "integrity": "sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==", "license": "MIT", "dependencies": { - "mimic-fn": "^1.0.0" + "base64-js": "^1.3.0" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "expo": "*" } }, - "node_modules/expo/node_modules/ora": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "node_modules/expo-dev-client": { + "version": "6.0.17", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.17.tgz", + "integrity": "sha512-zVilIum3sqXFbhYhPT6TuxR3ddH/IfHL82FiOTqJUiYaTQqun1I6ogSvU1djhY1eXUYhfYIBieQNWMVjXPxMvw==", "license": "MIT", "dependencies": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", - "wcwidth": "^1.0.1" + "expo-dev-launcher": "6.0.17", + "expo-dev-menu": "7.0.16", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.8", + "expo-updates-interface": "~2.0.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "expo": "*" } }, - "node_modules/expo/node_modules/ora/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/expo-dev-launcher": { + "version": "6.0.17", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.17.tgz", + "integrity": "sha512-riLxFXaw6Nvgb27TiQtUvoHkW/zTz0aO7M+qxDBBaEbJMJSFl51KSwOJJBTItVQIE9f9jB8x5L1CfLw81/McZw==", "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "expo-dev-menu": "7.0.16", + "expo-manifests": "~1.0.8" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "expo": "*" } }, - "node_modules/expo/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "node_modules/expo-dev-menu": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.16.tgz", + "integrity": "sha512-/kjTjk5tcZV0ixYnV3JyzPXKlMimpBNYaDo4XxBbRFIkTf/vmb/9e1BTR2nALnoa/D3MRwtR43gZYT+W/wfKXw==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "expo-dev-menu-interface": "2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "expo": "*" } }, - "node_modules/expo/node_modules/qrcode-terminal": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", - "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" } }, - "node_modules/expo/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "node_modules/expo-file-system": { + "version": "19.0.17", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.17.tgz", + "integrity": "sha512-WwaS01SUFrxBnExn87pg0sCTJjZpf2KAOzfImG0o8yhkU7fbYpihpl/oocXBEsNbj58a8hVt1Y4CVV5c1tzu/g==", "license": "MIT", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" + "peerDependencies": { + "expo": "*", + "react-native": "*" } }, - "node_modules/expo/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/expo-font": { + "version": "14.0.9", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz", + "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==", + "dependencies": { + "fontfaceobserver": "^2.1.0" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" } }, - "node_modules/expo/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" }, - "node_modules/expo/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/expo-keep-awake": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz", + "integrity": "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA==", "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" + "peerDependencies": { + "expo": "*", + "react": "*" } }, - "node_modules/expo/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/expo-linking": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz", + "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "peer": true, + "peerDependencies": { + "react": "*", + "react-native": "*" } }, - "node_modules/expo/node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", - "license": "BlueOak-1.0.0", + "node_modules/expo-modules-autolinking": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.21.tgz", + "integrity": "sha512-pOtPDLln3Ju8DW1zRW4OwZ702YqZ8g+kM/tEY1sWfv22kWUtxkvK+ytRDRpRdnKEnC28okbhWqeMnmVkSFzP6Q==", + "license": "MIT", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" }, - "engines": { - "node": ">=18" + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, - "node_modules/expo/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "node_modules/expo-modules-core": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.25.tgz", + "integrity": "sha512-0P8PT8UV6c5/+p8zeVM/FXvBgn/ErtGcMaasqUgbzzBUg94ktbkIrij9t9reGCrir03BYt/Bcpv+EQtYC8JOug==", "license": "MIT", - "engines": { - "node": ">=10.0.0" + "dependencies": { + "invariant": "^2.2.4" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "react": "*", + "react-native": "*" } }, - "node_modules/expo/node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "node_modules/expo-server": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.4.tgz", + "integrity": "sha512-IN06r3oPxFh3plSXdvBL7dx0x6k+0/g0bgxJlNISs6qL5Z+gyPuWS750dpTzOeu37KyBG0RcyO9cXUKzjYgd4A==", "license": "MIT", "engines": { - "node": ">=8.0" + "node": ">=20.16.0" } }, - "node_modules/expo/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node_modules/expo-status-bar": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz", + "integrity": "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.2.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" } }, "node_modules/exponential-backoff": { @@ -11102,7 +10423,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -11391,6 +10711,7 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "react-is": "^16.7.0" } @@ -11399,7 +10720,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hosted-git-info": { "version": "7.0.2", @@ -12477,10 +11799,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -13866,7 +13187,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } @@ -15099,7 +14419,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15146,7 +14466,7 @@ "version": "0.81.5", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", - "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -15604,9 +14924,10 @@ } }, "node_modules/react-native-svg": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.0.tgz", - "integrity": "sha512-/Wx6F/IZ88B/GcF88bK8K7ZseJDYt+7WGaiggyzLvTowChQ8BM5idmcd4pK+6QJP6a6DmzL2sfOMukFUn/NArg==", + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", + "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "license": "MIT", "peer": true, "dependencies": { "css-select": "^5.1.0", @@ -18449,10 +17770,10 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "dev": true, + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 086697b..aecc66c 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,19 @@ "@react-navigation/native": "^7.1.21", "@react-navigation/native-stack": "^7.7.0", "expo": "^54.0.0", +<<<<<<< HEAD + "expo-auth-session": "^7.0.9", + "expo-linear-gradient": "~15.0.7", + "expo-status-bar": "~3.0.8", + "expo-web-browser": "^15.0.9", +======= "expo-auth-session": "~7.0.8", "expo-build-properties": "~1.0.9", "expo-crypto": "~15.0.7", "expo-dev-client": "~6.0.17", "expo-status-bar": "~3.0.8", "expo-web-browser": "~15.0.9", +>>>>>>> origin/main "firebase": "^12.5.0", "nativewind": "latest", "react": "19.1.0", diff --git a/services/firestore.ts b/services/firestore.ts new file mode 100644 index 0000000..e98e745 --- /dev/null +++ b/services/firestore.ts @@ -0,0 +1,160 @@ +import { db, auth } from 'utils/firebase.js'; +import { + addDoc, + collection, + doc, + getDoc, + getDocs, + onSnapshot, + query, + serverTimestamp, + setDoc, + where, +} from 'firebase/firestore'; + +export type FirestoreTransaction = { + tipo: 'income' | 'expense' | 'transfer'; + categoria: string; + monto: number; + fecha: string; // stored as string per current UI + descripcion?: string; + cuentaCodigo?: string; // account code (numero) to link transaction to user's account + account?: string; // display account string from UI + labels?: string[]; + payee?: string; + createdAt?: any; + createdByUid?: string; + ownerUid?: string; +}; + +export type FirestoreUser = { + uid: string; + email?: string; + displayName?: string; + phone?: string; + photoURL?: string; + providerId?: string; +}; + +export type FirestoreCuenta = { + tipo?: string; + numero?: string; + saldo?: number; + cedula?: string; + propietario?: string; // or titular + email?: string; + ownerUid?: string; +}; + +// Add a transaction to 'transacciones' with audit fields +export async function addTransaction(tx: FirestoreTransaction & { cuentaId?: string }) { + const ownerUid = auth.currentUser?.uid; + const payload = { + ...tx, + ownerUid, + createdByUid: ownerUid, + createdAt: serverTimestamp(), + } as any; + + // Add strong references for ease of navigation from app + if (ownerUid) { + payload.refUsuario = doc(db, 'usuarios', ownerUid); + } + if (ownerUid && tx.cuentaId) { + payload.refCuenta = doc(db, 'usuarios', ownerUid, 'cuentas', tx.cuentaId); + } + + const ref = await addDoc(collection(db, 'transacciones'), payload); + return ref.id; +} + +// Fetch all transactions (simple one-shot) +export async function listTransactions(): Promise<(FirestoreTransaction & { id: string })[]> { + const snap = await getDocs(collection(db, 'transacciones')); + return snap.docs.map((d) => ({ id: d.id, ...(d.data() as any) })); +} + +// Subscribe to transactions in real time +export function onTransactions(cb: (items: (FirestoreTransaction & { id: string })[]) => void) { + const q = query(collection(db, 'transacciones')); + return onSnapshot(q, (snap) => { + const items = snap.docs.map((d) => ({ id: d.id, ...(d.data() as any) })); + cb(items); + }); +} + +// Upsert user document in 'usuarios/{uid}' +export async function upsertUserDoc(uid: string, data: FirestoreUser) { + const ref = doc(db, 'usuarios', uid); + await setDoc(ref, { ...data, createdAt: serverTimestamp() }, { merge: true }); +} + +export async function getUser(uid: string): Promise { + const ref = doc(db, 'usuarios', uid); + const snap = await getDoc(ref); + return snap.exists() ? (snap.data() as any) : null; +} + +// Convenience upsert accepting FirestoreUser shape with uid included +export async function upsertUser(user: FirestoreUser) { + const ref = doc(db, 'usuarios', user.uid); + await setDoc(ref, { ...user, updatedAt: serverTimestamp() }, { merge: true }); +} + +// List 'cuentas' collection +export async function listCuentas(ownerUid?: string): Promise<({ id: string } & FirestoreCuenta)[]> { + if (!ownerUid) { + ownerUid = auth.currentUser?.uid; + } + const base = collection(db, 'usuarios', ownerUid as string, 'cuentas'); + const snap = await getDocs(base); + const items = snap.docs.map((d) => ({ id: d.id, ...(d.data() as any) })); + // normalize saldo to number + return items.map((c) => ({ + ...c, + saldo: c.saldo != null ? Number(c.saldo) : 0, + propietario: c.propietario ?? c.titular ?? '', + tipo: c.tipo ?? 'corriente', + numero: c.numero ?? '', + })); +} + +// Subscribe to cuentas changes +export function onCuentas(cb: (items: ({ id: string } & FirestoreCuenta)[]) => void, ownerUid?: string) { + const base = collection(db, 'usuarios', (ownerUid || auth.currentUser?.uid || 'unknown') as string, 'cuentas'); + return onSnapshot(base, (snap) => { + const items = snap.docs.map((d) => ({ id: d.id, ...(d.data() as any) })); + cb(items.map((c) => ({ + ...c, + saldo: c.saldo != null ? Number(c.saldo) : 0, + propietario: c.propietario ?? c.titular ?? '', + tipo: c.tipo ?? 'corriente', + numero: c.numero ?? '', + }))); + }); +} + +// Create a cuenta inside user's embedded subcollection `usuarios/{uid}/cuentas` +export async function addCuenta(data: FirestoreCuenta, ownerUid?: string) { + let uid = ownerUid; + if (!uid) { + uid = auth.currentUser?.uid; + } + if (!uid) throw new Error('No authenticated user'); + const base = collection(db, 'usuarios', uid as string, 'cuentas'); + const payload: any = { + ...data, + ownerUid: uid, + createdAt: serverTimestamp(), + updatedAt: serverTimestamp(), + }; + // Firestore does not accept undefined; ensure optional fields are null or removed + Object.keys(payload).forEach((k) => { + if (payload[k] === undefined) { + delete payload[k]; + } + }); + if (payload.email === undefined) payload.email = null; + const ref = await addDoc(base, payload); + return ref.id; +} diff --git a/types/index.ts b/types/index.ts index 280e3b3..9d43fec 100644 --- a/types/index.ts +++ b/types/index.ts @@ -5,6 +5,7 @@ export interface Cuenta { nombre:string; balance:number; numero?: string; + tipo?: string; } // se podría colocar tambien una interface Transaccoin con id, tipo, categoria, monto, date. diff --git a/utils/firebase.js b/utils/firebase.js index a78ad5d..b444f73 100644 --- a/utils/firebase.js +++ b/utils/firebase.js @@ -1,6 +1,8 @@ import { initializeApp, getApps, getApp } from 'firebase/app'; import { getFirestore } from 'firebase/firestore'; -import { getAuth, signInAnonymously } from 'firebase/auth'; +import { getAuth, signInAnonymously, GoogleAuthProvider, signInWithCredential, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut } from 'firebase/auth'; +import * as AuthSession from 'expo-auth-session'; +import * as WebBrowser from 'expo-web-browser'; // Read Firebase config from environment variables (EXPO_PUBLIC_*) const firebaseConfig = { @@ -30,6 +32,93 @@ async function ensureAnonymousSignIn() { return null; } } +// Anonymous sign-in removed per project policy. +/** + * Sign in with Google using Expo AuthSession and Firebase Auth. + * Requires setting EXPO_PUBLIC_GOOGLE_CLIENT_ID (web) and/or platform client IDs in env. + * Returns the Firebase user on success or null on failure. + */ +async function signInWithGoogle() { + try { + // Use the web client id by default (set in app env) + const clientId = process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID; + if (!clientId) { + console.warn('EXPO_PUBLIC_GOOGLE_CLIENT_ID not set in environment'); + return null; + } + + const redirectUri = AuthSession.makeRedirectUri({ useProxy: true }); + const scopes = ['openid', 'profile', 'email']; + + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${encodeURIComponent( + clientId + )}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=id_token&scope=${encodeURIComponent( + scopes.join(' ') + )}&nonce=${encodeURIComponent('nonce')}`; + + let result; + if (typeof AuthSession.startAsync === 'function') { + result = await AuthSession.startAsync({ authUrl }); + } else { + // Fallback: open auth session with expo-web-browser and parse returned url + const webRes = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); + if (webRes.type === 'success' && webRes.url) { + // extract params from fragment or querystring + const raw = webRes.url; + const hashOrQuery = raw.split('#')[1] || raw.split('?')[1] || ''; + const params = new URLSearchParams(hashOrQuery); + result = { type: webRes.type, params: Object.fromEntries(params.entries()) }; + } else { + result = { type: webRes.type }; + } + } + + if (result.type === 'success' && result.params && result.params.id_token) { + const idToken = result.params.id_token; + const credential = GoogleAuthProvider.credential(idToken); + const userCred = await signInWithCredential(auth, credential); + return userCred.user; + } + + console.warn('Google sign-in cancelled or failed', result); + return null; + } catch (e) { + console.error('signInWithGoogle failed', e); + return null; + } +} export default app; -export { db, auth, ensureAnonymousSignIn }; \ No newline at end of file +async function signUpWithEmail(email, password) { + try { + const cred = await createUserWithEmailAndPassword(auth, email, password); + return cred.user; + } catch (e) { + console.error('signUpWithEmail failed', e); + throw e; + } +} + +async function signInWithEmail(email, password) { + try { + const cred = await signInWithEmailAndPassword(auth, email, password); + return cred.user; + } catch (e) { + console.error('signInWithEmail failed', e); + throw e; + } +} + +export { db, auth, signInWithGoogle, signUpWithEmail, signInWithEmail }; + +async function signOutUser() { + try { + await signOut(auth); + return true; + } catch (e) { + console.error('signOut failed', e); + throw e; + } +} + +export { signOutUser }; \ No newline at end of file