From b3f0eac548a3ea912098b52e470089e0aa8809fb Mon Sep 17 00:00:00 2001
From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com>
Date: Thu, 20 Nov 2025 21:38:59 +0530
Subject: [PATCH 1/2] Update app configuration and structure: Added iOS support
to .gitignore, modified app.json for new scheme and package identifiers,
refactored App.tsx to use Expo Router, and introduced install.sh for iOS app
setup. Added new components for Bluetooth management and audio streaming,
along with hooks for device connection and scanning. Enhanced logging and
storage utilities for better state management.
---
app/.gitignore | 3 +-
app/App.tsx | 14 +-
app/app.json | 15 +-
app/app/_layout.tsx | 14 +-
app/app/index.tsx | 59 +-
app/app/stats.tsx | 512 ++++++++
app/install.sh | 147 +++
app/package-lock.json | 1146 ++++++++++++++++-
app/package.json | 4 +
app/{app => src}/components/AuthSection.tsx | 0
app/{app => src}/components/BackendStatus.tsx | 0
.../components/BluetoothStatusBanner.tsx | 0
app/{app => src}/components/DeviceDetails.tsx | 0
.../components/DeviceListItem.tsx | 0
.../components/PhoneAudioButton.tsx | 0
app/{app => src}/components/ScanControls.tsx | 0
.../components/StatusIndicator.tsx | 0
app/{app => src}/hooks/.gitkeep | 0
app/{app => src}/hooks/useAudioListener.ts | 0
app/{app => src}/hooks/useAudioStreamer.ts | 0
app/src/hooks/useBluetoothLogger.ts | 194 +++
app/{app => src}/hooks/useBluetoothManager.ts | 0
app/{app => src}/hooks/useDeviceConnection.ts | 45 +-
app/{app => src}/hooks/useDeviceScanning.ts | 0
.../hooks/usePhoneAudioRecorder.ts | 0
app/src/utils/bluetoothLogger.ts | 299 +++++
app/{app => src}/utils/storage.ts | 0
27 files changed, 2385 insertions(+), 67 deletions(-)
create mode 100644 app/app/stats.tsx
create mode 100755 app/install.sh
rename app/{app => src}/components/AuthSection.tsx (100%)
rename app/{app => src}/components/BackendStatus.tsx (100%)
rename app/{app => src}/components/BluetoothStatusBanner.tsx (100%)
rename app/{app => src}/components/DeviceDetails.tsx (100%)
rename app/{app => src}/components/DeviceListItem.tsx (100%)
rename app/{app => src}/components/PhoneAudioButton.tsx (100%)
rename app/{app => src}/components/ScanControls.tsx (100%)
rename app/{app => src}/components/StatusIndicator.tsx (100%)
rename app/{app => src}/hooks/.gitkeep (100%)
rename app/{app => src}/hooks/useAudioListener.ts (100%)
rename app/{app => src}/hooks/useAudioStreamer.ts (100%)
create mode 100644 app/src/hooks/useBluetoothLogger.ts
rename app/{app => src}/hooks/useBluetoothManager.ts (100%)
rename app/{app => src}/hooks/useDeviceConnection.ts (79%)
rename app/{app => src}/hooks/useDeviceScanning.ts (100%)
rename app/{app => src}/hooks/usePhoneAudioRecorder.ts (100%)
create mode 100644 app/src/utils/bluetoothLogger.ts
rename app/{app => src}/utils/storage.ts (100%)
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
index 60e44938..71b670ec 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -1,3 +1,11 @@
-// App.tsx
-import App from './app/index'; // your actual entry file
-export default App;
\ No newline at end of file
+// App.tsx - Expo Router Entry Point
+import { registerRootComponent } from 'expo';
+import { ExpoRoot } from 'expo-router';
+
+// Must be exported or Fast Refresh won't update the context
+export function App() {
+ const ctx = require.context('./app');
+ return ;
+}
+
+registerRootComponent(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..7ad0c0d4 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"
@@ -77,6 +81,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2017,6 +2022,218 @@
"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",
+ "peer": true,
+ "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 +2501,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 +2966,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",
@@ -2755,6 +3017,7 @@
"integrity": "sha512-Q7UnBqOO/JsWfgmO9qZjrKgMi/0U9ih0FywXXheml8VH1hn/pBXKIeO/BvzA6g5gHIvBZ/6KyhdGoNok1R/ZJw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@react-native-community/cli-clean": "20.0.1",
"@react-native-community/cli-config": "20.0.1",
@@ -3299,6 +3562,118 @@
}
}
},
+ "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",
+ "peer": true,
+ "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 +3704,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 +3747,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 +3801,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 +3823,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 +3854,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",
@@ -3448,10 +3879,21 @@
"integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"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",
@@ -4126,6 +4568,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@@ -4187,6 +4630,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 +4875,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 +4945,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 +4989,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 +5224,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 +5292,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 +5431,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",
@@ -5043,6 +5631,7 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.22.tgz",
"integrity": "sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.24.21",
@@ -5131,6 +5720,7 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz",
"integrity": "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@expo/config": "~11.0.12",
"@expo/env": "~1.0.7"
@@ -5199,52 +5789,185 @@
"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",
+ "peer": true,
+ "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",
+ "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",
+ "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",
+ "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",
+ "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"
+ },
+ "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",
+ "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": "*"
+ "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": "*"
+ "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",
"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",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/expo-manifests": {
@@ -5297,6 +6020,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 +6207,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 +6531,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 +6630,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 +6662,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 +7261,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 +7309,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 +7859,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 +8423,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 +8594,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 +8793,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 +9129,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 +9173,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 +9221,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",
@@ -8344,6 +9279,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -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",
@@ -8390,6 +9344,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.6.tgz",
"integrity": "sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.79.6",
@@ -8483,6 +9438,32 @@
"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",
+ "peer": true,
+ "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",
+ "peer": true,
+ "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 +9719,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 +9750,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 +10090,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 +10115,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 +10268,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 +10389,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 +10461,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",
@@ -9875,6 +10930,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10008,6 +11064,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 +11140,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..66a57630 100644
--- a/app/package.json
+++ b/app/package.json
@@ -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
From 197649ec6d928a12e40d222348d2b508fafeff9b Mon Sep 17 00:00:00 2001
From: Roshan John <63011948+roshatron2@users.noreply.github.com>
Date: Sat, 22 Nov 2025 17:05:35 +0530
Subject: [PATCH 2/2] Remove App.tsx and update package.json main entry to use
expo-router. Clean up package-lock.json by removing unnecessary peer
dependencies.
---
app/App.tsx | 11 -----------
app/package-lock.json | 26 +++++++++++---------------
app/package.json | 2 +-
3 files changed, 12 insertions(+), 27 deletions(-)
delete mode 100644 app/App.tsx
diff --git a/app/App.tsx b/app/App.tsx
deleted file mode 100644
index 71b670ec..00000000
--- a/app/App.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-// App.tsx - Expo Router Entry Point
-import { registerRootComponent } from 'expo';
-import { ExpoRoot } from 'expo-router';
-
-// Must be exported or Fast Refresh won't update the context
-export function App() {
- const ctx = require.context('./app');
- return ;
-}
-
-registerRootComponent(App);
\ No newline at end of file
diff --git a/app/package-lock.json b/app/package-lock.json
index 7ad0c0d4..9c432c34 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -81,7 +81,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2027,7 +2026,6 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-5.0.5.tgz",
"integrity": "sha512-P8UFTi+YsmiD1BmdTdiIQITzDMcZgronsA3RTQ4QKJjHM3bas11oGzLQOnFaIZnlEV8Rrr3m1m+RHxvnpL+t/A==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"react-native": "*"
}
@@ -3017,7 +3015,6 @@
"integrity": "sha512-Q7UnBqOO/JsWfgmO9qZjrKgMi/0U9ih0FywXXheml8VH1hn/pBXKIeO/BvzA6g5gHIvBZ/6KyhdGoNok1R/ZJw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@react-native-community/cli-clean": "20.0.1",
"@react-native-community/cli-config": "20.0.1",
@@ -3633,7 +3630,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.20.tgz",
"integrity": "sha512-15luFq+35M2IOMHgbTJ0XDkPY7gm3YlR3yQKTuOTOHs+EeAUX71DlUuqcWMRqB0tt+OT6HimDQR7OboTB0N30g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@react-navigation/core": "^7.13.1",
"escape-string-regexp": "^4.0.0",
@@ -3879,7 +3875,6 @@
"integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4568,7 +4563,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@@ -5631,7 +5625,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.22.tgz",
"integrity": "sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.24.21",
@@ -5720,7 +5713,6 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz",
"integrity": "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@expo/config": "~11.0.12",
"@expo/env": "~1.0.7"
@@ -5813,7 +5805,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz",
"integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -5843,6 +5834,7 @@
"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"
@@ -5857,6 +5849,7 @@
"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"
}
@@ -5866,6 +5859,7 @@
"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",
@@ -5887,6 +5881,7 @@
"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",
@@ -5908,13 +5903,15 @@
"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"
+ "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",
@@ -5928,6 +5925,7 @@
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz",
"integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "~7.10.4",
"json5": "^2.2.3"
@@ -5938,6 +5936,7 @@
"resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz",
"integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@xmldom/xmldom": "^0.8.8",
"base64-js": "^1.2.3",
@@ -5949,6 +5948,7 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
"integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@expo/config": "~12.0.10",
"@expo/env": "~2.0.7"
@@ -5963,6 +5963,7 @@
"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"
},
@@ -9279,7 +9280,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9344,7 +9344,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.6.tgz",
"integrity": "sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.79.6",
@@ -9443,7 +9442,6 @@
"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",
- "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -9454,7 +9452,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz",
"integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
@@ -10930,7 +10927,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/app/package.json b/app/package.json
index 66a57630..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",