diff --git a/app/.gitignore b/app/.gitignore
index 6bf33056..21486565 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -34,4 +34,5 @@ yarn-error.*
# typescript
*.tsbuildinfo
-android/*
\ No newline at end of file
+android/*
+ios/*
diff --git a/app/App.tsx b/app/App.tsx
deleted file mode 100644
index 60e44938..00000000
--- a/app/App.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-// App.tsx
-import App from './app/index'; // your actual entry file
-export default App;
\ No newline at end of file
diff --git a/app/app.json b/app/app.json
index 9acdac77..1343e021 100644
--- a/app/app.json
+++ b/app/app.json
@@ -2,10 +2,10 @@
"expo": {
"name": "friend-lite-app",
"slug": "friend-lite-app",
+ "scheme": "friendlite",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
- "entryPoint": "./app/index.tsx",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
@@ -17,7 +17,7 @@
],
"ios": {
"supportsTablet": true,
- "bundleIdentifier": "com.cupbearer5517.friendlite",
+ "bundleIdentifier": "com.sigmoid.friendlite",
"infoPlist": {
"NSMicrophoneUsageDescription": "Friend-Lite needs access to your microphone to stream audio to the backend for processing."
}
@@ -27,7 +27,7 @@
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
- "package": "com.cupbearer5517.friendlite",
+ "package": "com.sigmoid.friendlite",
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
@@ -49,7 +49,9 @@
"enableNotifications": true,
"enableBackgroundAudio": true,
"enableDeviceDetection": true,
- "iosBackgroundModes": { "useProcessing": true },
+ "iosBackgroundModes": {
+ "useProcessing": true
+ },
"iosConfig": {
"microphoneUsageDescription": "We use the mic for live audio streaming"
}
@@ -91,7 +93,8 @@
]
}
}
- ]
+ ],
+ "expo-router"
],
"extra": {
"eas": {
@@ -99,4 +102,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/app/_layout.tsx b/app/app/_layout.tsx
index d2a8b0bc..be5a2eb9 100644
--- a/app/app/_layout.tsx
+++ b/app/app/_layout.tsx
@@ -1,5 +1,17 @@
import { Stack } from "expo-router";
+import { useEffect } from "react";
export default function RootLayout() {
- return ;
+ useEffect(() => {
+ console.log('[RootLayout] Navigation container initialized');
+ }, []);
+
+ return (
+
+ );
}
diff --git a/app/app/index.tsx b/app/app/index.tsx
index 8bb1234a..db5dfff5 100644
--- a/app/app/index.tsx
+++ b/app/app/index.tsx
@@ -2,11 +2,12 @@ import React, { useRef, useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, View, SafeAreaView, ScrollView, Platform, FlatList, ActivityIndicator, Alert, Switch, Button, TouchableOpacity, KeyboardAvoidingView } from 'react-native';
import { OmiConnection } from 'friend-lite-react-native'; // OmiDevice also comes from here
import { State as BluetoothState } from 'react-native-ble-plx'; // Import State from ble-plx
+import { Link } from 'expo-router';
// Hooks
-import { useBluetoothManager } from './hooks/useBluetoothManager';
-import { useDeviceScanning } from './hooks/useDeviceScanning';
-import { useDeviceConnection } from './hooks/useDeviceConnection';
+import { useBluetoothManager } from '../src/hooks/useBluetoothManager';
+import { useDeviceScanning } from '../src/hooks/useDeviceScanning';
+import { useDeviceConnection } from '../src/hooks/useDeviceConnection';
import {
saveLastConnectedDeviceId,
getLastConnectedDeviceId,
@@ -16,24 +17,26 @@ import {
getUserId,
getAuthEmail,
getJwtToken,
-} from './utils/storage';
-import { useAudioListener } from './hooks/useAudioListener';
-import { useAudioStreamer } from './hooks/useAudioStreamer';
-import { usePhoneAudioRecorder } from './hooks/usePhoneAudioRecorder';
+} from '../src/utils/storage';
+import { useAudioListener } from '../src/hooks/useAudioListener';
+import { useAudioStreamer } from '../src/hooks/useAudioStreamer';
+import { usePhoneAudioRecorder } from '../src/hooks/usePhoneAudioRecorder';
// Components
-import BluetoothStatusBanner from './components/BluetoothStatusBanner';
-import ScanControls from './components/ScanControls';
-import DeviceListItem from './components/DeviceListItem';
-import DeviceDetails from './components/DeviceDetails';
-import AuthSection from './components/AuthSection';
-import BackendStatus from './components/BackendStatus';
-import PhoneAudioButton from './components/PhoneAudioButton';
+import BluetoothStatusBanner from '../src/components/BluetoothStatusBanner';
+import ScanControls from '../src/components/ScanControls';
+import DeviceListItem from '../src/components/DeviceListItem';
+import DeviceDetails from '../src/components/DeviceDetails';
+import AuthSection from '../src/components/AuthSection';
+import BackendStatus from '../src/components/BackendStatus';
+import PhoneAudioButton from '../src/components/PhoneAudioButton';
export default function App() {
// Initialize OmiConnection
const omiConnection = useRef(new OmiConnection()).current;
+ // Removed router navigation - using Link component instead
+
// Filter state
const [showOnlyOmi, setShowOnlyOmi] = useState(false);
@@ -517,11 +520,18 @@ export default function App() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
>
-
- Friend Lite
+
+ Friend Lite
+
+
+ βοΈ
+
+
+
{/* Backend Connection - moved to top */}
{
+ try {
+ console.log('[StatsScreen] Attempting to go back');
+ if (routerHook && typeof routerHook.back === 'function') {
+ routerHook.back();
+ } else if (navigationRouter && typeof navigationRouter.back === 'function') {
+ navigationRouter.back();
+ } else {
+ console.error('[StatsScreen] No valid router for back navigation');
+ }
+ } catch (error) {
+ console.error('[StatsScreen] Navigation back error:', error);
+ }
+ };
+ const [logs, setLogs] = useState([]);
+ const [stats, setStats] = useState(null);
+ const [currentSession, setCurrentSession] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const loadData = async () => {
+ try {
+ setIsLoading(true);
+ const [logsData, statsData, sessionData] = await Promise.all([
+ getBluetoothLogs(),
+ getBluetoothStats(),
+ getCurrentSession(),
+ ]);
+
+ setLogs(logsData.slice(0, 20)); // Show last 20 events
+ setStats(statsData);
+ setCurrentSession(sessionData);
+ } catch (error) {
+ console.error('[StatsScreen] Error loading data:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const handleClearLogs = () => {
+ Alert.alert(
+ 'Clear All Logs',
+ 'Are you sure you want to clear all Bluetooth connection logs and statistics? This cannot be undone.',
+ [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Clear',
+ style: 'destructive',
+ onPress: async () => {
+ await clearBluetoothLogs();
+ await loadData();
+ },
+ },
+ ]
+ );
+ };
+
+ const formatDuration = (ms: number): string => {
+ if (ms < 1000) return `${ms}ms`;
+
+ const seconds = Math.floor(ms / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+
+ if (hours > 0) {
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
+ } else if (minutes > 0) {
+ return `${minutes}m ${seconds % 60}s`;
+ } else {
+ return `${seconds}s`;
+ }
+ };
+
+ const formatTimestamp = (timestamp: number): string => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString();
+ };
+
+ const getEventIcon = (event: BluetoothEvent): string => {
+ switch (event) {
+ case BluetoothEvent.CONNECT_SUCCESS:
+ case BluetoothEvent.AUTO_RECONNECT_SUCCESS:
+ return 'β
';
+ case BluetoothEvent.CONNECT_FAILURE:
+ case BluetoothEvent.AUTO_RECONNECT_FAILURE:
+ return 'β';
+ case BluetoothEvent.DISCONNECT:
+ return 'π';
+ case BluetoothEvent.CONNECT_ATTEMPT:
+ case BluetoothEvent.AUTO_RECONNECT_ATTEMPT:
+ return 'π';
+ case BluetoothEvent.ERROR:
+ return 'β οΈ';
+ case BluetoothEvent.STATE_CHANGE:
+ return 'π';
+ case BluetoothEvent.SCAN_START:
+ return 'π';
+ case BluetoothEvent.SCAN_STOP:
+ return 'βΈοΈ';
+ default:
+ return 'π';
+ }
+ };
+
+ const getEventLabel = (event: BluetoothEvent): string => {
+ switch (event) {
+ case BluetoothEvent.CONNECT_ATTEMPT:
+ return 'Connect Attempt';
+ case BluetoothEvent.CONNECT_SUCCESS:
+ return 'Connected';
+ case BluetoothEvent.CONNECT_FAILURE:
+ return 'Connection Failed';
+ case BluetoothEvent.DISCONNECT:
+ return 'Disconnected';
+ case BluetoothEvent.AUTO_RECONNECT_ATTEMPT:
+ return 'Auto Reconnect Attempt';
+ case BluetoothEvent.AUTO_RECONNECT_SUCCESS:
+ return 'Auto Reconnected';
+ case BluetoothEvent.AUTO_RECONNECT_FAILURE:
+ return 'Auto Reconnect Failed';
+ case BluetoothEvent.STATE_CHANGE:
+ return 'State Change';
+ case BluetoothEvent.ERROR:
+ return 'Error';
+ case BluetoothEvent.SCAN_START:
+ return 'Scan Started';
+ case BluetoothEvent.SCAN_STOP:
+ return 'Scan Stopped';
+ default:
+ return event;
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ Loading statistics...
+
+
+ );
+ }
+
+ return (
+
+
+
+ β Back
+
+ Connection Stats
+
+ ποΈ
+
+
+
+
+ {/* Bluetooth Summary Section */}
+
+ π Bluetooth Summary
+
+
+ {stats?.totalSessions || 0}
+ Total Sessions
+
+
+
+ {stats && stats.totalConnections > 0
+ ? `${((stats.successfulConnections / stats.totalConnections) * 100).toFixed(1)}%`
+ : '0%'}
+
+ Success Rate
+
+
+
+ {stats?.averageSessionDuration ? formatDuration(stats.averageSessionDuration) : '0s'}
+
+ Avg Duration
+
+
+ {stats?.totalDisconnects || 0}
+ Disconnects
+
+
+
+
+ {/* Current Connection Section */}
+ {currentSession && (
+
+ π· Current Connection
+
+
+ Status:
+ Connected β
+
+
+ Device:
+ {currentSession.deviceId}
+
+ {currentSession.deviceName && (
+
+ Name:
+ {currentSession.deviceName}
+
+ )}
+
+ Duration:
+
+ {formatDuration(Date.now() - currentSession.startTime)}
+
+
+
+ Started:
+ {formatTimestamp(currentSession.startTime)}
+
+
+
+ )}
+
+ {/* Recent Events Section */}
+
+ π Recent Events (Last 24h)
+ {logs.length > 0 ? (
+
+ {logs.map((log) => (
+
+
+ {formatTimestamp(log.timestamp)}
+ {getEventIcon(log.event)}
+
+ {getEventLabel(log.event)}
+ {log.details.deviceId && (
+ Device: {log.details.deviceId}
+ )}
+ {log.details.deviceName && (
+ Name: {log.details.deviceName}
+ )}
+ {log.details.state && (
+ State: {log.details.state}
+ )}
+ {log.details.duration && (
+ Duration: {formatDuration(log.details.duration)}
+ )}
+ {log.details.errorMessage && (
+
+ Error: {log.details.errorMessage}
+
+ )}
+
+ ))}
+
+ ) : (
+
+ No events recorded yet
+
+ Connect to a Bluetooth device to start logging
+
+
+ )}
+
+
+ {/* WebSocket Stub Section */}
+
+ π WebSocket Connection
+
+ π§
+ Logging Not Implemented
+
+ WebSocket connection logging will be added in Phase 2. It will track connection
+ state, reconnection attempts, and streaming metrics.
+
+
+
+
+ {/* Phone Audio Stub Section */}
+
+ π€ Phone Audio
+
+ π§
+ Logging Not Implemented
+
+ Phone audio recording logging will be added in Phase 2. It will track recording
+ sessions, duration, and audio streaming metrics.
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 15,
+ backgroundColor: 'white',
+ borderBottomWidth: 1,
+ borderBottomColor: '#e0e0e0',
+ },
+ backButton: {
+ padding: 5,
+ },
+ backButtonText: {
+ fontSize: 16,
+ color: '#007AFF',
+ fontWeight: '600',
+ },
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#333',
+ },
+ clearButton: {
+ padding: 5,
+ },
+ clearButtonText: {
+ fontSize: 20,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: 15,
+ paddingBottom: 30,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: 16,
+ color: '#666',
+ },
+ section: {
+ marginBottom: 20,
+ padding: 15,
+ backgroundColor: 'white',
+ borderRadius: 10,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 3,
+ elevation: 2,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 15,
+ },
+ statsGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ },
+ statItem: {
+ width: '48%',
+ padding: 15,
+ backgroundColor: '#f8f9fa',
+ borderRadius: 8,
+ marginBottom: 10,
+ alignItems: 'center',
+ },
+ statValue: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#007AFF',
+ marginBottom: 5,
+ },
+ statLabel: {
+ fontSize: 12,
+ color: '#666',
+ textAlign: 'center',
+ },
+ currentConnection: {
+ backgroundColor: '#f8f9fa',
+ padding: 15,
+ borderRadius: 8,
+ },
+ statusRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: 6,
+ },
+ statusLabel: {
+ fontSize: 14,
+ color: '#666',
+ fontWeight: '500',
+ },
+ statusValue: {
+ fontSize: 14,
+ color: '#333',
+ fontWeight: '600',
+ },
+ statusActive: {
+ color: '#4CD964',
+ },
+ eventsList: {
+ marginTop: 5,
+ },
+ eventItem: {
+ backgroundColor: '#f8f9fa',
+ padding: 12,
+ borderRadius: 8,
+ marginBottom: 10,
+ borderLeftWidth: 3,
+ borderLeftColor: '#007AFF',
+ },
+ eventHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 5,
+ },
+ eventTime: {
+ fontSize: 12,
+ color: '#666',
+ },
+ eventIcon: {
+ fontSize: 16,
+ },
+ eventType: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 5,
+ },
+ eventDetail: {
+ fontSize: 13,
+ color: '#666',
+ marginTop: 2,
+ },
+ errorText: {
+ color: '#FF3B30',
+ },
+ emptyState: {
+ padding: 30,
+ alignItems: 'center',
+ },
+ emptyStateText: {
+ fontSize: 16,
+ color: '#666',
+ fontWeight: '500',
+ marginBottom: 5,
+ },
+ emptyStateSubtext: {
+ fontSize: 14,
+ color: '#999',
+ textAlign: 'center',
+ },
+ stubCard: {
+ backgroundColor: '#FFF9E6',
+ padding: 20,
+ borderRadius: 8,
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: '#FFE066',
+ },
+ stubIcon: {
+ fontSize: 40,
+ marginBottom: 10,
+ },
+ stubTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 8,
+ },
+ stubDescription: {
+ fontSize: 13,
+ color: '#666',
+ textAlign: 'center',
+ lineHeight: 18,
+ },
+});
diff --git a/app/install.sh b/app/install.sh
new file mode 100755
index 00000000..0f0587b0
--- /dev/null
+++ b/app/install.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+# Friend-Lite iOS App Installer
+# Simple script to build and run the iOS app on simulators or physical devices
+# Usage: ./install.sh
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Print colored output
+print_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+print_header() {
+ echo -e "${CYAN}================================${NC}"
+ echo -e "${CYAN}$1${NC}"
+ echo -e "${CYAN}================================${NC}"
+}
+
+# Check if we're in the app directory
+if [ ! -f "package.json" ] || [ ! -f "app.json" ]; then
+ print_error "Please run this script from the app directory (./app/)"
+ exit 1
+fi
+
+print_header "Friend-Lite iOS App Installer"
+
+# Check for required tools
+print_info "Checking dependencies..."
+
+if ! command -v node &> /dev/null; then
+ print_error "Node.js is not installed. Please install it from https://nodejs.org/"
+ exit 1
+fi
+
+if ! command -v npm &> /dev/null; then
+ print_error "npm is not installed. Please install Node.js from https://nodejs.org/"
+ exit 1
+fi
+
+if ! command -v xcrun &> /dev/null; then
+ print_error "Xcode command line tools not found. Please install Xcode."
+ exit 1
+fi
+
+print_success "All required tools are installed"
+
+# Install npm dependencies if node_modules doesn't exist
+if [ ! -d "node_modules" ]; then
+ print_info "Installing npm dependencies..."
+ npm install
+ print_success "Dependencies installed"
+else
+ print_info "Dependencies already installed (skipping npm install)"
+ echo -e " ${YELLOW}Tip:${NC} Run 'npm install' manually if you need to update dependencies"
+fi
+
+# Check if iOS build exists
+if [ ! -d "ios" ]; then
+ print_warning "iOS native project not found. Will be generated on first build."
+fi
+
+# Get available iOS simulators
+print_info "Fetching available iOS devices..."
+DEVICES=$(xcrun simctl list devices available | grep -E "iPhone|iPad" | grep -v "unavailable" || true)
+
+if [ -z "$DEVICES" ]; then
+ print_warning "No iOS simulators found. Will build for connected device or default simulator."
+ DEVICE_OPTION=""
+else
+ print_info "Available iOS Simulators:"
+ echo "$DEVICES" | nl -w2 -s') '
+ echo ""
+ echo "Options:"
+ echo " 1) Build for specific simulator (enter number above)"
+ echo " 2) Build for connected physical device"
+ echo " 3) Build for default simulator"
+ echo ""
+ read -p "Select option (1-3) or press Enter for default simulator: " DEVICE_CHOICE
+
+ if [ "$DEVICE_CHOICE" = "1" ]; then
+ read -p "Enter simulator number: " SIM_NUM
+ DEVICE_NAME=$(echo "$DEVICES" | sed -n "${SIM_NUM}p" | sed -E 's/.*\(([-A-Z0-9]+)\).*/\1/' | xargs)
+ if [ -n "$DEVICE_NAME" ]; then
+ DEVICE_OPTION="--device $DEVICE_NAME"
+ print_success "Selected device: $DEVICE_NAME"
+ else
+ print_warning "Invalid selection, using default simulator"
+ DEVICE_OPTION=""
+ fi
+ elif [ "$DEVICE_CHOICE" = "2" ]; then
+ DEVICE_OPTION="--device"
+ print_success "Will build for connected physical device"
+ else
+ DEVICE_OPTION=""
+ print_success "Will use default simulator"
+ fi
+fi
+
+# Ask about tunnel mode
+echo ""
+print_info "Connection Mode:"
+echo " Tunnel mode allows connection from physical devices outside your local network"
+echo " Local mode is faster for development on simulators"
+echo ""
+read -p "Use tunnel mode? (y/N): " USE_TUNNEL
+
+TUNNEL_FLAG=""
+if [[ "$USE_TUNNEL" =~ ^[Yy]$ ]]; then
+ TUNNEL_FLAG="--tunnel"
+ print_success "Tunnel mode enabled"
+else
+ print_success "Using local network mode"
+fi
+
+# Build and run
+print_header "Building and Running iOS App"
+
+COMMAND="npx expo run:ios $DEVICE_OPTION $TUNNEL_FLAG"
+print_info "Executing: $COMMAND"
+echo ""
+
+# Run the command
+eval $COMMAND
+
+print_success "iOS app build and launch completed!"
+print_info "The app should now be running on your selected device"
diff --git a/app/package-lock.json b/app/package-lock.json
index be20753f..9c432c34 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -17,6 +17,7 @@
"expo": "~53.0.9",
"expo-build-properties": "~0.14.8",
"expo-dev-client": "~5.2.4",
+ "expo-router": "~5.1.7",
"expo-status-bar": "~2.2.3",
"friend-lite-react-native": "^1.0.2",
"install": "^0.13.0",
@@ -25,11 +26,14 @@
"react-native": "0.79.6",
"react-native-base64": "^0.2.1",
"react-native-ble-plx": "^3.5.0",
+ "react-native-safe-area-context": "^5.6.2",
+ "react-native-screens": "^4.18.0",
"setimmediate": "^1.0.5",
"webidl-conversions": "^7.0.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
+ "@expo/ngrok": "^4.1.3",
"@react-native-community/cli": "latest",
"@types/react": "~19.0.10",
"typescript": "~5.8.3"
@@ -2017,6 +2021,217 @@
"resolve-from": "^5.0.0"
}
},
+ "node_modules/@expo/metro-runtime": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-5.0.5.tgz",
+ "integrity": "sha512-P8UFTi+YsmiD1BmdTdiIQITzDMcZgronsA3RTQ4QKJjHM3bas11oGzLQOnFaIZnlEV8Rrr3m1m+RHxvnpL+t/A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-native": "*"
+ }
+ },
+ "node_modules/@expo/ngrok": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok/-/ngrok-4.1.3.tgz",
+ "integrity": "sha512-AESYaROGIGKWwWmUyQoUXcbvaUZjmpecC5buArXxYou+RID813F8T0Y5jQ2HUY49mZpYfJiy9oh4VSN37GgrXA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@expo/ngrok-bin": "2.3.42",
+ "got": "^11.5.1",
+ "uuid": "^3.3.2",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/@expo/ngrok-bin": {
+ "version": "2.3.42",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin/-/ngrok-bin-2.3.42.tgz",
+ "integrity": "sha512-kyhORGwv9XpbPeNIrX6QZ9wDVCDOScyTwxeS+ScNmUqYoZqD9LRmEqF7bpDh5VonTsrXgWrGl7wD2++oSHcaTQ==",
+ "dev": true,
+ "bin": {
+ "ngrok": "bin/ngrok.js"
+ },
+ "optionalDependencies": {
+ "@expo/ngrok-bin-darwin-arm64": "2.3.41",
+ "@expo/ngrok-bin-darwin-x64": "2.3.41",
+ "@expo/ngrok-bin-freebsd-ia32": "2.3.41",
+ "@expo/ngrok-bin-freebsd-x64": "2.3.41",
+ "@expo/ngrok-bin-linux-arm": "2.3.41",
+ "@expo/ngrok-bin-linux-arm64": "2.3.41",
+ "@expo/ngrok-bin-linux-ia32": "2.3.41",
+ "@expo/ngrok-bin-linux-x64": "2.3.41",
+ "@expo/ngrok-bin-sunos-x64": "2.3.41",
+ "@expo/ngrok-bin-win32-ia32": "2.3.41",
+ "@expo/ngrok-bin-win32-x64": "2.3.41"
+ }
+ },
+ "node_modules/@expo/ngrok-bin-darwin-arm64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-darwin-arm64/-/ngrok-bin-darwin-arm64-2.3.41.tgz",
+ "integrity": "sha512-TPf95xp6SkvbRONZjltTOFcCJbmzAH7lrQ36Dv+djrOckWGPVq4HCur48YAeiGDqspmFEmqZ7ykD5c/bDfRFOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-darwin-x64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-darwin-x64/-/ngrok-bin-darwin-x64-2.3.41.tgz",
+ "integrity": "sha512-29QZHfX4Ec0p0pQF5UrqiP2/Qe7t2rI96o+5b8045VCEl9AEAKHceGuyo+jfUDR4FSQBGFLSDb06xy8ghL3ZYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-freebsd-ia32": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-freebsd-ia32/-/ngrok-bin-freebsd-ia32-2.3.41.tgz",
+ "integrity": "sha512-YYXgwNZ+p0aIrwgb+1/RxJbsWhGEzBDBhZulKg1VB7tKDAd2C8uGnbK1rOCuZy013iOUsJDXaj9U5QKc13iIXw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-freebsd-x64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-freebsd-x64/-/ngrok-bin-freebsd-x64-2.3.41.tgz",
+ "integrity": "sha512-1Ei6K8BB+3etmmBT0tXYC4dyVkJMigT4ELbRTF5jKfw1pblqeXM9Qpf3p8851PTlH142S3bockCeO39rSkOnkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-linux-arm": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-arm/-/ngrok-bin-linux-arm-2.3.41.tgz",
+ "integrity": "sha512-B6+rW/+tEi7ZrKWQGkRzlwmKo7c1WJhNODFBSgkF/Sj9PmmNhBz67mer91S2+6nNt5pfcwLLd61CjtWfR1LUHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-linux-arm64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-arm64/-/ngrok-bin-linux-arm64-2.3.41.tgz",
+ "integrity": "sha512-eC8GA/xPcmQJy4h+g2FlkuQB3lf5DjITy8Y6GyydmPYMByjUYAGEXe0brOcP893aalAzRqbNOAjSuAw1lcCLSQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-linux-ia32": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-ia32/-/ngrok-bin-linux-ia32-2.3.41.tgz",
+ "integrity": "sha512-w5Cy31wSz4jYnygEHS7eRizR1yt8s9TX6kHlkjzayIiRTFRb2E1qD2l0/4T2w0LJpBjM5ZFPaaKqsNWgCUIEow==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-linux-x64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-x64/-/ngrok-bin-linux-x64-2.3.41.tgz",
+ "integrity": "sha512-LcU3MbYHv7Sn2eFz8Yzo2rXduufOvX1/hILSirwCkH+9G8PYzpwp2TeGqVWuO+EmvtBe6NEYwgdQjJjN6I4L1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-sunos-x64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-sunos-x64/-/ngrok-bin-sunos-x64-2.3.41.tgz",
+ "integrity": "sha512-bcOj45BLhiV2PayNmLmEVZlFMhEiiGpOr36BXC0XSL+cHUZHd6uNaS28AaZdz95lrRzGpeb0hAF8cuJjo6nq4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-win32-ia32": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-win32-ia32/-/ngrok-bin-win32-ia32-2.3.41.tgz",
+ "integrity": "sha512-0+vPbKvUA+a9ERgiAknmZCiWA3AnM5c6beI+51LqmjKEM4iAAlDmfXNJ89aAbvZMUtBNwEPHzJHnaM4s2SeBhA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@expo/ngrok-bin-win32-x64": {
+ "version": "2.3.41",
+ "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-win32-x64/-/ngrok-bin-win32-x64-2.3.41.tgz",
+ "integrity": "sha512-mncsPRaG462LiYrM8mQT8OYe3/i44m3N/NzUeieYpGi8+pCOo8TIC23kR9P93CVkbM9mmXsy3X6hq91a8FWBdA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@expo/ngrok/node_modules/uuid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+ "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "bin/uuid"
+ }
+ },
+ "node_modules/@expo/ngrok/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/@expo/osascript": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.2.5.tgz",
@@ -2284,6 +2499,18 @@
"integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==",
"license": "MIT"
},
+ "node_modules/@expo/server": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@expo/server/-/server-0.6.3.tgz",
+ "integrity": "sha512-Ea7NJn9Xk1fe4YeJ86rObHSv/bm3u/6WiQPXEqXJ2GrfYpVab2Swoh9/PnSM3KjR64JAgKjArDn1HiPjITCfHA==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "debug": "^4.3.4",
+ "source-map-support": "~0.5.21",
+ "undici": "^6.18.2 || ^7.0.0"
+ }
+ },
"node_modules/@expo/spawn-async": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz",
@@ -2737,6 +2964,39 @@
"node": ">=14"
}
},
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
+ "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"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",
@@ -3299,6 +3559,117 @@
}
}
},
+ "node_modules/@react-navigation/bottom-tabs": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.5.tgz",
+ "integrity": "sha512-Zm9UOTfEtBLL7Wm+JBc0v/lh72cen9a8WVN5KSCEN7EtiQIPXbQUZg1ktEzme600HhxvaNZzzSz0X+w2E5nG5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.8.2",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.1.20",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/core": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.1.tgz",
+ "integrity": "sha512-aPf1vjQhMytPC9CmJu28hT5eTaBJuqIf9T6IRICtap5HHgFLrsYizLZrg3D0H2AoPyOoijMPWzwf7VCBzfGvrg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/routers": "^7.5.1",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "query-string": "^7.1.3",
+ "react-is": "^19.1.0",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0"
+ }
+ },
+ "node_modules/@react-navigation/core/node_modules/react-is": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
+ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
+ "license": "MIT"
+ },
+ "node_modules/@react-navigation/elements": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.2.tgz",
+ "integrity": "sha512-K5NWIMar81oAoRAgLwrWcLpXzY2K5yG3gNU/56uyC12u+i5SyIVAv+ygP36UXvrNLzDigg8OdRSdEBb8ePqQtA==",
+ "license": "MIT",
+ "dependencies": {
+ "color": "^4.2.3",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@react-native-masked-view/masked-view": ">= 0.2.0",
+ "@react-navigation/native": "^7.1.20",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-masked-view/masked-view": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-navigation/native": {
+ "version": "7.1.20",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.20.tgz",
+ "integrity": "sha512-15luFq+35M2IOMHgbTJ0XDkPY7gm3YlR3yQKTuOTOHs+EeAUX71DlUuqcWMRqB0tt+OT6HimDQR7OboTB0N30g==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/core": "^7.13.1",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "use-latest-callback": "^0.2.4"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0",
+ "react-native": "*"
+ }
+ },
+ "node_modules/@react-navigation/native-stack": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.6.3.tgz",
+ "integrity": "sha512-F0f0+3K1mVWiQEZbyZen0LAl7Gc4qpbWM4Tpva5aCqBAECZyn7/uLbVhSXtC/EwzMqQ+ojPLtceFQhXhJqfqfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.8.2",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0",
+ "warn-once": "^0.1.1"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.1.20",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/routers": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz",
+ "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==",
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -3329,6 +3700,19 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"license": "MIT"
},
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
@@ -3359,6 +3743,19 @@
"react-native": "*"
}
},
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3400,6 +3797,19 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -3409,6 +3819,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "dev": true,
+ "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",
@@ -3433,6 +3850,16 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "24.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
@@ -3452,6 +3879,16 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -4187,6 +4624,51 @@
"node": ">= 0.8"
}
},
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -4387,6 +4869,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -4451,6 +4939,32 @@
"node": ">=0.8"
}
},
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4469,6 +4983,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@@ -4694,6 +5218,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -4724,6 +5286,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@@ -4853,6 +5425,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -5199,52 +5781,194 @@
"expo": "*"
}
},
- "node_modules/expo-dev-menu-interface": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz",
- "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==",
+ "node_modules/expo-dev-menu-interface": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz",
+ "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-file-system": {
+ "version": "18.1.11",
+ "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz",
+ "integrity": "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-font": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz",
+ "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==",
+ "license": "MIT",
+ "dependencies": {
+ "fontfaceobserver": "^2.1.0"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
+ }
+ },
+ "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": "14.1.4",
+ "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz",
+ "integrity": "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
+ }
+ },
+ "node_modules/expo-linking": {
+ "version": "8.0.9",
+ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz",
+ "integrity": "sha512-a0UHhlVyfwIbn8b1PSFPoFiIDJeps2iEq109hVH3CHd0CMKuRxFfNio9Axe2BjXhiJCYWR4OV1iIyzY/GjiVkQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "expo-constants": "~18.0.10",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-linking/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",
+ "peer": true,
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/expo-linking/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",
+ "peer": true,
+ "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-linking/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",
+ "peer": true,
+ "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-linking/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",
+ "peer": true
+ },
+ "node_modules/expo-linking/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",
+ "peer": true,
+ "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-linking/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",
- "peerDependencies": {
- "expo": "*"
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "~7.10.4",
+ "json5": "^2.2.3"
}
},
- "node_modules/expo-file-system": {
- "version": "18.1.11",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz",
- "integrity": "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ==",
+ "node_modules/expo-linking/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",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
+ "peer": true,
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.2.3",
+ "xmlbuilder": "^15.1.1"
}
},
- "node_modules/expo-font": {
- "version": "13.3.2",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz",
- "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==",
+ "node_modules/expo-linking/node_modules/expo-constants": {
+ "version": "18.0.10",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
+ "integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
- "fontfaceobserver": "^2.1.0"
+ "@expo/config": "~12.0.10",
+ "@expo/env": "~2.0.7"
},
"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": "14.1.4",
- "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz",
- "integrity": "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
+ "node_modules/expo-linking/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",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/expo-manifests": {
@@ -5297,6 +6021,60 @@
"invariant": "^2.2.4"
}
},
+ "node_modules/expo-router": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.1.7.tgz",
+ "integrity": "sha512-E7hIqTZs4Cub4sbYPeednfYPi+2cyRGMdqc5IYBJ/vC+WBKoYJ8C9eU13ZLbPz//ZybSo2Dsm7v89uFIlO2Gow==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/metro-runtime": "5.0.5",
+ "@expo/schema-utils": "^0.1.0",
+ "@expo/server": "^0.6.3",
+ "@radix-ui/react-slot": "1.2.0",
+ "@react-navigation/bottom-tabs": "^7.3.10",
+ "@react-navigation/native": "^7.1.6",
+ "@react-navigation/native-stack": "^7.3.10",
+ "client-only": "^0.0.1",
+ "invariant": "^2.2.4",
+ "react-fast-compare": "^3.2.2",
+ "react-native-is-edge-to-edge": "^1.1.6",
+ "semver": "~7.6.3",
+ "server-only": "^0.0.1",
+ "shallowequal": "^1.1.0"
+ },
+ "peerDependencies": {
+ "@react-navigation/drawer": "^7.3.9",
+ "expo": "*",
+ "expo-constants": "*",
+ "expo-linking": "*",
+ "react-native-reanimated": "*",
+ "react-native-safe-area-context": "*",
+ "react-native-screens": "*"
+ },
+ "peerDependenciesMeta": {
+ "@react-navigation/drawer": {
+ "optional": true
+ },
+ "@testing-library/jest-native": {
+ "optional": true
+ },
+ "react-native-reanimated": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/expo-status-bar": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.2.3.tgz",
@@ -5430,6 +6208,15 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -5745,6 +6532,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5818,6 +6631,13 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -5843,6 +6663,20 @@
"node": ">= 0.8"
}
},
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -6428,6 +7262,13 @@
"node": ">=6"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -6469,6 +7310,16 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -7009,6 +7860,16 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -7563,6 +8424,16 @@
"node": ">=6"
}
},
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -7724,6 +8595,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/npm-package-arg": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
@@ -7910,6 +8794,16 @@
"node": ">=8"
}
},
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -8236,6 +9130,17 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8269,6 +9174,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/query-string": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
+ "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+ "license": "MIT",
+ "dependencies": {
+ "decode-uri-component": "^0.2.2",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -8299,6 +9222,19 @@
],
"license": "MIT"
},
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -8379,6 +9315,24 @@
}
}
},
+ "node_modules/react-fast-compare": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
+ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-freeze": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
+ "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -8483,6 +9437,30 @@
"react-native": "*"
}
},
+ "node_modules/react-native-safe-area-context": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
+ "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-screens": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz",
+ "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-freeze": "^1.0.0",
+ "warn-once": "^0.1.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
"version": "0.79.6",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.6.tgz",
@@ -8738,6 +9716,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -8762,6 +9747,19 @@
"node": ">=10"
}
},
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -9089,6 +10087,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/server-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
+ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
+ "license": "MIT"
+ },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -9108,6 +10112,21 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/sf-symbols-typescript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.1.0.tgz",
+ "integrity": "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -9246,6 +10265,21 @@
"node": ">= 5.10.0"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -9352,6 +10386,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -9415,6 +10458,15 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -10008,6 +11060,24 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-latest-callback": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
+ "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10066,6 +11136,12 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/warn-once": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
+ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
+ "license": "MIT"
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
diff --git a/app/package.json b/app/package.json
index 91ab6690..9a3e3b64 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,7 +1,7 @@
{
"name": "friend-lite-app",
"version": "1.0.0",
- "main": "node_modules/expo/AppEntry.js",
+ "main": "expo-router/entry",
"scripts": {
"start": "expo start",
"android": "expo run:android",
@@ -18,6 +18,7 @@
"expo": "~53.0.9",
"expo-build-properties": "~0.14.8",
"expo-dev-client": "~5.2.4",
+ "expo-router": "~5.1.7",
"expo-status-bar": "~2.2.3",
"friend-lite-react-native": "^1.0.2",
"install": "^0.13.0",
@@ -26,11 +27,14 @@
"react-native": "0.79.6",
"react-native-base64": "^0.2.1",
"react-native-ble-plx": "^3.5.0",
+ "react-native-safe-area-context": "^5.6.2",
+ "react-native-screens": "^4.18.0",
"setimmediate": "^1.0.5",
"webidl-conversions": "^7.0.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
+ "@expo/ngrok": "^4.1.3",
"@react-native-community/cli": "latest",
"@types/react": "~19.0.10",
"typescript": "~5.8.3"
diff --git a/app/app/components/AuthSection.tsx b/app/src/components/AuthSection.tsx
similarity index 100%
rename from app/app/components/AuthSection.tsx
rename to app/src/components/AuthSection.tsx
diff --git a/app/app/components/BackendStatus.tsx b/app/src/components/BackendStatus.tsx
similarity index 100%
rename from app/app/components/BackendStatus.tsx
rename to app/src/components/BackendStatus.tsx
diff --git a/app/app/components/BluetoothStatusBanner.tsx b/app/src/components/BluetoothStatusBanner.tsx
similarity index 100%
rename from app/app/components/BluetoothStatusBanner.tsx
rename to app/src/components/BluetoothStatusBanner.tsx
diff --git a/app/app/components/DeviceDetails.tsx b/app/src/components/DeviceDetails.tsx
similarity index 100%
rename from app/app/components/DeviceDetails.tsx
rename to app/src/components/DeviceDetails.tsx
diff --git a/app/app/components/DeviceListItem.tsx b/app/src/components/DeviceListItem.tsx
similarity index 100%
rename from app/app/components/DeviceListItem.tsx
rename to app/src/components/DeviceListItem.tsx
diff --git a/app/app/components/PhoneAudioButton.tsx b/app/src/components/PhoneAudioButton.tsx
similarity index 100%
rename from app/app/components/PhoneAudioButton.tsx
rename to app/src/components/PhoneAudioButton.tsx
diff --git a/app/app/components/ScanControls.tsx b/app/src/components/ScanControls.tsx
similarity index 100%
rename from app/app/components/ScanControls.tsx
rename to app/src/components/ScanControls.tsx
diff --git a/app/app/components/StatusIndicator.tsx b/app/src/components/StatusIndicator.tsx
similarity index 100%
rename from app/app/components/StatusIndicator.tsx
rename to app/src/components/StatusIndicator.tsx
diff --git a/app/app/hooks/.gitkeep b/app/src/hooks/.gitkeep
similarity index 100%
rename from app/app/hooks/.gitkeep
rename to app/src/hooks/.gitkeep
diff --git a/app/app/hooks/useAudioListener.ts b/app/src/hooks/useAudioListener.ts
similarity index 100%
rename from app/app/hooks/useAudioListener.ts
rename to app/src/hooks/useAudioListener.ts
diff --git a/app/app/hooks/useAudioStreamer.ts b/app/src/hooks/useAudioStreamer.ts
similarity index 100%
rename from app/app/hooks/useAudioStreamer.ts
rename to app/src/hooks/useAudioStreamer.ts
diff --git a/app/src/hooks/useBluetoothLogger.ts b/app/src/hooks/useBluetoothLogger.ts
new file mode 100644
index 00000000..4ef4c48e
--- /dev/null
+++ b/app/src/hooks/useBluetoothLogger.ts
@@ -0,0 +1,194 @@
+import { useCallback, useRef } from 'react';
+import {
+ saveBluetoothLog,
+ startSession,
+ endCurrentSession,
+ updateSession,
+ generateId,
+ truncateDeviceId,
+ BluetoothEvent,
+ BluetoothLogEntry,
+} from '../utils/bluetoothLogger';
+
+interface UseBluetoothLogger {
+ logScanStart: () => Promise;
+ logScanStop: () => Promise;
+ logDeviceFound: (deviceId: string, deviceName?: string) => Promise;
+ logConnectionAttempt: (deviceId: string, deviceName?: string, isAutoReconnect?: boolean) => Promise;
+ logConnectionSuccess: (deviceId: string, deviceName?: string, sessionId?: string) => Promise;
+ logConnectionFailure: (deviceId: string, errorMessage: string, sessionId?: string) => Promise;
+ logDisconnect: (deviceId: string, deviceName?: string) => Promise;
+ logStateChange: (deviceId: string, state: string) => Promise;
+ logError: (deviceId: string, errorMessage: string) => Promise;
+}
+
+export const useBluetoothLogger = (): UseBluetoothLogger => {
+ const currentSessionIdRef = useRef(null);
+
+ const logScanStart = useCallback(async (): Promise => {
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.SCAN_START,
+ details: {},
+ };
+ await saveBluetoothLog(entry);
+ }, []);
+
+ const logScanStop = useCallback(async (): Promise => {
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.SCAN_STOP,
+ details: {},
+ };
+ await saveBluetoothLog(entry);
+ }, []);
+
+ const logDeviceFound = useCallback(async (deviceId: string, deviceName?: string): Promise => {
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.DEVICE_FOUND,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ deviceName,
+ },
+ };
+ await saveBluetoothLog(entry);
+ }, []);
+
+ const logConnectionAttempt = useCallback(
+ async (deviceId: string, deviceName?: string, isAutoReconnect: boolean = false): Promise => {
+ // Start a new session
+ const sessionId = await startSession(deviceId, deviceName);
+ currentSessionIdRef.current = sessionId;
+
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: isAutoReconnect ? BluetoothEvent.AUTO_RECONNECT_ATTEMPT : BluetoothEvent.CONNECT_ATTEMPT,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ deviceName,
+ isAutoReconnect,
+ },
+ sessionId,
+ };
+ await saveBluetoothLog(entry);
+
+ return sessionId;
+ },
+ []
+ );
+
+ const logConnectionSuccess = useCallback(
+ async (deviceId: string, deviceName?: string, sessionId?: string): Promise => {
+ const activeSessionId = sessionId || currentSessionIdRef.current;
+
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.CONNECT_SUCCESS,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ deviceName,
+ success: true,
+ },
+ sessionId: activeSessionId || undefined,
+ };
+ await saveBluetoothLog(entry);
+
+ // Update session status
+ if (activeSessionId) {
+ await updateSession({ status: 'active' });
+ }
+ },
+ []
+ );
+
+ const logConnectionFailure = useCallback(
+ async (deviceId: string, errorMessage: string, sessionId?: string): Promise => {
+ const activeSessionId = sessionId || currentSessionIdRef.current;
+
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.CONNECT_FAILURE,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ errorMessage,
+ success: false,
+ },
+ sessionId: activeSessionId || undefined,
+ };
+ await saveBluetoothLog(entry);
+
+ // Update session status
+ if (activeSessionId) {
+ await updateSession({ status: 'failed' });
+ }
+
+ // Clear session reference
+ currentSessionIdRef.current = null;
+ },
+ []
+ );
+
+ const logDisconnect = useCallback(async (deviceId: string, deviceName?: string): Promise => {
+ // End the current session (which will log the disconnect event with duration)
+ await endCurrentSession();
+
+ // Clear session reference
+ currentSessionIdRef.current = null;
+ }, []);
+
+ const logStateChange = useCallback(async (deviceId: string, state: string): Promise => {
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.STATE_CHANGE,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ state,
+ },
+ sessionId: currentSessionIdRef.current || undefined,
+ };
+ await saveBluetoothLog(entry);
+ }, []);
+
+ const logError = useCallback(async (deviceId: string, errorMessage: string): Promise => {
+ const entry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: Date.now(),
+ event: BluetoothEvent.ERROR,
+ details: {
+ deviceId: truncateDeviceId(deviceId),
+ errorMessage,
+ },
+ sessionId: currentSessionIdRef.current || undefined,
+ };
+ await saveBluetoothLog(entry);
+
+ // Update session error count
+ if (currentSessionIdRef.current) {
+ const currentSession = await import('../utils/bluetoothLogger').then(m => m.getCurrentSession());
+ const session = await currentSession;
+ if (session) {
+ await updateSession({ totalErrors: session.totalErrors + 1 });
+ }
+ }
+ }, []);
+
+ return {
+ logScanStart,
+ logScanStop,
+ logDeviceFound,
+ logConnectionAttempt,
+ logConnectionSuccess,
+ logConnectionFailure,
+ logDisconnect,
+ logStateChange,
+ logError,
+ };
+};
diff --git a/app/app/hooks/useBluetoothManager.ts b/app/src/hooks/useBluetoothManager.ts
similarity index 100%
rename from app/app/hooks/useBluetoothManager.ts
rename to app/src/hooks/useBluetoothManager.ts
diff --git a/app/app/hooks/useDeviceConnection.ts b/app/src/hooks/useDeviceConnection.ts
similarity index 79%
rename from app/app/hooks/useDeviceConnection.ts
rename to app/src/hooks/useDeviceConnection.ts
index e729169e..ad4b442a 100644
--- a/app/app/hooks/useDeviceConnection.ts
+++ b/app/src/hooks/useDeviceConnection.ts
@@ -1,6 +1,7 @@
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useRef } from 'react';
import { Alert } from 'react-native';
import { OmiConnection, BleAudioCodec, OmiDevice } from 'friend-lite-react-native';
+import { useBluetoothLogger } from './useBluetoothLogger';
interface UseDeviceConnection {
connectedDevice: OmiDevice | null;
@@ -25,24 +26,39 @@ export const useDeviceConnection = (
const [batteryLevel, setBatteryLevel] = useState(-1);
const [connectedDeviceId, setConnectedDeviceId] = useState(null);
+ // Bluetooth logger
+ const bluetoothLogger = useBluetoothLogger();
+ const currentSessionIdRef = useRef(null);
+
const handleConnectionStateChange = useCallback((id: string, state: string) => {
console.log(`Device ${id} connection state: ${state}`);
const isNowConnected = state === 'connected';
setIsConnecting(false);
+ // Log state change
+ bluetoothLogger.logStateChange(id, state);
+
if (isNowConnected) {
setConnectedDeviceId(id);
+
+ // Log successful connection
+ bluetoothLogger.logConnectionSuccess(id, undefined, currentSessionIdRef.current || undefined);
+
// Potentially fetch the device details from omiConnection if needed to set connectedDevice
// For now, we'll assume the app manages the full OmiDevice object elsewhere or doesn't need it here.
if (onConnect) onConnect();
} else {
+ // Log disconnect
+ bluetoothLogger.logDisconnect(id);
+ currentSessionIdRef.current = null;
+
setConnectedDeviceId(null);
setConnectedDevice(null);
setCurrentCodec(null);
setBatteryLevel(-1);
- if (onDisconnect) onDisconnect();
+ if (onDisconnect) onDisconnect();
}
- }, [onDisconnect, onConnect]);
+ }, [onDisconnect, onConnect, bluetoothLogger]);
const connectToDevice = useCallback(async (deviceId: string) => {
if (connectedDeviceId && connectedDeviceId !== deviceId) {
@@ -59,6 +75,10 @@ export const useDeviceConnection = (
setCurrentCodec(null);
setBatteryLevel(-1);
+ // Log connection attempt and get session ID
+ const sessionId = await bluetoothLogger.logConnectionAttempt(deviceId);
+ currentSessionIdRef.current = sessionId;
+
try {
const success = await omiConnection.connect(deviceId, handleConnectionStateChange);
if (success) {
@@ -66,6 +86,9 @@ export const useDeviceConnection = (
// Note: actual connected state is set by handleConnectionStateChange callback
} else {
setIsConnecting(false);
+ // Log connection failure
+ await bluetoothLogger.logConnectionFailure(deviceId, 'Connection returned false', sessionId);
+ currentSessionIdRef.current = null;
Alert.alert('Connection Failed', 'Could not connect to the device. Please try again.');
}
} catch (error) {
@@ -73,9 +96,14 @@ export const useDeviceConnection = (
setIsConnecting(false);
setConnectedDevice(null);
setConnectedDeviceId(null);
+
+ // Log connection error
+ await bluetoothLogger.logConnectionFailure(deviceId, String(error), sessionId);
+ currentSessionIdRef.current = null;
+
Alert.alert('Connection Error', String(error));
}
- }, [omiConnection, handleConnectionStateChange, connectedDeviceId]); // Added connectedDeviceId
+ }, [omiConnection, handleConnectionStateChange, connectedDeviceId, bluetoothLogger]);
const disconnectFromDevice = useCallback(async () => {
console.log('Attempting to disconnect...');
@@ -91,8 +119,15 @@ export const useDeviceConnection = (
setCurrentCodec(null);
setBatteryLevel(-1);
// The handleConnectionStateChange should also be triggered by the SDK upon disconnection
+ // Logging will happen in handleConnectionStateChange
} catch (error) {
console.error('Disconnect error:', error);
+
+ // Log disconnect error
+ if (connectedDeviceId) {
+ await bluetoothLogger.logError(connectedDeviceId, `Disconnect error: ${String(error)}`);
+ }
+
Alert.alert('Disconnect Error', String(error));
// Even if disconnect fails, reset state as we intend to be disconnected
setConnectedDevice(null);
@@ -100,7 +135,7 @@ export const useDeviceConnection = (
setCurrentCodec(null);
setBatteryLevel(-1);
}
- }, [omiConnection, onDisconnect]);
+ }, [omiConnection, onDisconnect, connectedDeviceId, bluetoothLogger]);
const getAudioCodec = useCallback(async () => {
if (!omiConnection.isConnected() || !connectedDeviceId) {
diff --git a/app/app/hooks/useDeviceScanning.ts b/app/src/hooks/useDeviceScanning.ts
similarity index 100%
rename from app/app/hooks/useDeviceScanning.ts
rename to app/src/hooks/useDeviceScanning.ts
diff --git a/app/app/hooks/usePhoneAudioRecorder.ts b/app/src/hooks/usePhoneAudioRecorder.ts
similarity index 100%
rename from app/app/hooks/usePhoneAudioRecorder.ts
rename to app/src/hooks/usePhoneAudioRecorder.ts
diff --git a/app/src/utils/bluetoothLogger.ts b/app/src/utils/bluetoothLogger.ts
new file mode 100644
index 00000000..34d77ad3
--- /dev/null
+++ b/app/src/utils/bluetoothLogger.ts
@@ -0,0 +1,299 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+// Storage keys
+const BLUETOOTH_LOGS_KEY = 'BLUETOOTH_LOGS';
+const BLUETOOTH_STATS_KEY = 'BLUETOOTH_STATS';
+const CURRENT_SESSION_KEY = 'BLUETOOTH_CURRENT_SESSION';
+const MAX_LOG_ENTRIES = 500;
+const LOG_RETENTION_DAYS = 7;
+
+// Enums
+export enum BluetoothEvent {
+ SCAN_START = 'scan_start',
+ SCAN_STOP = 'scan_stop',
+ DEVICE_FOUND = 'device_found',
+ CONNECT_ATTEMPT = 'connect_attempt',
+ CONNECT_SUCCESS = 'connect_success',
+ CONNECT_FAILURE = 'connect_failure',
+ DISCONNECT = 'disconnect',
+ AUTO_RECONNECT_ATTEMPT = 'auto_reconnect_attempt',
+ AUTO_RECONNECT_SUCCESS = 'auto_reconnect_success',
+ AUTO_RECONNECT_FAILURE = 'auto_reconnect_failure',
+ STATE_CHANGE = 'state_change',
+ ERROR = 'error',
+}
+
+// Interfaces
+export interface BluetoothLogEntry {
+ id: string;
+ timestamp: number;
+ event: BluetoothEvent;
+ details: {
+ deviceId?: string;
+ deviceName?: string;
+ state?: string;
+ errorMessage?: string;
+ duration?: number;
+ success?: boolean;
+ isAutoReconnect?: boolean;
+ };
+ sessionId?: string;
+}
+
+export interface BluetoothSession {
+ sessionId: string;
+ deviceId: string;
+ deviceName?: string;
+ startTime: number;
+ endTime?: number;
+ status: 'active' | 'completed' | 'failed';
+ totalConnectAttempts: number;
+ totalDisconnects: number;
+ totalErrors: number;
+ connectionDuration?: number;
+}
+
+export interface BluetoothStats {
+ totalSessions: number;
+ totalConnections: number;
+ successfulConnections: number;
+ failedConnections: number;
+ totalDisconnects: number;
+ averageSessionDuration: number;
+ lastConnectionTime?: number;
+ currentSession?: BluetoothSession;
+}
+
+// Helper functions
+export const generateId = (): string => {
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+};
+
+export const truncateDeviceId = (deviceId: string): string => {
+ // Keep only last 8 characters for privacy
+ if (deviceId.length > 8) {
+ return `...${deviceId.slice(-8)}`;
+ }
+ return deviceId;
+};
+
+export const isLogExpired = (timestamp: number): boolean => {
+ const now = Date.now();
+ const dayInMs = 24 * 60 * 60 * 1000;
+ return now - timestamp > LOG_RETENTION_DAYS * dayInMs;
+};
+
+// Storage functions
+export const saveBluetoothLog = async (entry: BluetoothLogEntry): Promise => {
+ try {
+ const existingLogs = await getBluetoothLogs();
+
+ // Filter out expired logs
+ const validLogs = existingLogs.filter(log => !isLogExpired(log.timestamp));
+
+ // Add new log and limit to MAX_LOG_ENTRIES
+ const newLogs = [entry, ...validLogs].slice(0, MAX_LOG_ENTRIES);
+
+ await AsyncStorage.setItem(BLUETOOTH_LOGS_KEY, JSON.stringify(newLogs));
+ console.log('[BluetoothLogger] Log saved:', entry.event);
+
+ // Update stats
+ await updateBluetoothStats(entry);
+ } catch (error) {
+ console.error('[BluetoothLogger] Error saving log:', error);
+ }
+};
+
+export const getBluetoothLogs = async (): Promise => {
+ try {
+ const logs = await AsyncStorage.getItem(BLUETOOTH_LOGS_KEY);
+ if (!logs) return [];
+
+ const parsedLogs = JSON.parse(logs);
+
+ // Filter out expired logs
+ return parsedLogs.filter((log: BluetoothLogEntry) => !isLogExpired(log.timestamp));
+ } catch (error) {
+ console.error('[BluetoothLogger] Error reading logs:', error);
+ return [];
+ }
+};
+
+export const clearBluetoothLogs = async (): Promise => {
+ try {
+ await AsyncStorage.removeItem(BLUETOOTH_LOGS_KEY);
+ await AsyncStorage.removeItem(BLUETOOTH_STATS_KEY);
+ await AsyncStorage.removeItem(CURRENT_SESSION_KEY);
+ console.log('[BluetoothLogger] All logs and stats cleared');
+ } catch (error) {
+ console.error('[BluetoothLogger] Error clearing logs:', error);
+ }
+};
+
+export const getBluetoothStats = async (): Promise => {
+ try {
+ const statsJson = await AsyncStorage.getItem(BLUETOOTH_STATS_KEY);
+ if (!statsJson) {
+ return {
+ totalSessions: 0,
+ totalConnections: 0,
+ successfulConnections: 0,
+ failedConnections: 0,
+ totalDisconnects: 0,
+ averageSessionDuration: 0,
+ };
+ }
+ return JSON.parse(statsJson);
+ } catch (error) {
+ console.error('[BluetoothLogger] Error reading stats:', error);
+ return {
+ totalSessions: 0,
+ totalConnections: 0,
+ successfulConnections: 0,
+ failedConnections: 0,
+ totalDisconnects: 0,
+ averageSessionDuration: 0,
+ };
+ }
+};
+
+export const updateBluetoothStats = async (entry: BluetoothLogEntry): Promise => {
+ try {
+ const stats = await getBluetoothStats();
+
+ switch (entry.event) {
+ case BluetoothEvent.CONNECT_ATTEMPT:
+ stats.totalConnections++;
+ break;
+
+ case BluetoothEvent.CONNECT_SUCCESS:
+ case BluetoothEvent.AUTO_RECONNECT_SUCCESS:
+ stats.successfulConnections++;
+ stats.lastConnectionTime = entry.timestamp;
+ break;
+
+ case BluetoothEvent.CONNECT_FAILURE:
+ case BluetoothEvent.AUTO_RECONNECT_FAILURE:
+ stats.failedConnections++;
+ break;
+
+ case BluetoothEvent.DISCONNECT:
+ stats.totalDisconnects++;
+
+ // Update average session duration if duration is provided
+ if (entry.details.duration) {
+ const totalSessions = stats.totalSessions || 1;
+ const currentAvg = stats.averageSessionDuration || 0;
+ stats.averageSessionDuration =
+ (currentAvg * (totalSessions - 1) + entry.details.duration) / totalSessions;
+ }
+ break;
+ }
+
+ await AsyncStorage.setItem(BLUETOOTH_STATS_KEY, JSON.stringify(stats));
+ } catch (error) {
+ console.error('[BluetoothLogger] Error updating stats:', error);
+ }
+};
+
+export const getCurrentSession = async (): Promise => {
+ try {
+ const sessionJson = await AsyncStorage.getItem(CURRENT_SESSION_KEY);
+ if (!sessionJson) return null;
+ return JSON.parse(sessionJson);
+ } catch (error) {
+ console.error('[BluetoothLogger] Error reading current session:', error);
+ return null;
+ }
+};
+
+export const startSession = async (deviceId: string, deviceName?: string): Promise => {
+ try {
+ const sessionId = generateId();
+ const session: BluetoothSession = {
+ sessionId,
+ deviceId,
+ deviceName,
+ startTime: Date.now(),
+ status: 'active',
+ totalConnectAttempts: 1,
+ totalDisconnects: 0,
+ totalErrors: 0,
+ };
+
+ await AsyncStorage.setItem(CURRENT_SESSION_KEY, JSON.stringify(session));
+
+ // Update stats
+ const stats = await getBluetoothStats();
+ stats.totalSessions++;
+ await AsyncStorage.setItem(BLUETOOTH_STATS_KEY, JSON.stringify(stats));
+
+ console.log('[BluetoothLogger] Session started:', sessionId);
+ return sessionId;
+ } catch (error) {
+ console.error('[BluetoothLogger] Error starting session:', error);
+ return generateId();
+ }
+};
+
+export const updateSession = async (updates: Partial): Promise => {
+ try {
+ const session = await getCurrentSession();
+ if (!session) return;
+
+ const updatedSession = { ...session, ...updates };
+ await AsyncStorage.setItem(CURRENT_SESSION_KEY, JSON.stringify(updatedSession));
+ } catch (error) {
+ console.error('[BluetoothLogger] Error updating session:', error);
+ }
+};
+
+export const endCurrentSession = async (): Promise => {
+ try {
+ const session = await getCurrentSession();
+ if (!session) return;
+
+ const endTime = Date.now();
+ const duration = endTime - session.startTime;
+
+ const completedSession: BluetoothSession = {
+ ...session,
+ endTime,
+ connectionDuration: duration,
+ status: 'completed',
+ };
+
+ // Save completed session as a log entry for history
+ const logEntry: BluetoothLogEntry = {
+ id: generateId(),
+ timestamp: endTime,
+ event: BluetoothEvent.DISCONNECT,
+ details: {
+ deviceId: truncateDeviceId(session.deviceId),
+ deviceName: session.deviceName,
+ duration,
+ },
+ sessionId: session.sessionId,
+ };
+
+ await saveBluetoothLog(logEntry);
+
+ // Clear current session
+ await AsyncStorage.removeItem(CURRENT_SESSION_KEY);
+
+ console.log('[BluetoothLogger] Session ended:', session.sessionId, 'Duration:', duration);
+ } catch (error) {
+ console.error('[BluetoothLogger] Error ending session:', error);
+ }
+};
+
+export const getRecentLogs = async (limit: number = 20): Promise => {
+ const logs = await getBluetoothLogs();
+ return logs.slice(0, limit);
+};
+
+export const getLogsInLastHours = async (hours: number): Promise => {
+ const logs = await getBluetoothLogs();
+ const cutoffTime = Date.now() - hours * 60 * 60 * 1000;
+ return logs.filter(log => log.timestamp >= cutoffTime);
+};
diff --git a/app/app/utils/storage.ts b/app/src/utils/storage.ts
similarity index 100%
rename from app/app/utils/storage.ts
rename to app/src/utils/storage.ts