diff --git a/README.md b/README.md index 1717834..3c832f5 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,14 @@ Tracker app 2. Firebase 3. Google Auth + Build locally and test +npx expo run:android + +To clean and rebuild +npx expo prebuild --clean && npx expo run:android Build for device (apk) - expo always has googleservices.json issue for local as well as remote diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 83eb07b..7240e72 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,7 @@ - + diff --git a/app.config.ts b/app.config.ts index 4864364..21fa143 100644 --- a/app.config.ts +++ b/app.config.ts @@ -11,10 +11,18 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ userInterfaceStyle: "automatic", newArchEnabled: true, scheme: "tracker", // Only define scheme once here + plugins: [ + [ + "expo-notifications", + { + icon: "./assets/images/adaptive-icon.png" + } + ] + ], ios: { supportsTablet: true, - bundleIdentifier: "com.hiteshjoshi.tracker", // Add this - googleServicesFile: "./GoogleService-Info.plist", // Add this if using Firebase + bundleIdentifier: "com.hiteshjoshi.tracker", + googleServicesFile: "./GoogleService-Info.plist", }, android: { adaptiveIcon: { @@ -23,22 +31,20 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, package: "com.hiteshjoshi.tracker", googleServicesFile: "./google-services.json", - versionCode: 1 , - // Add this if not present - intentFilters: [ - { - action: "VIEW", - autoVerify: true, - data: [ - { - scheme: "tracker", - host: "*" - } - ], - category: ["BROWSABLE", "DEFAULT"] - } - ] - + versionCode: 1, + intentFilters: [ + { + action: "VIEW", + autoVerify: true, + data: [ + { + scheme: "tracker", + host: "*" + } + ], + category: ["BROWSABLE", "DEFAULT"] + } + ] }, web: { bundler: "metro", @@ -60,4 +66,4 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ }, enableDebugLogging: true } -}); +}); \ No newline at end of file diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 418634d..3feb87b 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,174 +1,95 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; -import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { router } from 'expo-router'; import { TouchableOpacity, Alert } from 'react-native'; -import { auth } from '../../config/firebase'; +import { useAuth } from '../../context/AuthContext'; +import AuthProtectedRoute from '../../components/AuthProtectedRoute'; export default function TabsLayout() { - useEffect(() => { - // Set up auth state listener for tabs - const unsubscribe = onAuthStateChanged(auth, (user) => { - // If user signs out or no user is found, redirect to login - if (!user) { - console.log('No user found in tabs layout, redirecting to login'); - router.replace('/login'); - } - }); + const { signOut } = useAuth(); - // Check if user exists in AsyncStorage - const checkUser = async () => { - const userJSON = await AsyncStorage.getItem('@user'); - if (!userJSON) { - console.log('No user in AsyncStorage, redirecting to login'); - router.replace('/login'); - } - }; - - checkUser(); - - // Clean up subscription - return () => unsubscribe(); - }, []); - - // Handle logout - const handleLogout = async () => { + const handleLogout = () => { Alert.alert( - 'Logout', - 'Are you sure you want to log out?', + "Sign Out", + "Are you sure you want to sign out?", [ { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Logout', - onPress: async () => { - try { - console.log('Logging out user'); - // Sign out from Firebase - await signOut(auth); - // Clear AsyncStorage user data - await AsyncStorage.removeItem('@user'); - // Navigate to login - router.replace('/login'); - } catch (error) { - console.error('Error during logout:', error); - Alert.alert('Error', 'Failed to log out. Please try again.'); - } - }, + text: "Cancel", + style: "cancel" }, - ], - { cancelable: true } + { + text: "Sign Out", + onPress: () => signOut(), + style: "destructive" + } + ] ); }; + // Create logout button component for header + const LogoutButton = () => ( + + + + ); + return ( - ( - - - - ), - }}> - ( - - ), - }} - /> - ( - - ), + + }} - /> - + > + ( + + ), + }} + /> + + ( + + ), + }} + /> + + {/* Add your future tabs here */} + {/* + ( + + ), + }} + /> + + ( + + ), + }} + /> + */} + + ); -} -// // app/(tabs)/_layout.tsx -// import { Tabs } from 'expo-router'; -// import { useColorScheme, Platform, TouchableOpacity } from 'react-native'; -// import FontAwesome from '@expo/vector-icons/FontAwesome'; -// import { useGoogleAuth } from '../../services/authService'; -// import { useRouter } from 'expo-router'; - -// export default function TabLayout() { -// const colorScheme = useColorScheme(); -// const router = useRouter(); -// // const { handleSignOut } = useGoogleAuth(); - -// return ( -// ( -// -// -// -// ), -// }}> -// ( -// -// ), -// headerTitle: 'Life Companion', -// }} -// /> -// ( -// -// ), -// headerTitle: 'Habits', -// }} -// /> -// -// ); -// } \ No newline at end of file +} \ No newline at end of file diff --git a/app/(tabs)/habits.tsx b/app/(tabs)/habits.tsx index 8cd3dd6..f5f2f56 100644 --- a/app/(tabs)/habits.tsx +++ b/app/(tabs)/habits.tsx @@ -1,252 +1,446 @@ -//app/(tabs)/habits.tsx -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, - TextInput, + Alert, Platform, + ActivityIndicator, } from 'react-native'; import DateHeader from '../../components/DateHeader'; +import AddHabitModal from '../../components/AddHabitModal'; +import HabitDayIndicator from '../../components/HabitDayIndicator'; +import { habitService } from '../../services/habitService'; +import { Habit } from '../../models/types'; +import { format, isToday, parseISO, isSameDay, addDays, subDays } from 'date-fns'; +import { useAuth } from '../../context/AuthContext'; +import { useNotifications } from '../../context/NotificationContext'; -interface WeekDay { - date: number; - day: string; - isToday: boolean; -} - -interface Habit { - id: string; - name: string; - status: 'completed' | 'failed' | 'untracked'; - streak: number; - lastCompleted: Date | null; -} +const DAYS_OF_WEEK = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; const HabitsScreen: React.FC = () => { - const [habits, setHabits] = useState([ - { - id: '1', - name: 'Morning Meditation', - status: 'completed', - streak: 5, - lastCompleted: new Date(), - }, - { - id: '2', - name: 'Reading', - status: 'failed', - streak: 0, - lastCompleted: null, - }, - { - id: '3', - name: 'Exercise', - status: 'untracked', - streak: 3, - lastCompleted: new Date(), - }, - ]); - - const [newHabit, setNewHabit] = useState(''); + const [habits, setHabits] = useState([]); const [isAddingHabit, setIsAddingHabit] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [selectedDate, setSelectedDate] = useState(new Date()); + const { userInfo } = useAuth(); // Access the current user from auth context + const { scheduleDailyReminder, cancelReminder, rescheduleAllReminders } = useNotifications(); // Add this line + + useEffect(() => { + if (userInfo?.uid) { + loadHabits(); + + // Set up real-time listener for habit changes + const unsubscribe = habitService.subscribeToUserItems( + userInfo.uid, + (updatedHabits) => { + setHabits(updatedHabits); + setIsLoading(false); + } + ); + + // Clean up listener on unmount + return () => unsubscribe(); + } + }, [userInfo]); + + const loadHabits = async () => { + if (!userInfo?.uid) return; + + try { + setIsLoading(true); + const userHabits = await habitService.getUserHabits(userInfo.uid); + setHabits(userHabits); + } catch (error) { + console.error('Error loading habits:', error); + Alert.alert('Error', 'Failed to load habits. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const handleAddHabit = async (habitData: Partial): Promise => { + if (!userInfo?.uid) throw new Error("User not logged in"); + + try { + // Add the habit to the database + const habitId = await habitService.addHabit(userInfo.uid, habitData); + + // Note: We don't need to manually schedule notifications here anymore + // The AddHabitModal component will handle that with useNotifications hook + + return habitId; + } catch (error) { + console.error('Error adding habit:', error); + Alert.alert('Error', 'Failed to add habit. Please try again.'); + throw error; + } + }; + + // Handle editing a habit (if you implement an edit feature) + const handleEditHabit = async (habitId: string, updates: Partial) => { + try { + await habitService.updateHabit(habitId, updates); + + // If reminder time or days changed, update notifications + if ('reminderTime' in updates || 'reminderDays' in updates) { + // Get the updated habit + const updatedHabit = habits.find(h => h.id === habitId); + if (updatedHabit) { + // Cancel existing notifications + await cancelReminder(habitId); + + // Schedule new notifications if there's a reminder time + if (updatedHabit.reminderTime) { + await scheduleDailyReminder(updatedHabit); + } + } + } + } catch (error) { + console.error('Error updating habit:', error); + Alert.alert('Error', 'Failed to update habit. Please try again.'); + } + }; + + const updateHabitStatus = async ( + habit: Habit, + newStatus: 'completed' | 'failed' | 'untracked', + date: Date = selectedDate // Use selectedDate as default, allowing custom date parameter + ) => { + try { + await habitService.updateHabitStatusForDate(habit.id, newStatus, date, habit); + } catch (error) { + console.error('Error updating habit status:', error); + Alert.alert('Error', 'Failed to update habit status. Please try again.'); + } + }; + + // Handle deleting a habit (if you implement a delete feature) + const handleDeleteHabit = async (habitId: string) => { + try { + // Cancel notifications first + await cancelReminder(habitId); + + // Then delete the habit + await habitService.deleteHabit(habitId); + } catch (error) { + console.error('Error deleting habit:', error); + Alert.alert('Error', 'Failed to delete habit. Please try again.'); + } + }; - const isTrackedToday = (habit: Habit) => { + const isTrackedOnDate = (habit: Habit, date: Date = selectedDate) => { + // Check completion history first if available + if (habit.completionHistory) { + const dateStr = format(date, 'yyyy-MM-dd'); + return habit.completionHistory[dateStr] === 'completed'; + } + + // Fall back to checking lastCompleted if (!habit.lastCompleted) return false; - const today = new Date(); - const lastCompleted = new Date(habit.lastCompleted); - return ( - today.getDate() === lastCompleted.getDate() && - today.getMonth() === lastCompleted.getMonth() && - today.getFullYear() === lastCompleted.getFullYear() - ); + + return isSameDay(new Date(habit.lastCompleted), date) && habit.status === 'completed'; }; const getLastTrackedTime = (habit: Habit) => { if (!habit.lastCompleted) return ''; const now = new Date(); const last = new Date(habit.lastCompleted); - - if (isTrackedToday(habit)) { + + if (isToday(last)) { const hours = last.getHours().toString().padStart(2, '0'); const minutes = last.getMinutes().toString().padStart(2, '0'); return `Today at ${hours}:${minutes}`; } - + + // Show date for non-today completions + if (isSameDay(last, selectedDate)) { + const hours = last.getHours().toString().padStart(2, '0'); + const minutes = last.getMinutes().toString().padStart(2, '0'); + return `Tracked at ${hours}:${minutes}`; + } + const days = Math.floor((now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)); return `${days} day${days !== 1 ? 's' : ''} ago`; }; - const updateHabitStatus = (id: string, newStatus: 'completed' | 'failed' | 'untracked') => { - setHabits(habits.map(habit => { - if (habit.id === id) { - let newStreak = habit.streak; - let newLastCompleted = habit.lastCompleted; - - if (newStatus === 'completed') { - newStreak = habit.streak + 1; - newLastCompleted = new Date(); - } else if (newStatus === 'failed') { - newStreak = 0; - newLastCompleted = null; - } - // 'untracked' status doesn't change the streak - - return { - ...habit, - status: newStatus, - streak: newStreak, - lastCompleted: newLastCompleted, - }; + const getStreakColor = (streak: number) => { + if (streak >= 10) return '#27ae60'; + if (streak >= 5) return '#2ecc71'; + if (streak >= 3) return '#3498db'; + return '#95a5a6'; + }; + + // Sort habits by reminder time + const sortHabitsByReminderTime = (habits: Habit[]): Habit[] => { + return [...habits].sort((a, b) => { + // If either habit doesn't have a reminder time, place it at the end + if (!a.reminderTime) return 1; + if (!b.reminderTime) return -1; + + // Parse times for comparison (format is "hh:mm a", e.g. "08:30 AM") + const timeA = parseReminderTime(a.reminderTime); + const timeB = parseReminderTime(b.reminderTime); + + // Compare the parsed 24-hour time values + return timeA - timeB; + }); + }; + + // Helper function to parse reminder time strings to comparable values + const parseReminderTime = (timeString: string): number => { + try { + const [time, period] = timeString.split(' '); + let [hours, minutes] = time.split(':').map(num => parseInt(num, 10)); + + // Convert to 24-hour format for proper comparison + if (period && period.toLowerCase() === 'pm' && hours < 12) { + hours += 12; + } else if (period && period.toLowerCase() === 'am' && hours === 12) { + hours = 0; } - return habit; - })); + + return hours * 60 + minutes; // Convert to minutes for comparison + } catch (error) { + console.error('Error parsing time:', timeString, error); + return 0; + } }; - const addHabit = () => { - if (newHabit.trim()) { - const newHabitItem: Habit = { - id: Date.now().toString(), - name: newHabit.trim(), - status: 'untracked', - streak: 0, - lastCompleted: null, + // Generate a week view for the selected date's week + const generateWeekDays = () => { + // Start from Monday of the week containing the selectedDate + const currentDay = selectedDate.getDay(); // 0 for Sunday, 1 for Monday, etc. + const startDay = subDays(selectedDate, currentDay === 0 ? 6 : currentDay - 1); // Start from Monday + + return Array.from({ length: 7 }).map((_, index) => { + const date = addDays(startDay, index); + const dayNumber = date.getDate(); + const dayName = ['S', 'M', 'T', 'W', 'T', 'F', 'S'][date.getDay()]; + + return { + date: dayNumber, + day: dayName, + fullDate: date, + isToday: isToday(date), + isSelected: isSameDay(date, selectedDate) }; - setHabits([...habits, newHabitItem]); - setNewHabit(''); - setIsAddingHabit(false); + }); + }; + + const weekDays = generateWeekDays(); + + // Check if a habit was completed on a specific date + const wasCompletedOnDate = (habit: Habit, date: Date): boolean => { + if (!habit.completionHistory) { + // If no completion history exists, fall back to checking lastCompleted + // Make sure we handle null/undefined properly + if (!habit.lastCompleted) return false; + + return isSameDay(new Date(habit.lastCompleted), date) && habit.status === 'completed'; } + + // Otherwise check the completion history + const dateStr = format(date, 'yyyy-MM-dd'); + return habit.completionHistory[dateStr] === 'completed'; }; - const getStreakColor = (streak: number) => { - if (streak >= 10) return '#27ae60'; - if (streak >= 5) return '#2ecc71'; - if (streak >= 3) return '#3498db'; - return '#95a5a6'; + // Handle date change from DateHeader + const handleDateChange = (date: Date) => { + setSelectedDate(date); + }; + + // Get status for the currently selected date + const getStatusForSelectedDate = (habit: Habit) => { + // If checking for today, use the current status property + if (isToday(selectedDate)) { + return habit.status; + } + + // Check completion history if available + if (habit.completionHistory) { + const dateStr = format(selectedDate, 'yyyy-MM-dd'); + return habit.completionHistory[dateStr] || 'untracked'; + } + + // Fall back to last completed date + if (habit.lastCompleted && isSameDay(new Date(habit.lastCompleted), selectedDate)) { + return 'completed'; + } + + return 'untracked'; }; + if (isLoading) { + return ( + + + Loading habits... + + ); + } + return ( - - - {!isAddingHabit ? ( - setIsAddingHabit(true)} - > - + Add New Habit - - ) : ( - - - - { - setIsAddingHabit(false); - setNewHabit(''); - }} - > - Cancel - - - Save - - + {/* Use the DateHeader component */} + + + setIsAddingHabit(true)} + > + + Add New Habit + + + setIsAddingHabit(false)} + onAdd={handleAddHabit} + /> + + {/* Display date information if not today */} + {!isToday(selectedDate) && ( + + + Viewing habits for {format(selectedDate, 'MMMM d, yyyy')} + )} - - {habits.map(habit => ( - - - - updateHabitStatus(habit.id, 'completed')} - style={[ - styles.habitButton, - habit.status === 'completed' && styles.activeButton, - styles.completeButton, - isTrackedToday(habit) && styles.trackedTodayButton - ]} - > - - - updateHabitStatus(habit.id, 'untracked')} - style={[ - styles.habitButton, - habit.status === 'untracked' && styles.activeButton, - styles.untrackedButton - ]} - > - - - - updateHabitStatus(habit.id, 'failed')} - style={[ - styles.habitButton, - habit.status === 'failed' && styles.activeButton, - styles.failButton - ]} - > - - - - - {habit.name} - {!isTrackedToday(habit) && ( - - Not tracked today + {habits.length === 0 ? ( + + + You don't have any habits yet. Add your first habit to start tracking! + + + ) : ( + + {sortHabitsByReminderTime(habits).map(habit => { + const currentStatus = getStatusForSelectedDate(habit); + + return ( + + + + {habit.name} + {habit.description && ( + {habit.description} + )} + + {currentStatus === 'untracked' && ( + + + Not tracked {isToday(selectedDate) ? 'today' : 'on this date'} + + + )} + + {habit.lastCompleted && ( + + Last tracked: {getLastTrackedTime(habit)} + + )} + + + + updateHabitStatus(habit, 'completed', selectedDate)} + style={[ + styles.habitButton, + currentStatus === 'completed' && styles.activeButton, + styles.completeButton, + isTrackedOnDate(habit, selectedDate) && styles.trackedTodayButton + ]} + > + + + updateHabitStatus(habit, 'untracked', selectedDate)} + style={[ + styles.habitButton, + currentStatus === 'untracked' && styles.activeButton, + styles.untrackedButton + ]} + > + - + + updateHabitStatus(habit, 'failed', selectedDate)} + style={[ + styles.habitButton, + currentStatus === 'failed' && styles.activeButton, + styles.failButton + ]} + > + + + + + + {/* Weekly habit tracker */} + + {generateWeekDays().map((day, index) => ( + { + // Set the selected date to this day and update UI + handleDateChange(day.fullDate); + }} + /> + ))} + + + + + + + + + {habit.streak} day{habit.streak !== 1 ? 's' : ''} streak + + {habit.longestStreak > 0 && ( + + Longest: {habit.longestStreak} day{habit.longestStreak !== 1 ? 's' : ''} + + )} + + + + {habit.reminderTime && ( + + + Daily reminder: {habit.reminderTime} + )} - {habit.lastCompleted && ( - - Last tracked: {getLastTrackedTime(habit)} - - )} - - - - - - - - {habit.streak} day{habit.streak !== 1 ? 's' : ''} streak - - - - ))} - + ); + })} + + )} ); }; @@ -257,77 +451,60 @@ const styles = StyleSheet.create({ backgroundColor: '#fff', padding: 0, }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + }, + loadingText: { + marginTop: 12, + fontSize: 16, + color: '#7f8c8d', + }, + selectedDateInfo: { + paddingHorizontal: 16, + marginBottom: 8, + backgroundColor: '#f0f4f8', + paddingVertical: 8, + borderRadius: 8, + marginHorizontal: 16, + }, + selectedDateText: { + color: '#2c3e50', + fontWeight: '500', + textAlign: 'center', + }, addButton: { backgroundColor: '#2c3e50', padding: 16, borderRadius: 12, alignItems: 'center', - marginHorizontal: 16, // Added horizontal margin - marginTop: 16, // Added top margin to create space after DateHeader + marginHorizontal: 16, + marginTop: 16, marginBottom: 16, -}, + }, addButtonText: { color: '#fff', fontSize: 16, fontWeight: 'bold', }, - addHabitContainer: { - backgroundColor: '#f8f9fa', - padding: 16, - borderRadius: 12, - marginBottom: 16, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - }, - android: { - elevation: 3, - }, - }), - }, - input: { - backgroundColor: '#fff', - padding: 12, - borderRadius: 8, - fontSize: 16, - borderWidth: 1, - borderColor: '#dfe6e9', - marginBottom: 12, - }, - addHabitButtons: { - flexDirection: 'row', - justifyContent: 'flex-end', - }, - button: { - padding: 12, - borderRadius: 8, - minWidth: 80, + emptyStateContainer: { + padding: 24, alignItems: 'center', - marginLeft: 12, - }, - cancelButton: { - backgroundColor: '#f8f9fa', - borderWidth: 1, - borderColor: '#dfe6e9', - }, - saveButton: { - backgroundColor: '#2c3e50', - }, - cancelButtonText: { - color: '#2c3e50', - fontWeight: '600', + justifyContent: 'center', }, - saveButtonText: { - color: '#fff', - fontWeight: '600', + emptyStateText: { + textAlign: 'center', + fontSize: 16, + color: '#7f8c8d', + lineHeight: 24, }, habitsList: { gap: 12, - paddingHorizontal: 16, // Added horizontal padding -}, + paddingHorizontal: 16, + paddingBottom: 32, + }, habitCard: { backgroundColor: '#f8f9fa', padding: 16, @@ -346,16 +523,28 @@ const styles = StyleSheet.create({ }, habitHeader: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, }, habitControls: { flexDirection: 'row', gap: 8, - marginRight: 12, }, habitInfo: { flex: 1, + marginRight: 12, + }, + habitText: { + fontSize: 18, + color: '#2c3e50', + fontWeight: '600', + marginBottom: 4, + }, + habitDescription: { + fontSize: 14, + color: '#34495e', + marginBottom: 8, }, habitButton: { width: 36, @@ -391,11 +580,6 @@ const styles = StyleSheet.create({ borderColor: '#27ae60', borderWidth: 2, }, - habitText: { - fontSize: 16, - color: '#2c3e50', - fontWeight: '500', - }, notTrackedBadge: { backgroundColor: '#fff3e0', paddingHorizontal: 8, @@ -403,6 +587,7 @@ const styles = StyleSheet.create({ borderRadius: 4, alignSelf: 'flex-start', marginTop: 4, + marginBottom: 8, }, notTrackedText: { color: '#f57c00', @@ -414,6 +599,12 @@ const styles = StyleSheet.create({ color: '#95a5a6', marginTop: 4, }, + weeklyTracker: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + marginTop: 4, + }, streakContainer: { marginTop: 8, }, @@ -428,11 +619,30 @@ const styles = StyleSheet.create({ height: '100%', borderRadius: 3, }, + streakInfoContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, streakText: { fontSize: 14, - color: '#95a5a6', + color: '#2c3e50', fontWeight: '500', }, + longestStreakText: { + fontSize: 12, + color: '#95a5a6', + }, + reminderContainer: { + marginTop: 12, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: '#f0f0f0', + }, + reminderText: { + fontSize: 14, + color: '#7f8c8d', + }, }); export default HabitsScreen; \ No newline at end of file diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx deleted file mode 100644 index c3bd148..0000000 --- a/app/(tabs)/index.tsx +++ /dev/null @@ -1,713 +0,0 @@ -//app/(tabs)/index.tsx -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { - View, - Text, - ScrollView, - TouchableOpacity, - Platform, - Modal, - KeyboardAvoidingView, - TextInput, -} from 'react-native'; -import DateHeader from '../../components/DateHeader'; -import { isToday, isBefore, isSameDay } from 'date-fns'; - -import { goalService, goodDeedService, reflectionService } from '../../services/firebaseService'; -import { auth } from '../../config/firebase'; -import styles from '../../styles/TodayScreenStyles'; - -// Types -interface Goal { - id: string; - text: string; - completed: boolean; - date?: Date; - isExpired?: boolean; -} - -interface ModalConfig { - isVisible: boolean; - initialText: string; - editId: string | null; -} - -interface GoodnessItem { - id: string; - text: string; - timestamp: Date; - date?: Date; -} - -interface ReflectionItem { - id: string; - text: string; - timestamp: Date; - date?: Date; -} - -const TodayScreen: React.FC = () => { - // State for selected date - const [selectedDate, setSelectedDate] = useState(new Date()); - - // Data states - const [goals, setGoals] = useState([]); - const [goodnessItems, setGoodnessItems] = useState([]); - const [reflectionItems, setReflectionItems] = useState([]); - - // Modal states - const [goalModalVisible, setGoalModalVisible] = useState({ - isVisible: false, - initialText: '', - editId: null - }); - const [goodnessModalVisible, setGoodnessModalVisible] = useState({ - isVisible: false, - initialText: '', - editId: null, - }); - const [reflectionModalVisible, setReflectionModalVisible] = useState({ - isVisible: false, - initialText: '', - editId: null, - }); - - // Input states - const [newGoalText, setNewGoalText] = useState(''); - const [goodnessText, setGoodnessText] = useState(''); - const [reflectionText, setReflectionText] = useState(''); - - // Track if component is mounted to prevent state updates after unmount - const isMounted = useRef(true); - - // Track active user - const [userId, setUserId] = useState(null); - - // Set up authentication listener only once - useEffect(() => { - // Set mounted flag - isMounted.current = true; - - // Check if user is logged in - const unsubscribeAuth = auth.onAuthStateChanged((user) => { - if (isMounted.current) { - if (user) { - setUserId(user.uid); - } else { - setUserId(null); - } - } - }); - - // Cleanup - return () => { - isMounted.current = false; - unsubscribeAuth(); - }; - }, []); - - // Fetch data when user or date changes - useEffect(() => { - if (!userId) return; - - // Fetch data for the selected date - fetchDataForDate(selectedDate); - }, [userId, selectedDate]); - - // Function to fetch all data for a specific date - const fetchDataForDate = useCallback(async (date: Date) => { - if (!userId || !isMounted.current) return; - - try { - // Fetch goals - const goalsForDate = await goalService.getItemsByDate(userId, date); - - // Mark expired goals - if (isBefore(date, new Date()) && !isToday(date)) { - if (isMounted.current) { - setGoals(goalsForDate.map(goal => ({ - ...goal, - isExpired: !goal.completed - }))); - } - } else { - if (isMounted.current) { - setGoals(goalsForDate); - } - } - - // Fetch good deeds - const deedsForDate = await goodDeedService.getItemsByDate(userId, date); - if (isMounted.current) { - setGoodnessItems(deedsForDate); - } - - // Fetch reflections - const reflectionsForDate = await reflectionService.getItemsByDate(userId, date); - if (isMounted.current) { - setReflectionItems(reflectionsForDate); - } - } catch (error) { - console.error('Error fetching data for date:', error); - } - }, [userId]); - - // Handle date change from the DateHeader component - const handleDateChange = useCallback((date: Date) => { - setSelectedDate(date); - }, []); - - // Calculate progress percentage - const getProgressPercentage = useCallback(() => { - if (goals.length === 0) return 0; - const completedGoals = goals.filter(goal => goal.completed).length; - return (completedGoals / goals.length) * 100; - }, [goals]); - - // Goal functions - const toggleGoal = async (id: string) => { - if (!userId) return; - - try { - const goal = goals.find(g => g.id === id); - if (goal) { - await goalService.toggleCompletion(id, !goal.completed); - - // Update local state - setGoals(prevGoals => - prevGoals.map(g => - g.id === id ? { ...g, completed: !goal.completed } : g - ) - ); - } - } catch (error) { - console.error('Error toggling goal:', error); - } - }; - - const handleAddGoal = async () => { - if (!userId || !newGoalText.trim()) return; - - try { - if (goalModalVisible.editId) { - // Editing existing goal - await goalService.updateItem(goalModalVisible.editId, { - text: newGoalText.trim() - }); - - // Update local state - setGoals(prevGoals => - prevGoals.map(goal => - goal.id === goalModalVisible.editId - ? { ...goal, text: newGoalText.trim() } - : goal - ) - ); - } else { - // Adding new goal for the selected date - const newGoalId = await goalService.addItem(userId, { - text: newGoalText.trim(), - completed: false - }, selectedDate); - - // Add to local state - setGoals(prevGoals => [...prevGoals, { - id: newGoalId, - text: newGoalText.trim(), - completed: false, - date: selectedDate - }]); - } - - // Close modal and clear text - handleCloseGoalModal(); - } catch (error) { - console.error('Error managing goal:', error); - } - }; - - const handleOpenGoalModal = (itemId?: string) => { - if (itemId) { - const goalToEdit = goals.find(goal => goal.id === itemId); - setGoalModalVisible({ - isVisible: true, - initialText: goalToEdit?.text || '', - editId: itemId - }); - setNewGoalText(goalToEdit?.text || ''); - } else { - setGoalModalVisible({ - isVisible: true, - initialText: '', - editId: null - }); - setNewGoalText(''); - } - }; - - const handleCloseGoalModal = () => { - setGoalModalVisible({ isVisible: false, initialText: '', editId: null }); - setNewGoalText(''); - }; - - // Goodness functions - const handleAddGoodness = async () => { - if (!userId || !goodnessText.trim()) return; - - try { - if (goodnessModalVisible.editId) { - // Editing existing item - await goodDeedService.updateItem(goodnessModalVisible.editId, { - text: goodnessText.trim() - }); - - // Update local state - setGoodnessItems(prevItems => - prevItems.map(item => - item.id === goodnessModalVisible.editId - ? { ...item, text: goodnessText.trim() } - : item - ) - ); - } else { - // Adding new item for the selected date - const now = new Date(); - const newItemId = await goodDeedService.addItem(userId, { - text: goodnessText.trim(), - timestamp: now - }, selectedDate); - - // Add to local state - setGoodnessItems(prevItems => [...prevItems, { - id: newItemId, - text: goodnessText.trim(), - timestamp: now, - date: selectedDate - }]); - } - - // Close modal and clear text - handleCloseGoodnessModal(); - } catch (error) { - console.error('Error managing good deed:', error); - } - }; - - const handleOpenGoodnessModal = (itemId?: string) => { - if (itemId) { - const itemToEdit = goodnessItems.find(item => item.id === itemId); - setGoodnessModalVisible({ - isVisible: true, - initialText: itemToEdit?.text || '', - editId: itemId - }); - setGoodnessText(itemToEdit?.text || ''); - } else { - setGoodnessModalVisible({ - isVisible: true, - initialText: '', - editId: null - }); - setGoodnessText(''); - } - }; - - const handleCloseGoodnessModal = () => { - setGoodnessModalVisible({ isVisible: false, initialText: '', editId: null }); - setGoodnessText(''); - }; - - // Reflection functions - const handleAddReflection = async () => { - if (!userId || !reflectionText.trim()) return; - - try { - if (reflectionModalVisible.editId) { - // Editing existing item - await reflectionService.updateItem(reflectionModalVisible.editId, { - text: reflectionText.trim() - }); - - // Update local state - setReflectionItems(prevItems => - prevItems.map(item => - item.id === reflectionModalVisible.editId - ? { ...item, text: reflectionText.trim() } - : item - ) - ); - } else { - // Adding new item for the selected date - const now = new Date(); - const newItemId = await reflectionService.addItem(userId, { - text: reflectionText.trim(), - timestamp: now - }, selectedDate); - - // Add to local state - setReflectionItems(prevItems => [...prevItems, { - id: newItemId, - text: reflectionText.trim(), - timestamp: now, - date: selectedDate - }]); - } - - // Close modal and clear text - handleCloseReflectionModal(); - } catch (error) { - console.error('Error managing reflection:', error); - } - }; - - const handleOpenReflectionModal = (itemId?: string) => { - if (itemId) { - const itemToEdit = reflectionItems.find(item => item.id === itemId); - setReflectionModalVisible({ - isVisible: true, - initialText: itemToEdit?.text || '', - editId: itemId - }); - setReflectionText(itemToEdit?.text || ''); - } else { - setReflectionModalVisible({ - isVisible: true, - initialText: '', - editId: null - }); - setReflectionText(''); - } - }; - - const handleCloseReflectionModal = () => { - setReflectionModalVisible({ isVisible: false, initialText: '', editId: null }); - setReflectionText(''); - }; - - return ( - - - - - {/* Goals Section */} - - - - {isToday(selectedDate) ? "Today's Goals" : `Goals for ${selectedDate.toLocaleDateString()}`} - - handleOpenGoalModal()} - > - + - - - {goals.length > 0 ? ( - goals.map(goal => ( - - toggleGoal(goal.id)} - > - {goal.completed && ( - - )} - - - {goal.text} - - handleOpenGoalModal(goal.id)} - > - Edit - - - )) - ) : ( - - No goals for this date - Tap + to add a goal - - )} - - - {/* Goodness Section */} - - - - {isToday(selectedDate) - ? "What good shall I do today?" - : `Good deeds for ${selectedDate.toLocaleDateString()}` - } - - handleOpenGoodnessModal()} - > - + - - - {goodnessItems.length > 0 ? ( - - {goodnessItems.map(item => ( - - - {item.text} - - {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - })} - - - handleOpenGoodnessModal(item.id)} - > - Edit - - - ))} - - ) : ( - - - {isToday(selectedDate) - ? "What good will you do today?" - : "No good deeds recorded for this date" - } - - Tap + to add a good deed - - )} - - - {/* Reflection Section */} - - - - {isToday(selectedDate) - ? "Today's Reflections" - : `Reflections for ${selectedDate.toLocaleDateString()}` - } - - handleOpenReflectionModal()} - > - + - - - {reflectionItems.length > 0 ? ( - - {reflectionItems.map(item => ( - - - {item.text} - - {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - })} - - - handleOpenReflectionModal(item.id)} - > - Edit - - - ))} - - ) : ( - - - {isToday(selectedDate) - ? "No reflections yet" - : "No reflections recorded for this date" - } - - - {isToday(selectedDate) - ? "Take a moment to reflect on your day" - : "Tap + to add a reflection" - } - - - )} - - - - {/* Modals */} - {/* Goal Modal */} - - - - - {goalModalVisible.editId ? 'Edit Goal' : 'Add New Goal'} - - - - - Cancel - - - Save - - - - - - - {/* Goodness Modal */} - - - - e.stopPropagation()} - > - - - - {goodnessModalVisible.editId ? 'Edit Good Deed' : 'Add Good Deed'} - - - - - - - - - - - Cancel - - - Save - - - - - - - - - {/* Reflection Modal */} - - - - - {reflectionModalVisible.editId ? 'Edit Reflection' : 'Add Reflection'} - - - - - Cancel - - - Save - - - - - - - ); -}; - -export default TodayScreen; \ No newline at end of file diff --git a/app/(tabs)/today.tsx b/app/(tabs)/today.tsx new file mode 100644 index 0000000..599558a --- /dev/null +++ b/app/(tabs)/today.tsx @@ -0,0 +1,1416 @@ +//app/(tabs)/index.tsx + +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { + View, + Text, + ScrollView, + TouchableOpacity, + Platform, + Modal, + KeyboardAvoidingView, + TextInput, +} from 'react-native'; +import DateHeader from '../../components/DateHeader'; +import { isToday, isBefore, isSameDay } from 'date-fns'; + +import { goalService, goodDeedService, reflectionService } from '../../services/firebaseService'; +import { useAuth } from '../../context/AuthContext'; +import styles from '../../styles/TodayScreenStyles'; + +// Types +interface Goal { + id: string; + text: string; + completed: boolean; + date?: Date; + isExpired?: boolean; +} + +interface ModalConfig { + isVisible: boolean; + initialText: string; + editId: string | null; +} + +interface GoodnessItem { + id: string; + text: string; + timestamp: Date; + date?: Date; +} + +interface ReflectionItem { + id: string; + text: string; + timestamp: Date; + date?: Date; +} + +const TodayScreen: React.FC = () => { + // Get user info from auth context + const { userInfo } = useAuth(); + + // State for selected date + const [selectedDate, setSelectedDate] = useState(new Date()); + + // Data states + const [goals, setGoals] = useState([]); + const [goodnessItems, setGoodnessItems] = useState([]); + const [reflectionItems, setReflectionItems] = useState([]); + + // Modal states + const [goalModalVisible, setGoalModalVisible] = useState({ + isVisible: false, + initialText: '', + editId: null + }); + const [goodnessModalVisible, setGoodnessModalVisible] = useState({ + isVisible: false, + initialText: '', + editId: null, + }); + const [reflectionModalVisible, setReflectionModalVisible] = useState({ + isVisible: false, + initialText: '', + editId: null, + }); + + // Input states + const [newGoalText, setNewGoalText] = useState(''); + const [goodnessText, setGoodnessText] = useState(''); + const [reflectionText, setReflectionText] = useState(''); + + // Track if component is mounted to prevent state updates after unmount + const isMounted = useRef(true); + + // Set up component lifecycle + useEffect(() => { + // Set mounted flag + isMounted.current = true; + + // Cleanup + return () => { + isMounted.current = false; + }; + }, []); + + // Fetch data when user or date changes + useEffect(() => { + if (userInfo?.uid) { + // Fetch data for the selected date + fetchDataForDate(selectedDate); + } + }, [userInfo, selectedDate]); + + // Function to fetch all data for a specific date + const fetchDataForDate = useCallback(async (date: Date) => { + if (!userInfo?.uid || !isMounted.current) return; + + try { + // Fetch goals + const goalsForDate = await goalService.getItemsByDate(userInfo.uid, date); + + // Mark expired goals + if (isBefore(date, new Date()) && !isToday(date)) { + if (isMounted.current) { + setGoals(goalsForDate.map(goal => ({ + ...goal, + isExpired: !goal.completed + }))); + } + } else { + if (isMounted.current) { + setGoals(goalsForDate); + } + } + + // Fetch good deeds + const deedsForDate = await goodDeedService.getItemsByDate(userInfo.uid, date); + if (isMounted.current) { + setGoodnessItems(deedsForDate); + } + + // Fetch reflections + const reflectionsForDate = await reflectionService.getItemsByDate(userInfo.uid, date); + if (isMounted.current) { + setReflectionItems(reflectionsForDate); + } + } catch (error) { + console.error('Error fetching data for date:', error); + } + }, [userInfo]); + + // Handle date change from the DateHeader component + const handleDateChange = useCallback((date: Date) => { + setSelectedDate(date); + }, []); + + // Calculate progress percentage + const getProgressPercentage = useCallback(() => { + if (goals.length === 0) return 0; + const completedGoals = goals.filter(goal => goal.completed).length; + return (completedGoals / goals.length) * 100; + }, [goals]); + + // Goal functions + const toggleGoal = async (id: string) => { + if (!userInfo?.uid) return; + + try { + const goal = goals.find(g => g.id === id); + if (goal) { + await goalService.toggleCompletion(id, !goal.completed); + + // Update local state + setGoals(prevGoals => + prevGoals.map(g => + g.id === id ? { ...g, completed: !goal.completed } : g + ) + ); + } + } catch (error) { + console.error('Error toggling goal:', error); + } + }; + + const handleAddGoal = async () => { + if (!userInfo?.uid || !newGoalText.trim()) return; + + try { + if (goalModalVisible.editId) { + // Editing existing goal + await goalService.updateItem(goalModalVisible.editId, { + text: newGoalText.trim() + }); + + // Update local state + setGoals(prevGoals => + prevGoals.map(goal => + goal.id === goalModalVisible.editId + ? { ...goal, text: newGoalText.trim() } + : goal + ) + ); + } else { + // Adding new goal for the selected date + const newGoalId = await goalService.addItem(userInfo.uid, { + text: newGoalText.trim(), + completed: false + }, selectedDate); + + // Add to local state + setGoals(prevGoals => [...prevGoals, { + id: newGoalId, + text: newGoalText.trim(), + completed: false, + date: selectedDate + }]); + } + + // Close modal and clear text + handleCloseGoalModal(); + } catch (error) { + console.error('Error managing goal:', error); + } + }; + + const handleOpenGoalModal = (itemId?: string) => { + if (itemId) { + const goalToEdit = goals.find(goal => goal.id === itemId); + setGoalModalVisible({ + isVisible: true, + initialText: goalToEdit?.text || '', + editId: itemId + }); + setNewGoalText(goalToEdit?.text || ''); + } else { + setGoalModalVisible({ + isVisible: true, + initialText: '', + editId: null + }); + setNewGoalText(''); + } + }; + + const handleCloseGoalModal = () => { + setGoalModalVisible({ isVisible: false, initialText: '', editId: null }); + setNewGoalText(''); + }; + + // Goodness functions + const handleAddGoodness = async () => { + if (!userInfo?.uid || !goodnessText.trim()) return; + + try { + if (goodnessModalVisible.editId) { + // Editing existing item + await goodDeedService.updateItem(goodnessModalVisible.editId, { + text: goodnessText.trim() + }); + + // Update local state + setGoodnessItems(prevItems => + prevItems.map(item => + item.id === goodnessModalVisible.editId + ? { ...item, text: goodnessText.trim() } + : item + ) + ); + } else { + // Adding new item for the selected date + const now = new Date(); + const newItemId = await goodDeedService.addItem(userInfo.uid, { + text: goodnessText.trim(), + timestamp: now + }, selectedDate); + + // Add to local state + setGoodnessItems(prevItems => [...prevItems, { + id: newItemId, + text: goodnessText.trim(), + timestamp: now, + date: selectedDate + }]); + } + + // Close modal and clear text + handleCloseGoodnessModal(); + } catch (error) { + console.error('Error managing good deed:', error); + } + }; + + const handleOpenGoodnessModal = (itemId?: string) => { + if (itemId) { + const itemToEdit = goodnessItems.find(item => item.id === itemId); + setGoodnessModalVisible({ + isVisible: true, + initialText: itemToEdit?.text || '', + editId: itemId + }); + setGoodnessText(itemToEdit?.text || ''); + } else { + setGoodnessModalVisible({ + isVisible: true, + initialText: '', + editId: null + }); + setGoodnessText(''); + } + }; + + const handleCloseGoodnessModal = () => { + setGoodnessModalVisible({ isVisible: false, initialText: '', editId: null }); + setGoodnessText(''); + }; + + // Reflection functions + const handleAddReflection = async () => { + if (!userInfo?.uid || !reflectionText.trim()) return; + + try { + if (reflectionModalVisible.editId) { + // Editing existing item + await reflectionService.updateItem(reflectionModalVisible.editId, { + text: reflectionText.trim() + }); + + // Update local state + setReflectionItems(prevItems => + prevItems.map(item => + item.id === reflectionModalVisible.editId + ? { ...item, text: reflectionText.trim() } + : item + ) + ); + } else { + // Adding new item for the selected date + const now = new Date(); + const newItemId = await reflectionService.addItem(userInfo.uid, { + text: reflectionText.trim(), + timestamp: now + }, selectedDate); + + // Add to local state + setReflectionItems(prevItems => [...prevItems, { + id: newItemId, + text: reflectionText.trim(), + timestamp: now, + date: selectedDate + }]); + } + + // Close modal and clear text + handleCloseReflectionModal(); + } catch (error) { + console.error('Error managing reflection:', error); + } + }; + + const handleOpenReflectionModal = (itemId?: string) => { + if (itemId) { + const itemToEdit = reflectionItems.find(item => item.id === itemId); + setReflectionModalVisible({ + isVisible: true, + initialText: itemToEdit?.text || '', + editId: itemId + }); + setReflectionText(itemToEdit?.text || ''); + } else { + setReflectionModalVisible({ + isVisible: true, + initialText: '', + editId: null + }); + setReflectionText(''); + } + }; + + const handleCloseReflectionModal = () => { + setReflectionModalVisible({ isVisible: false, initialText: '', editId: null }); + setReflectionText(''); + }; + + return ( + + + + + {/* Goals Section */} + + + + {isToday(selectedDate) ? "Today's Goals" : `Goals for ${selectedDate.toLocaleDateString()}`} + + handleOpenGoalModal()} + > + + + + + {goals.length > 0 ? ( + goals.map(goal => ( + + toggleGoal(goal.id)} + > + {goal.completed && ( + + )} + + + {goal.text} + + handleOpenGoalModal(goal.id)} + > + Edit + + + )) + ) : ( + + No goals for this date + Tap + to add a goal + + )} + + + {/* Goodness Section */} + + + + {isToday(selectedDate) + ? "What good shall I do today?" + : `Good deeds for ${selectedDate.toLocaleDateString()}` + } + + handleOpenGoodnessModal()} + > + + + + + {goodnessItems.length > 0 ? ( + + {goodnessItems.map(item => ( + + + {item.text} + + {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} + + + handleOpenGoodnessModal(item.id)} + > + Edit + + + ))} + + ) : ( + + + {isToday(selectedDate) + ? "What good will you do today?" + : "No good deeds recorded for this date" + } + + Tap + to add a good deed + + )} + + + {/* Reflection Section */} + + + + {isToday(selectedDate) + ? "Today's Reflections" + : `Reflections for ${selectedDate.toLocaleDateString()}` + } + + handleOpenReflectionModal()} + > + + + + + {reflectionItems.length > 0 ? ( + + {reflectionItems.map(item => ( + + + {item.text} + + {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} + + + handleOpenReflectionModal(item.id)} + > + Edit + + + ))} + + ) : ( + + + {isToday(selectedDate) + ? "No reflections yet" + : "No reflections recorded for this date" + } + + + {isToday(selectedDate) + ? "Take a moment to reflect on your day" + : "Tap + to add a reflection" + } + + + )} + + + + {/* Modals */} + {/* Goal Modal */} + + + + + {goalModalVisible.editId ? 'Edit Goal' : 'Add New Goal'} + + + + + Cancel + + + Save + + + + + + + {/* Goodness Modal */} + + + + e.stopPropagation()} + > + + + + {goodnessModalVisible.editId ? 'Edit Good Deed' : 'Add Good Deed'} + + + + + + + + + + + Cancel + + + Save + + + + + + + + + {/* Reflection Modal */} + + + + + {reflectionModalVisible.editId ? 'Edit Reflection' : 'Add Reflection'} + + + + + Cancel + + + Save + + + + + + + ); +}; + +export default TodayScreen; + + +// import React, { useState, useEffect, useCallback, useRef } from 'react'; +// import { +// View, +// Text, +// ScrollView, +// TouchableOpacity, +// Platform, +// Modal, +// KeyboardAvoidingView, +// TextInput, +// } from 'react-native'; +// import DateHeader from '../../components/DateHeader'; +// import { isToday, isBefore, isSameDay } from 'date-fns'; + +// import { goalService, goodDeedService, reflectionService } from '../../services/firebaseService'; +// import { auth } from '../../config/firebase'; +// import styles from '../../styles/TodayScreenStyles'; + +// // Types +// interface Goal { +// id: string; +// text: string; +// completed: boolean; +// date?: Date; +// isExpired?: boolean; +// } + +// interface ModalConfig { +// isVisible: boolean; +// initialText: string; +// editId: string | null; +// } + +// interface GoodnessItem { +// id: string; +// text: string; +// timestamp: Date; +// date?: Date; +// } + +// interface ReflectionItem { +// id: string; +// text: string; +// timestamp: Date; +// date?: Date; +// } + +// const TodayScreen: React.FC = () => { +// // State for selected date +// const [selectedDate, setSelectedDate] = useState(new Date()); + +// // Data states +// const [goals, setGoals] = useState([]); +// const [goodnessItems, setGoodnessItems] = useState([]); +// const [reflectionItems, setReflectionItems] = useState([]); + +// // Modal states +// const [goalModalVisible, setGoalModalVisible] = useState({ +// isVisible: false, +// initialText: '', +// editId: null +// }); +// const [goodnessModalVisible, setGoodnessModalVisible] = useState({ +// isVisible: false, +// initialText: '', +// editId: null, +// }); +// const [reflectionModalVisible, setReflectionModalVisible] = useState({ +// isVisible: false, +// initialText: '', +// editId: null, +// }); + +// // Input states +// const [newGoalText, setNewGoalText] = useState(''); +// const [goodnessText, setGoodnessText] = useState(''); +// const [reflectionText, setReflectionText] = useState(''); + +// // Track if component is mounted to prevent state updates after unmount +// const isMounted = useRef(true); + +// // Track active user +// const [userId, setUserId] = useState(null); + +// // Set up authentication listener only once +// useEffect(() => { +// // Set mounted flag +// isMounted.current = true; + +// // Check if user is logged in +// const unsubscribeAuth = auth.onAuthStateChanged((user) => { +// if (isMounted.current) { +// if (user) { +// setUserId(user.uid); +// } else { +// setUserId(null); +// } +// } +// }); + +// // Cleanup +// return () => { +// isMounted.current = false; +// unsubscribeAuth(); +// }; +// }, []); + +// // Fetch data when user or date changes +// useEffect(() => { +// if (!userId) return; + +// // Fetch data for the selected date +// fetchDataForDate(selectedDate); +// }, [userId, selectedDate]); + +// // Function to fetch all data for a specific date +// const fetchDataForDate = useCallback(async (date: Date) => { +// if (!userId || !isMounted.current) return; + +// try { +// // Fetch goals +// const goalsForDate = await goalService.getItemsByDate(userId, date); + +// // Mark expired goals +// if (isBefore(date, new Date()) && !isToday(date)) { +// if (isMounted.current) { +// setGoals(goalsForDate.map(goal => ({ +// ...goal, +// isExpired: !goal.completed +// }))); +// } +// } else { +// if (isMounted.current) { +// setGoals(goalsForDate); +// } +// } + +// // Fetch good deeds +// const deedsForDate = await goodDeedService.getItemsByDate(userId, date); +// if (isMounted.current) { +// setGoodnessItems(deedsForDate); +// } + +// // Fetch reflections +// const reflectionsForDate = await reflectionService.getItemsByDate(userId, date); +// if (isMounted.current) { +// setReflectionItems(reflectionsForDate); +// } +// } catch (error) { +// console.error('Error fetching data for date:', error); +// } +// }, [userId]); + +// // Handle date change from the DateHeader component +// const handleDateChange = useCallback((date: Date) => { +// setSelectedDate(date); +// }, []); + +// // Calculate progress percentage +// const getProgressPercentage = useCallback(() => { +// if (goals.length === 0) return 0; +// const completedGoals = goals.filter(goal => goal.completed).length; +// return (completedGoals / goals.length) * 100; +// }, [goals]); + +// // Goal functions +// const toggleGoal = async (id: string) => { +// if (!userId) return; + +// try { +// const goal = goals.find(g => g.id === id); +// if (goal) { +// await goalService.toggleCompletion(id, !goal.completed); + +// // Update local state +// setGoals(prevGoals => +// prevGoals.map(g => +// g.id === id ? { ...g, completed: !goal.completed } : g +// ) +// ); +// } +// } catch (error) { +// console.error('Error toggling goal:', error); +// } +// }; + +// const handleAddGoal = async () => { +// if (!userId || !newGoalText.trim()) return; + +// try { +// if (goalModalVisible.editId) { +// // Editing existing goal +// await goalService.updateItem(goalModalVisible.editId, { +// text: newGoalText.trim() +// }); + +// // Update local state +// setGoals(prevGoals => +// prevGoals.map(goal => +// goal.id === goalModalVisible.editId +// ? { ...goal, text: newGoalText.trim() } +// : goal +// ) +// ); +// } else { +// // Adding new goal for the selected date +// const newGoalId = await goalService.addItem(userId, { +// text: newGoalText.trim(), +// completed: false +// }, selectedDate); + +// // Add to local state +// setGoals(prevGoals => [...prevGoals, { +// id: newGoalId, +// text: newGoalText.trim(), +// completed: false, +// date: selectedDate +// }]); +// } + +// // Close modal and clear text +// handleCloseGoalModal(); +// } catch (error) { +// console.error('Error managing goal:', error); +// } +// }; + +// const handleOpenGoalModal = (itemId?: string) => { +// if (itemId) { +// const goalToEdit = goals.find(goal => goal.id === itemId); +// setGoalModalVisible({ +// isVisible: true, +// initialText: goalToEdit?.text || '', +// editId: itemId +// }); +// setNewGoalText(goalToEdit?.text || ''); +// } else { +// setGoalModalVisible({ +// isVisible: true, +// initialText: '', +// editId: null +// }); +// setNewGoalText(''); +// } +// }; + +// const handleCloseGoalModal = () => { +// setGoalModalVisible({ isVisible: false, initialText: '', editId: null }); +// setNewGoalText(''); +// }; + +// // Goodness functions +// const handleAddGoodness = async () => { +// if (!userId || !goodnessText.trim()) return; + +// try { +// if (goodnessModalVisible.editId) { +// // Editing existing item +// await goodDeedService.updateItem(goodnessModalVisible.editId, { +// text: goodnessText.trim() +// }); + +// // Update local state +// setGoodnessItems(prevItems => +// prevItems.map(item => +// item.id === goodnessModalVisible.editId +// ? { ...item, text: goodnessText.trim() } +// : item +// ) +// ); +// } else { +// // Adding new item for the selected date +// const now = new Date(); +// const newItemId = await goodDeedService.addItem(userId, { +// text: goodnessText.trim(), +// timestamp: now +// }, selectedDate); + +// // Add to local state +// setGoodnessItems(prevItems => [...prevItems, { +// id: newItemId, +// text: goodnessText.trim(), +// timestamp: now, +// date: selectedDate +// }]); +// } + +// // Close modal and clear text +// handleCloseGoodnessModal(); +// } catch (error) { +// console.error('Error managing good deed:', error); +// } +// }; + +// const handleOpenGoodnessModal = (itemId?: string) => { +// if (itemId) { +// const itemToEdit = goodnessItems.find(item => item.id === itemId); +// setGoodnessModalVisible({ +// isVisible: true, +// initialText: itemToEdit?.text || '', +// editId: itemId +// }); +// setGoodnessText(itemToEdit?.text || ''); +// } else { +// setGoodnessModalVisible({ +// isVisible: true, +// initialText: '', +// editId: null +// }); +// setGoodnessText(''); +// } +// }; + +// const handleCloseGoodnessModal = () => { +// setGoodnessModalVisible({ isVisible: false, initialText: '', editId: null }); +// setGoodnessText(''); +// }; + +// // Reflection functions +// const handleAddReflection = async () => { +// if (!userId || !reflectionText.trim()) return; + +// try { +// if (reflectionModalVisible.editId) { +// // Editing existing item +// await reflectionService.updateItem(reflectionModalVisible.editId, { +// text: reflectionText.trim() +// }); + +// // Update local state +// setReflectionItems(prevItems => +// prevItems.map(item => +// item.id === reflectionModalVisible.editId +// ? { ...item, text: reflectionText.trim() } +// : item +// ) +// ); +// } else { +// // Adding new item for the selected date +// const now = new Date(); +// const newItemId = await reflectionService.addItem(userId, { +// text: reflectionText.trim(), +// timestamp: now +// }, selectedDate); + +// // Add to local state +// setReflectionItems(prevItems => [...prevItems, { +// id: newItemId, +// text: reflectionText.trim(), +// timestamp: now, +// date: selectedDate +// }]); +// } + +// // Close modal and clear text +// handleCloseReflectionModal(); +// } catch (error) { +// console.error('Error managing reflection:', error); +// } +// }; + +// const handleOpenReflectionModal = (itemId?: string) => { +// if (itemId) { +// const itemToEdit = reflectionItems.find(item => item.id === itemId); +// setReflectionModalVisible({ +// isVisible: true, +// initialText: itemToEdit?.text || '', +// editId: itemId +// }); +// setReflectionText(itemToEdit?.text || ''); +// } else { +// setReflectionModalVisible({ +// isVisible: true, +// initialText: '', +// editId: null +// }); +// setReflectionText(''); +// } +// }; + +// const handleCloseReflectionModal = () => { +// setReflectionModalVisible({ isVisible: false, initialText: '', editId: null }); +// setReflectionText(''); +// }; + +// return ( +// +// + +// +// {/* Goals Section */} +// +// +// +// {isToday(selectedDate) ? "Today's Goals" : `Goals for ${selectedDate.toLocaleDateString()}`} +// +// handleOpenGoalModal()} +// > +// + +// +// +// {goals.length > 0 ? ( +// goals.map(goal => ( +// +// toggleGoal(goal.id)} +// > +// {goal.completed && ( +// +// )} +// +// +// {goal.text} +// +// handleOpenGoalModal(goal.id)} +// > +// Edit +// +// +// )) +// ) : ( +// +// No goals for this date +// Tap + to add a goal +// +// )} +// + +// {/* Goodness Section */} +// +// +// +// {isToday(selectedDate) +// ? "What good shall I do today?" +// : `Good deeds for ${selectedDate.toLocaleDateString()}` +// } +// +// handleOpenGoodnessModal()} +// > +// + +// +// +// {goodnessItems.length > 0 ? ( +// +// {goodnessItems.map(item => ( +// +// +// {item.text} +// +// {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { +// hour: '2-digit', +// minute: '2-digit' +// })} +// +// +// handleOpenGoodnessModal(item.id)} +// > +// Edit +// +// +// ))} +// +// ) : ( +// +// +// {isToday(selectedDate) +// ? "What good will you do today?" +// : "No good deeds recorded for this date" +// } +// +// Tap + to add a good deed +// +// )} +// + +// {/* Reflection Section */} +// +// +// +// {isToday(selectedDate) +// ? "Today's Reflections" +// : `Reflections for ${selectedDate.toLocaleDateString()}` +// } +// +// handleOpenReflectionModal()} +// > +// + +// +// +// {reflectionItems.length > 0 ? ( +// +// {reflectionItems.map(item => ( +// +// +// {item.text} +// +// {item.timestamp && new Date(item.timestamp).toLocaleTimeString([], { +// hour: '2-digit', +// minute: '2-digit' +// })} +// +// +// handleOpenReflectionModal(item.id)} +// > +// Edit +// +// +// ))} +// +// ) : ( +// +// +// {isToday(selectedDate) +// ? "No reflections yet" +// : "No reflections recorded for this date" +// } +// +// +// {isToday(selectedDate) +// ? "Take a moment to reflect on your day" +// : "Tap + to add a reflection" +// } +// +// +// )} +// +// + +// {/* Modals */} +// {/* Goal Modal */} +// +// +// +// +// {goalModalVisible.editId ? 'Edit Goal' : 'Add New Goal'} +// +// +// +// +// Cancel +// +// +// Save +// +// +// +// +// + +// {/* Goodness Modal */} +// +// +// +// e.stopPropagation()} +// > +// +// +// +// {goodnessModalVisible.editId ? 'Edit Good Deed' : 'Add Good Deed'} +// +// +// +// +// + +// + +// +// +// Cancel +// +// +// Save +// +// +// +// +// +// +// + +// {/* Reflection Modal */} +// +// +// +// +// {reflectionModalVisible.editId ? 'Edit Reflection' : 'Add Reflection'} +// +// +// +// +// Cancel +// +// +// Save +// +// +// +// +// +// +// ); +// }; + +// export default TodayScreen; \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index 0655ea7..4745872 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,82 +1,60 @@ +import React, { useEffect } from 'react'; import { Stack } from 'expo-router'; -import { useEffect } from 'react'; -import { auth } from '../config/firebase'; -import { onAuthStateChanged } from 'firebase/auth'; -import { router } from 'expo-router'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { StatusBar } from 'expo-status-bar'; +import { Platform } from 'react-native'; +import { AuthProvider } from '../context/AuthContext'; +import { NotificationProvider } from '../context/NotificationContext'; +import * as NavigationBar from 'expo-navigation-bar'; export default function RootLayout() { + // Set up status bar and navigation bar (Android) colors useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - if (!user) { - router.replace('/login'); - } - }); - - return () => unsubscribe(); + if (Platform.OS === 'android') { + NavigationBar.setBackgroundColorAsync('#ffffff'); + NavigationBar.setButtonStyleAsync('dark'); + } }, []); return ( - - - - + + + + + + + + + + + + ); -} - - -// app/_layout.tsx -// import { useEffect } from 'react'; -// import { Slot, useRouter, useSegments } from 'expo-router'; -// import { onAuthStateChanged } from 'firebase/auth'; -// import { auth } from '../config/firebase'; - -// export default function RootLayout() { -// const segments = useSegments(); -// const router = useRouter(); - -// useEffect(() => { -// const unsubscribe = onAuthStateChanged(auth, (user) => { -// const inAuthGroup = segments[0] === '(auth)'; - -// if (!user && !inAuthGroup) { -// // Not signed in, redirect to login -// router.replace('/login'); -// } else if (user && inAuthGroup) { -// // Signed in, redirect to main app -// router.replace('/(tabs)'); -// } -// }); - -// return () => unsubscribe(); -// }, [segments]); - -// return ; -// } - -// // app/_layout.tsx -// import { useEffect } from 'react'; -// import { Slot, useRouter, useSegments } from 'expo-router'; -// import { auth } from '../config/firebase'; - -// export default function RootLayout() { -// const segments = useSegments(); -// const router = useRouter(); - -// useEffect(() => { -// const unsubscribe = auth.onAuthStateChanged((user) => { -// const inTabsGroup = segments[0] === '(tabs)'; - -// if (!user && inTabsGroup) { -// // Redirect to login if user is not authenticated and trying to access tabs -// router.replace('/login'); -// } else if (user && !inTabsGroup) { -// // Redirect to tabs if user is authenticated and not in tabs -// router.replace('/(tabs)'); -// } -// }); - -// return unsubscribe; -// }, [segments]); - -// return ; -// } +} \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index b3b6abf..9d0d41b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -8,7 +8,7 @@ export default function Index() { const user = auth.currentUser; if (user) { - return ; + return ; } else { return ; } diff --git a/app/login.tsx b/app/login.tsx index 1ecfdca..c29c336 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,184 +1,29 @@ -import React, { useState, useEffect } from 'react'; +//app/logim.tsx + +import React from 'react'; import { StyleSheet, View, Text, TouchableOpacity, - Image, - Alert, ActivityIndicator, KeyboardAvoidingView, Platform } from 'react-native'; -import { router } from 'expo-router'; -import * as WebBrowser from 'expo-web-browser'; -import * as Google from 'expo-auth-session/providers/google'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { - GoogleAuthProvider, - signInWithCredential, - onAuthStateChanged -} from 'firebase/auth'; -import { auth, googleAuthProvider } from '../config/firebase'; import { LinearGradient } from 'expo-linear-gradient'; -import Constants from 'expo-constants'; - -// Register for redirect callback -WebBrowser.maybeCompleteAuthSession(); +import { useAuth } from '../context/AuthContext'; +import { router } from 'expo-router'; const LoginScreen = () => { - const [userInfo, setUserInfo] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [loadingMessage, setLoadingMessage] = useState(''); - - // Setup Google Auth Request -// const [request, response, promptAsync] = Google.useAuthRequest({ -// androidClientId: Constants.expoConfig?.extra?.androidClientId, -// webClientId: Constants.expoConfig?.extra?.webClientId, -// responseType: 'id_token', -// selectAccount: true, -// }); - -const [request, response, promptAsync] = Google.useAuthRequest({ - webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID, - androidClientId: process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID, - scopes: ['profile', 'email', 'openid'], - redirectUri: Platform.select({ - android: 'com.hiteshjoshi.tracker:/oauthredirect', - web: 'https://auth.expo.io/@hiteshjoshi/Tracker', - ios: 'com.hiteshjoshi.tracker:/oauthredirect' - }) - }); - - // Check for existing session on mount - useEffect(() => { - const checkExistingSession = async () => { - try { - const userJSON = await AsyncStorage.getItem('@user'); - if (userJSON) { - const userData = JSON.parse(userJSON); - setUserInfo(userData); - // Navigate to home screen if user is already logged in - router.replace('/(tabs)'); - } - } catch (error) { - console.error('Error checking existing session:', error); - } - }; - - checkExistingSession(); - - // Monitor Firebase auth state - const unsubscribe = onAuthStateChanged(auth, (user) => { - if (user) { - console.log('User is signed in:', user.uid); - // User is signed in - setUserInfo(user); - saveUserToStorage(user); - if (isLoading) { - setIsLoading(false); - router.replace('/(tabs)'); - } - } else { - // User is signed out - console.log('User is signed out'); - setUserInfo(null); - AsyncStorage.removeItem('@user'); - } - }); - - // Cleanup subscription - return () => unsubscribe(); - }, []); - - // Handle Google Auth Response - useEffect(() => { - if (response?.type === 'success') { - setIsLoading(true); - setLoadingMessage('Getting user info...'); - handleGoogleSignInResponse(response); - } else if (response?.type === 'error' || response?.type === 'dismiss') { - setIsLoading(false); - Alert.alert('Authentication failed', 'Google sign in was unsuccessful. Please try again.'); - } - }, [response]); - - const handleGoogleSignInResponse = async (response: any) => { - try { - console.log('Got authentication response:', response.type); - - if (response.type === 'success') { - // Check if we have an id_token (using responseType: 'id_token') - if (response.params.id_token) { - const { id_token } = response.params; - console.log('Received ID token from Google Auth'); - - setLoadingMessage('Authenticating with Firebase...'); - // Create a Google credential with ID token - const credential = GoogleAuthProvider.credential(id_token); - - // Sign in to Firebase with the Google credential - const result = await signInWithCredential(auth, credential); - console.log('Firebase sign in successful:', result.user.uid); - - // Save user data - const userData = { - uid: result.user.uid, - email: result.user.email, - displayName: result.user.displayName, - photoURL: result.user.photoURL - }; - - setUserInfo(userData); - saveUserToStorage(userData); - - // Navigate to home screen - setLoadingMessage('Redirecting to app...'); - router.replace('/(tabs)'); - } - // If we got an authorization code instead (default responseType) - else if (response.params.code) { - console.log('Received authorization code from Google Auth'); - Alert.alert( - 'Authorization Code Flow', - 'Received an authorization code instead of an ID token. ' + - 'Please update the configuration to use responseType: "id_token".' - ); - setIsLoading(false); - return; - } else { - console.error('No id_token or code found in response'); - setIsLoading(false); - Alert.alert('Authentication Error', 'No authentication credentials received from Google.'); - return; - } - } - } catch (error) { - console.error('Error signing in with Google:', error); - setIsLoading(false); - Alert.alert('Authentication Error', 'An error occurred during Google sign in. Please try again.'); - } - }; - - const saveUserToStorage = async (user: any) => { - try { - await AsyncStorage.setItem('@user', JSON.stringify(user)); - } catch (error) { - console.error('Error saving user to storage:', error); - } - }; + // Use the authentication context instead of local state + const { isLoading, loadingMessage, signInWithGoogle, isAuthenticated } = useAuth(); - const handleGoogleSignIn = async () => { - setIsLoading(true); - setLoadingMessage('Opening Google Sign In...'); - try { - await promptAsync(); - } catch (error) { - console.error('Error opening Google Sign In:', error); - setIsLoading(false); - Alert.alert('Sign In Error', 'Could not open Google Sign In. Please try again.'); + // Redirect to tabs if already authenticated + React.useEffect(() => { + if (isAuthenticated) { + router.replace('/today'); } - }; + }, [isAuthenticated]); return ( Sign in with Google @@ -275,4 +119,4 @@ const styles = StyleSheet.create({ }, }); -export default LoginScreen; \ No newline at end of file +export default LoginScreen; diff --git a/components/AddHabitModal.tsx b/components/AddHabitModal.tsx new file mode 100644 index 0000000..8eab73a --- /dev/null +++ b/components/AddHabitModal.tsx @@ -0,0 +1,417 @@ +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + Modal, + Platform, + Pressable, + ScrollView, + Switch, +} from 'react-native'; +import DateTimePicker from '@react-native-community/datetimepicker'; +import { Habit } from '../models/types' +import { format } from 'date-fns'; +import { useNotifications } from '../context/NotificationContext'; +import { useAuth } from '../context/AuthContext'; + +interface AddHabitModalProps { + visible: boolean; + onClose: () => void; + onAdd: (habitData: Partial) => Promise; +} + +const AddHabitModal: React.FC = ({ visible, onClose, onAdd }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [reminderTime, setReminderTime] = useState(new Date()); + const [showTimePicker, setShowTimePicker] = useState(false); + const [enableReminders, setEnableReminders] = useState(true); + const [selectedDays, setSelectedDays] = useState({ + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: true, + sunday: true, + }); + + // Get notification functions from context + const { scheduleDailyReminder } = useNotifications(); + // Get user info from auth context + const { userInfo } = useAuth(); + + const resetForm = () => { + setName(''); + setDescription(''); + setReminderTime(new Date()); + setEnableReminders(true); + setSelectedDays({ + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: true, + sunday: true, + }); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const handleAdd = async () => { + if (name.trim()) { + const formattedTime = format(reminderTime, 'hh:mm a'); + + // Create habit data with proper types + const habitData: Partial = { + name: name.trim(), + // Only set description if there is a value + ...(description.trim() ? { description: description.trim() } : {}), + // Only include reminder-related fields if reminders are enabled + ...(enableReminders ? { + reminderTime: formattedTime, + reminderDays: selectedDays + } : {}) + }; + + try { + // Add the habit and get the ID + const habitId = await onAdd(habitData); + + // Schedule notification if reminders are enabled + if (enableReminders) { + // Create a full habit object for the notification scheduler + const habitForNotification: Habit = { + id: habitId, + name: name.trim(), + ...(description.trim() ? { description: description.trim() } : {}), + reminderTime: formattedTime, + reminderDays: selectedDays, + // Add required fields with defaults + status: 'untracked', + streak: 0, + longestStreak: 0, + lastCompleted: null, + date: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + completionHistory: {}, + userId: userInfo?.uid || '', // Add the userId from auth context + }; + + await scheduleDailyReminder(habitForNotification); + } + + // Reset form and close modal + resetForm(); + onClose(); + } catch (error) { + console.error('Error adding habit:', error); + // Here you could show an error message to the user + } + } + }; + + const toggleDay = (day: keyof typeof selectedDays) => { + setSelectedDays({ + ...selectedDays, + [day]: !selectedDays[day], + }); + }; + + const DayButton = ({ day, label }: { day: keyof typeof selectedDays; label: string }) => ( + toggleDay(day)} + disabled={!enableReminders} + > + + {label} + + + ); + + const handleTimeChange = (event: any, selectedTime?: Date) => { + setShowTimePicker(Platform.OS === 'ios'); + if (selectedTime) { + setReminderTime(selectedTime); + } + }; + + return ( + + + + Add New Habit + + + Habit Name + + + + + Description (Optional) + + + + + + Enable Reminders + + + + + {enableReminders && ( + <> + + Reminder Time + {Platform.OS === 'ios' ? ( + + ) : ( + <> + setShowTimePicker(true)} + style={styles.timePickerButton} + > + + {format(reminderTime, 'hh:mm a')} + + + {showTimePicker && ( + + )} + + )} + + + + Repeat on days + + + + + + + + + + + + )} + + + + Cancel + + + Add Habit + + + + + + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalView: { + width: '90%', + maxHeight: '80%', + backgroundColor: 'white', + borderRadius: 20, + padding: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalTitle: { + fontSize: 22, + fontWeight: 'bold', + color: '#2c3e50', + marginBottom: 20, + textAlign: 'center', + }, + inputGroup: { + marginBottom: 16, + }, + reminderHeaderRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + inputLabel: { + fontSize: 16, + fontWeight: '600', + color: '#2c3e50', + marginBottom: 8, + }, + input: { + backgroundColor: '#f8f9fa', + borderWidth: 1, + borderColor: '#dfe6e9', + borderRadius: 8, + padding: 12, + fontSize: 16, + color: '#2c3e50', + }, + textArea: { + height: 80, + textAlignVertical: 'top', + }, + timePicker: { + height: 120, + marginTop: -10, + }, + timePickerButton: { + backgroundColor: '#f8f9fa', + borderWidth: 1, + borderColor: '#dfe6e9', + borderRadius: 8, + padding: 12, + }, + timeText: { + fontSize: 16, + color: '#2c3e50', + }, + daysContainer: { + flexDirection: 'row', + gap: 8, + paddingVertical: 8, + }, + dayButton: { + width: 40, + height: 40, + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + }, + selectedDay: { + backgroundColor: '#3498db', + }, + unselectedDay: { + backgroundColor: '#f0f0f0', + }, + disabledDay: { + backgroundColor: '#ecf0f1', + opacity: 0.6, + }, + dayButtonText: { + fontSize: 16, + fontWeight: '600', + }, + selectedDayText: { + color: 'white', + }, + unselectedDayText: { + color: '#7f8c8d', + }, + disabledDayText: { + color: '#bdc3c7', + }, + buttonContainer: { + flexDirection: 'row', + marginTop: 20, + justifyContent: 'space-between', + }, + cancelButton: { + backgroundColor: '#f8f9fa', + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + borderWidth: 1, + borderColor: '#dfe6e9', + flex: 1, + marginRight: 8, + alignItems: 'center', + }, + cancelButtonText: { + color: '#2c3e50', + fontSize: 16, + fontWeight: '600', + }, + addButton: { + backgroundColor: '#3498db', + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + flex: 1, + marginLeft: 8, + alignItems: 'center', + }, + disabledButton: { + backgroundColor: '#95a5a6', + }, + addButtonText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + }, +}); + +export default AddHabitModal; \ No newline at end of file diff --git a/components/AuthProtectedRoute.tsx b/components/AuthProtectedRoute.tsx new file mode 100644 index 0000000..ddd458d --- /dev/null +++ b/components/AuthProtectedRoute.tsx @@ -0,0 +1,55 @@ +import React, { ReactNode, useEffect } from 'react'; +import { View, ActivityIndicator, Text, StyleSheet } from 'react-native'; +import { router } from 'expo-router'; +import { useAuth } from '../context/AuthContext'; + +interface AuthProtectedRouteProps { + children: ReactNode; +} + +/** + * A component that protects routes by checking if the user is authenticated. + * If not authenticated, redirects to the login screen. + * Shows a loading indicator while checking authentication status. + */ +const AuthProtectedRoute: React.FC = ({ children }) => { + const { isAuthenticated, isLoading, loadingMessage } = useAuth(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + // Not authenticated, redirect to login + router.replace('/login'); + } + }, [isAuthenticated, isLoading]); + + if (isLoading) { + return ( + + + {loadingMessage} + + ); + } + + if (!isAuthenticated) { + return null; // Will redirect to login via the useEffect + } + + return <>{children}; +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + color: '#555', + }, +}); + +export default AuthProtectedRoute; \ No newline at end of file diff --git a/components/HabitDayIndicator.tsx b/components/HabitDayIndicator.tsx new file mode 100644 index 0000000..2554a4d --- /dev/null +++ b/components/HabitDayIndicator.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Platform +} from 'react-native'; + +interface HabitDayIndicatorProps { + day: string; + isCompleted: boolean; + isToday: boolean; + isSelected?: boolean; + onPress: () => void; +} + +const HabitDayIndicator: React.FC = ({ + day, + isCompleted, + isToday, + isSelected = false, + onPress +}) => { + return ( + + + {day} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f0f0f0', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + }, + android: { + elevation: 1, + }, + }), + }, + dayText: { + fontSize: 14, + fontWeight: '600', + color: '#7f8c8d', + }, + completedContainer: { + backgroundColor: '#27ae60', + // No border when completed + borderWidth: 0, + }, + completedText: { + color: '#fff', + fontWeight: 'bold', + }, + todayContainer: { + borderWidth: 2, + borderColor: '#3498db', + }, + todayText: { + color: '#3498db', + fontWeight: 'bold', + }, + selectedContainer: { + backgroundColor: '#4a90e2', + borderWidth: 0, + }, + selectedText: { + color: '#fff', + fontWeight: 'bold', + } +}); + +export default HabitDayIndicator; \ No newline at end of file diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx new file mode 100644 index 0000000..b454f30 --- /dev/null +++ b/context/AuthContext.tsx @@ -0,0 +1,231 @@ +import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; +import { Alert } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { router } from 'expo-router'; +import * as WebBrowser from 'expo-web-browser'; +import * as Google from 'expo-auth-session/providers/google'; +import { + GoogleAuthProvider, + signInWithCredential, + onAuthStateChanged, + User, + signOut as firebaseSignOut +} from 'firebase/auth'; +import { auth } from '../config/firebase'; +import { Platform } from 'react-native'; + +// Register for redirect callback +WebBrowser.maybeCompleteAuthSession(); + +// Define types for our context +type UserInfo = User | null | undefined; + +interface AuthContextType { + userInfo: UserInfo; + isLoading: boolean; + loadingMessage: string; + signInWithGoogle: () => Promise; + signOut: () => Promise; + isAuthenticated: boolean; +} + +// Create the context with a default value +const AuthContext = createContext({ + userInfo: null, + isLoading: false, + loadingMessage: '', + signInWithGoogle: async () => {}, + signOut: async () => {}, + isAuthenticated: false, +}); + +// Create a provider component +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [userInfo, setUserInfo] = useState(null); + const [isLoading, setIsLoading] = useState(true); // Start as true to check for existing session + const [loadingMessage, setLoadingMessage] = useState('Checking login status...'); + + // Setup Google Auth Request + // This is moved from login.tsx to the context for better reusability + const [request, response, promptAsync] = Google.useAuthRequest({ + webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID, + androidClientId: process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID, + scopes: ['profile', 'email', 'openid'], + redirectUri: Platform.select({ + android: 'com.hiteshjoshi.tracker:/oauthredirect', + web: 'https://auth.expo.io/@hiteshjoshi/Tracker', + ios: 'com.hiteshjoshi.tracker:/oauthredirect' + }) + }); + + // Check for existing session and set up auth state listener + useEffect(() => { + const checkExistingSession = async () => { + try { + // Check if there's stored user data + const userJSON = await AsyncStorage.getItem('@user'); + if (userJSON) { + const userData = JSON.parse(userJSON); + setUserInfo(userData); + } + } catch (error) { + console.error('Error checking existing session:', error); + } finally { + // Always finish loading after checking, even if there's an error + setIsLoading(false); + } + }; + + // Monitor Firebase auth state + const unsubscribe = onAuthStateChanged(auth, (user) => { + if (user) { + console.log('User is signed in:', user.uid); + setUserInfo(user); + saveUserToStorage(user); + } else { + console.log('User is signed out'); + setUserInfo(null); + // Clear stored user data + AsyncStorage.removeItem('@user'); + } + // Update loading state + setIsLoading(false); + }); + + // Check for existing session + checkExistingSession(); + + // Cleanup subscription on unmount + return () => unsubscribe(); + }, []); + + // Handle Google Auth Response + useEffect(() => { + if (response?.type === 'success') { + setIsLoading(true); + setLoadingMessage('Getting user info...'); + handleGoogleSignInResponse(response); + } else if (response?.type === 'error' || response?.type === 'dismiss') { + setIsLoading(false); + Alert.alert('Authentication failed', 'Google sign in was unsuccessful. Please try again.'); + } + }, [response]); + + // Save user data to AsyncStorage + const saveUserToStorage = async (user: any) => { + try { + const userData = { + uid: user.uid, + email: user.email, + displayName: user.displayName, + photoURL: user.photoURL + }; + await AsyncStorage.setItem('@user', JSON.stringify(userData)); + } catch (error) { + console.error('Error saving user to storage:', error); + } + }; + + // Process Google sign-in response + const handleGoogleSignInResponse = async (response: any) => { + try { + console.log('Got authentication response:', response.type); + + if (response.type === 'success') { + // Check if we have an id_token (using responseType: 'id_token') + if (response.params.id_token) { + const { id_token } = response.params; + console.log('Received ID token from Google Auth'); + + setLoadingMessage('Authenticating with Firebase...'); + // Create a Google credential with ID token + const credential = GoogleAuthProvider.credential(id_token); + + // Sign in to Firebase with the Google credential + const result = await signInWithCredential(auth, credential); + console.log('Firebase sign in successful:', result.user.uid); + + // Save user data + setUserInfo(result.user); + saveUserToStorage(result.user); + + // Navigate to home screen + setLoadingMessage('Redirecting to app...'); + router.replace('/today'); + return null; + } + // If we got an authorization code instead (default responseType) + else if (response.params.code) { + console.log('Received authorization code from Google Auth'); + Alert.alert( + 'Authorization Code Flow', + 'Received an authorization code instead of an ID token. ' + + 'Please update the configuration to use responseType: "id_token".' + ); + setIsLoading(false); + } else { + console.error('No id_token or code found in response'); + setIsLoading(false); + Alert.alert('Authentication Error', 'No authentication credentials received from Google.'); + } + } + } catch (error) { + console.error('Error signing in with Google:', error); + setIsLoading(false); + Alert.alert('Authentication Error', 'An error occurred during Google sign in. Please try again.'); + } + }; + + // Sign in with Google + const signInWithGoogle = async () => { + setIsLoading(true); + setLoadingMessage('Opening Google Sign In...'); + try { + await promptAsync(); + } catch (error) { + console.error('Error opening Google Sign In:', error); + setIsLoading(false); + Alert.alert('Sign In Error', 'Could not open Google Sign In. Please try again.'); + } + }; + + // Sign out + const signOut = async () => { + setIsLoading(true); + setLoadingMessage('Signing out...'); + try { + await firebaseSignOut(auth); + await AsyncStorage.removeItem('@user'); + setUserInfo(null); + // Navigate to login screen + router.replace('/login'); + } catch (error) { + console.error('Error signing out:', error); + Alert.alert('Sign Out Error', 'Could not sign out. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( + + {children} + + ); +}; + +// Custom hook to use the auth context +export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/context/NotificationContext.tsx b/context/NotificationContext.tsx new file mode 100644 index 0000000..30bcf72 --- /dev/null +++ b/context/NotificationContext.tsx @@ -0,0 +1,100 @@ +import React, { createContext, useContext, useEffect, useState, useRef } from 'react'; +import * as Notifications from 'expo-notifications'; +import { NotificationService } from '../services/notificationService'; +import { habitService } from '../services/habitService'; +import { useAuth } from './AuthContext'; +import { router } from 'expo-router'; +import { Habit, NotificationContextType, NotificationProviderProps } from '../models/types'; + +// Create the context +const NotificationContext = createContext(null); + +// Custom hook to use the notification context +export const useNotifications = () => { + const context = useContext(NotificationContext); + if (!context) { + throw new Error('useNotifications must be used within a NotificationProvider'); + } + return context; +}; + +// Provider component +export const NotificationProvider: React.FC = ({ children }) => { + const { userInfo } = useAuth(); + const [expoPushToken, setExpoPushToken] = useState(); + const notificationListener = useRef(); + const responseListener = useRef(); + + // Initialize notifications when the app starts + useEffect(() => { + // Register for push notifications + NotificationService.registerForPushNotificationsAsync() + .then(token => { + if (token) { + setExpoPushToken(token.data); + } + }) + .catch(err => console.error('Failed to get push token:', err)); + + // Set up notification listeners + notificationListener.current = Notifications.addNotificationReceivedListener(notification => { + // Handle received notification + console.log('Notification received:', notification); + }); + + // Set up notification response handler + responseListener.current = NotificationService.setNotificationResponseHandler((habitId) => { + // Navigate to the habit screen when a notification is tapped + router.push('/habits'); + }); + + // Clean up listeners when component unmounts + return () => { + if (notificationListener.current) { + Notifications.removeNotificationSubscription(notificationListener.current); + } + if (responseListener.current) { + Notifications.removeNotificationSubscription(responseListener.current); + } + }; + }, []); + + // Set up user-specific notifications when the user changes + useEffect(() => { + if (userInfo?.uid) { + // Load the user's habits and schedule notifications for them + habitService.getUserHabits(userInfo.uid) + .then(habits => { + NotificationService.rescheduleAllHabitNotifications(habits); + }) + .catch(err => console.error('Failed to load habits for notifications:', err)); + } + }, [userInfo]); + + // Function to schedule a daily reminder for a habit + const scheduleDailyReminder = async (habit: Habit) => { + return await NotificationService.scheduleHabitNotification(habit); + }; + + // Function to cancel a reminder for a habit + const cancelReminder = async (habitId: string) => { + await NotificationService.cancelHabitNotifications(habitId); + }; + + // Function to reschedule all reminders + const rescheduleAllReminders = async (habits: Habit[]) => { + await NotificationService.rescheduleAllHabitNotifications(habits); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..8beb344 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/ios/.xcode.env b/ios/.xcode.env new file mode 100644 index 0000000..3d5782c --- /dev/null +++ b/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..3b75106 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,66 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] + +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' +install! 'cocoapods', + :deterministic_uuids => false + +prepare_react_native_project! + +target 'Tracker' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', + ) + + # This is necessary for Xcode 14, because it signs resource bundles by default + # when building for devices. + installer.target_installation_results.pod_target_installation_results + .each do |pod_name, target_installation_result| + target_installation_result.resource_bundle_targets.each do |resource_bundle_target| + resource_bundle_target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..be3e2aa --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,2437 @@ +PODS: + - boost (1.84.0) + - DoubleConversion (1.1.6) + - EXApplication (6.0.2): + - ExpoModulesCore + - EXConstants (17.0.7): + - ExpoModulesCore + - EXJSONUtils (0.14.0) + - EXManifests (0.15.6): + - ExpoModulesCore + - EXNotifications (0.29.13): + - ExpoModulesCore + - Expo (52.0.37): + - ExpoModulesCore + - expo-dev-client (5.0.12): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (5.0.29): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Main (= 5.0.29) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Main (5.0.29): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Unsafe (5.0.29): + - DoubleConversion + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu (6.0.19): + - DoubleConversion + - expo-dev-menu/Main (= 6.0.19) + - expo-dev-menu/ReactNativeCompatibles (= 6.0.19) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu-interface (1.9.3) + - expo-dev-menu/Main (6.0.19): + - DoubleConversion + - EXManifests + - expo-dev-menu-interface + - expo-dev-menu/Vendored + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/ReactNativeCompatibles (6.0.19): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/SafeAreaView (6.0.19): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/Vendored (6.0.19): + - DoubleConversion + - expo-dev-menu/SafeAreaView + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoAsset (11.0.4): + - ExpoModulesCore + - ExpoCrypto (14.0.2): + - ExpoModulesCore + - ExpoDevice (7.0.2): + - ExpoModulesCore + - ExpoFileSystem (18.0.11): + - ExpoModulesCore + - ExpoFont (13.0.4): + - ExpoModulesCore + - ExpoHead (4.0.17): + - ExpoModulesCore + - ExpoKeepAwake (14.0.3): + - ExpoModulesCore + - ExpoLinearGradient (14.0.2): + - ExpoModulesCore + - ExpoLinking (7.0.5): + - ExpoModulesCore + - ExpoModulesCore (2.2.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoRandom (14.0.1): + - ExpoModulesCore + - ExpoSplashScreen (0.29.22): + - ExpoModulesCore + - ExpoSystemUI (4.0.8): + - ExpoModulesCore + - ExpoWebBrowser (14.0.2): + - ExpoModulesCore + - EXUpdatesInterface (1.0.0): + - ExpoModulesCore + - FBLazyVector (0.76.7) + - fmt (9.1.0) + - glog (0.3.5) + - hermes-engine (0.76.7): + - hermes-engine/Pre-built (= 0.76.7) + - hermes-engine/Pre-built (0.76.7) + - RCT-Folly (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Default (= 2024.01.01.00) + - RCT-Folly/Default (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Fabric (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCTDeprecation (0.76.7) + - RCTRequired (0.76.7) + - RCTTypeSafety (0.76.7): + - FBLazyVector (= 0.76.7) + - RCTRequired (= 0.76.7) + - React-Core (= 0.76.7) + - React (0.76.7): + - React-Core (= 0.76.7) + - React-Core/DevSupport (= 0.76.7) + - React-Core/RCTWebSocket (= 0.76.7) + - React-RCTActionSheet (= 0.76.7) + - React-RCTAnimation (= 0.76.7) + - React-RCTBlob (= 0.76.7) + - React-RCTImage (= 0.76.7) + - React-RCTLinking (= 0.76.7) + - React-RCTNetwork (= 0.76.7) + - React-RCTSettings (= 0.76.7) + - React-RCTText (= 0.76.7) + - React-RCTVibration (= 0.76.7) + - React-callinvoker (0.76.7) + - React-Core (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.76.7) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/CoreModulesHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/Default (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/DevSupport (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.76.7) + - React-Core/RCTWebSocket (= 0.76.7) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTActionSheetHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTAnimationHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTBlobHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTImageHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTLinkingHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTNetworkHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTSettingsHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTTextHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTVibrationHeaders (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTWebSocket (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.76.7) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-CoreModules (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety (= 0.76.7) + - React-Core/CoreModulesHeaders (= 0.76.7) + - React-jsi (= 0.76.7) + - React-jsinspector + - React-NativeModulesApple + - React-RCTBlob + - React-RCTImage (= 0.76.7) + - ReactCodegen + - ReactCommon + - SocketRocket (= 0.7.1) + - React-cxxreact (0.76.7): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.76.7) + - React-debug (= 0.76.7) + - React-jsi (= 0.76.7) + - React-jsinspector + - React-logger (= 0.76.7) + - React-perflogger (= 0.76.7) + - React-runtimeexecutor (= 0.76.7) + - React-timing (= 0.76.7) + - React-debug (0.76.7) + - React-defaultsnativemodule (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-domnativemodule + - React-Fabric + - React-featureflags + - React-featureflagsnativemodule + - React-graphics + - React-idlecallbacksnativemodule + - React-ImageManager + - React-microtasksnativemodule + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-domnativemodule (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricComponents + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/animations (= 0.76.7) + - React-Fabric/attributedstring (= 0.76.7) + - React-Fabric/componentregistry (= 0.76.7) + - React-Fabric/componentregistrynative (= 0.76.7) + - React-Fabric/components (= 0.76.7) + - React-Fabric/core (= 0.76.7) + - React-Fabric/dom (= 0.76.7) + - React-Fabric/imagemanager (= 0.76.7) + - React-Fabric/leakchecker (= 0.76.7) + - React-Fabric/mounting (= 0.76.7) + - React-Fabric/observers (= 0.76.7) + - React-Fabric/scheduler (= 0.76.7) + - React-Fabric/telemetry (= 0.76.7) + - React-Fabric/templateprocessor (= 0.76.7) + - React-Fabric/uimanager (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/animations (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistry (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistrynative (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.76.7) + - React-Fabric/components/root (= 0.76.7) + - React-Fabric/components/view (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/legacyviewmanagerinterop (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/root (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/view (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric/core (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/dom (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/imagemanager (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/leakchecker (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/mounting (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers/events (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/scheduler (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancetimeline + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/telemetry (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/templateprocessor (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager/consistency (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-FabricComponents (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.76.7) + - React-FabricComponents/textlayoutmanager (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.76.7) + - React-FabricComponents/components/iostextinput (= 0.76.7) + - React-FabricComponents/components/modal (= 0.76.7) + - React-FabricComponents/components/rncore (= 0.76.7) + - React-FabricComponents/components/safeareaview (= 0.76.7) + - React-FabricComponents/components/scrollview (= 0.76.7) + - React-FabricComponents/components/text (= 0.76.7) + - React-FabricComponents/components/textinput (= 0.76.7) + - React-FabricComponents/components/unimplementedview (= 0.76.7) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/inputaccessory (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/iostextinput (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/modal (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/rncore (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/safeareaview (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/scrollview (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/text (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/textinput (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/unimplementedview (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/textlayoutmanager (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricImage (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired (= 0.76.7) + - RCTTypeSafety (= 0.76.7) + - React-Fabric + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.76.7) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - Yoga + - React-featureflags (0.76.7) + - React-featureflagsnativemodule (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-graphics (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-jsi + - React-jsiexecutor + - React-utils + - React-hermes (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-cxxreact (= 0.76.7) + - React-jsi + - React-jsiexecutor (= 0.76.7) + - React-jsinspector + - React-perflogger (= 0.76.7) + - React-runtimeexecutor + - React-idlecallbacksnativemodule (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-ImageManager (0.76.7): + - glog + - RCT-Folly/Fabric + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - React-jserrorhandler (0.76.7): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-cxxreact + - React-debug + - React-jsi + - React-jsi (0.76.7): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-jsiexecutor (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-cxxreact (= 0.76.7) + - React-jsi (= 0.76.7) + - React-jsinspector + - React-perflogger (= 0.76.7) + - React-jsinspector (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-featureflags + - React-jsi + - React-perflogger (= 0.76.7) + - React-runtimeexecutor (= 0.76.7) + - React-jsitracing (0.76.7): + - React-jsi + - React-logger (0.76.7): + - glog + - React-Mapbuffer (0.76.7): + - glog + - React-debug + - React-microtasksnativemodule (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context (4.12.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 4.12.0) + - react-native-safe-area-context/fabric (= 4.12.0) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (4.12.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (4.12.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-nativeconfig (0.76.7) + - React-NativeModulesApple (0.76.7): + - glog + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-jsi + - React-jsinspector + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-perflogger (0.76.7): + - DoubleConversion + - RCT-Folly (= 2024.01.01.00) + - React-performancetimeline (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - React-cxxreact + - React-timing + - React-RCTActionSheet (0.76.7): + - React-Core/RCTActionSheetHeaders (= 0.76.7) + - React-RCTAnimation (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Core/RCTAnimationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTAppDelegate (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-nativeconfig + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-RCTNetwork + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon + - React-RCTBlob (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTNetwork + - ReactCodegen + - ReactCommon + - React-RCTFabric (0.76.7): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-Core + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-nativeconfig + - React-performancetimeline + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - Yoga + - React-RCTImage (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTNetwork + - ReactCodegen + - ReactCommon + - React-RCTLinking (0.76.7): + - React-Core/RCTLinkingHeaders (= 0.76.7) + - React-jsi (= 0.76.7) + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - ReactCommon/turbomodule/core (= 0.76.7) + - React-RCTNetwork (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Core/RCTNetworkHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTSettings (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTText (0.76.7): + - React-Core/RCTTextHeaders (= 0.76.7) + - Yoga + - React-RCTVibration (0.76.7): + - RCT-Folly (= 2024.01.01.00) + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-rendererconsistency (0.76.7) + - React-rendererdebug (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - RCT-Folly (= 2024.01.01.00) + - React-debug + - React-rncore (0.76.7) + - React-RuntimeApple (0.76.7): + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-callinvoker + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - React-RuntimeCore (0.76.7): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - React-runtimeexecutor (0.76.7): + - React-jsi (= 0.76.7) + - React-RuntimeHermes (0.76.7): + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsitracing + - React-nativeconfig + - React-RuntimeCore + - React-utils + - React-runtimescheduler (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - React-timing (0.76.7) + - React-utils (0.76.7): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-debug + - React-jsi (= 0.76.7) + - ReactCodegen (0.76.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactCommon (0.76.7): + - ReactCommon/turbomodule (= 0.76.7) + - ReactCommon/turbomodule (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.76.7) + - React-cxxreact (= 0.76.7) + - React-jsi (= 0.76.7) + - React-logger (= 0.76.7) + - React-perflogger (= 0.76.7) + - ReactCommon/turbomodule/bridging (= 0.76.7) + - ReactCommon/turbomodule/core (= 0.76.7) + - ReactCommon/turbomodule/bridging (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.76.7) + - React-cxxreact (= 0.76.7) + - React-jsi (= 0.76.7) + - React-logger (= 0.76.7) + - React-perflogger (= 0.76.7) + - ReactCommon/turbomodule/core (0.76.7): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.76.7) + - React-cxxreact (= 0.76.7) + - React-debug (= 0.76.7) + - React-featureflags (= 0.76.7) + - React-jsi (= 0.76.7) + - React-logger (= 0.76.7) + - React-perflogger (= 0.76.7) + - React-utils (= 0.76.7) + - RNCAsyncStorage (1.24.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNDateTimePicker (8.2.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNGestureHandler (2.20.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated (= 3.16.7) + - RNReanimated/worklets (= 3.16.7) + - Yoga + - RNReanimated/reanimated (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated/apple (= 3.16.7) + - Yoga + - RNReanimated/reanimated/apple (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated/worklets (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (4.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 4.4.0) + - Yoga + - RNScreens/common (4.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - SocketRocket (0.7.1) + - Yoga (0.0.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXApplication (from `../node_modules/expo-application/ios`) + - EXConstants (from `../node_modules/expo-constants/ios`) + - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) + - EXManifests (from `../node_modules/expo-manifests/ios`) + - EXNotifications (from `../node_modules/expo-notifications/ios`) + - Expo (from `../node_modules/expo`) + - expo-dev-client (from `../node_modules/expo-dev-client/ios`) + - expo-dev-launcher (from `../node_modules/expo-dev-launcher`) + - expo-dev-menu (from `../node_modules/expo-dev-menu`) + - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) + - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoCrypto (from `../node_modules/expo-crypto/ios`) + - ExpoDevice (from `../node_modules/expo-device/ios`) + - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) + - ExpoFont (from `../node_modules/expo-font/ios`) + - ExpoHead (from `../node_modules/expo-router/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) + - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoRandom (from `../node_modules/expo-random/ios`) + - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) + - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) + - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) + - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactCodegen (from `build/generated/ios`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - SocketRocket + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/expo-application/ios" + EXConstants: + :path: "../node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../node_modules/expo-json-utils/ios" + EXManifests: + :path: "../node_modules/expo-manifests/ios" + EXNotifications: + :path: "../node_modules/expo-notifications/ios" + Expo: + :path: "../node_modules/expo" + expo-dev-client: + :path: "../node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../node_modules/expo-dev-menu-interface/ios" + ExpoAsset: + :path: "../node_modules/expo-asset/ios" + ExpoCrypto: + :path: "../node_modules/expo-crypto/ios" + ExpoDevice: + :path: "../node_modules/expo-device/ios" + ExpoFileSystem: + :path: "../node_modules/expo-file-system/ios" + ExpoFont: + :path: "../node_modules/expo-font/ios" + ExpoHead: + :path: "../node_modules/expo-router/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" + ExpoLinearGradient: + :path: "../node_modules/expo-linear-gradient/ios" + ExpoLinking: + :path: "../node_modules/expo-linking/ios" + ExpoModulesCore: + :path: "../node_modules/expo-modules-core" + ExpoRandom: + :path: "../node_modules/expo-random/ios" + ExpoSplashScreen: + :path: "../node_modules/expo-splash-screen/ios" + ExpoSystemUI: + :path: "../node_modules/expo-system-ui/ios" + ExpoWebBrowser: + :path: "../node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../node_modules/expo-updates-interface/ios" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + fmt: + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2024-11-12-RNv0.76.2-5b4aa20c719830dcf5684832b89a6edb95ac3d64 + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + React-nativeconfig: + :path: "../node_modules/react-native/ReactCommon" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactCodegen: + :path: build/generated/ios + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNDateTimePicker: + :path: "../node_modules/@react-native-community/datetimepicker" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + boost: 1dca942403ed9342f98334bf4c3621f011aa7946 + DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819 + EXConstants: 30c43b9ca8391c31bab9cc45c97f16eddcf11042 + EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 + EXManifests: 6bcc8c0997fc00831f2aa874d563004cfc509af0 + EXNotifications: a762c9647d87682e9822f5798ca6666b5a81e213 + Expo: 63515728b6f238842dc95702eb0fb385f00d7c5a + expo-dev-client: 6f30fa7fbf4aa6cb5765b983da2411bf63f665bb + expo-dev-launcher: f37027e55535ac28df696da95e2b222e7333ecd7 + expo-dev-menu: 9adf097a707609148baa3002e87ae5860f27615e + expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4 + ExpoAsset: 4033893dfb333b444bf45e951351254137b658d5 + ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4 + ExpoDevice: 1662ecdf42a8b5b2cf60ef7cecaf5a297639dd63 + ExpoFileSystem: 2d804c73034c0d65eb2a640e0a77627033b69edc + ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 + ExpoHead: 5d1c5c20e3933db5d9ca349424cb3b9a4a84b706 + ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 + ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60 + ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 + ExpoModulesCore: 7f5e59799b89e5fad1d7cc6070744c1003ca320f + ExpoRandom: d1444df65007bdd4070009efd5dab18e20bf0f00 + ExpoSplashScreen: 0f281e3c2ded4757d2309276c682d023c6299c77 + ExpoSystemUI: 2e5356d22b077c56c132b4dcbc5d69dd667b1f8c + ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3 + EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db + FBLazyVector: ca8044c9df513671c85167838b4188791b6f37e1 + fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be + glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + hermes-engine: eb4a80f6bf578536c58a44198ec93a30f6e69218 + RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 + RCTDeprecation: 7691283dd69fed46f6653d376de6fa83aaad774c + RCTRequired: eac044a04629288f272ee6706e31f81f3a2b4bfe + RCTTypeSafety: cfe499e127eda6dd46e5080e12d80d0bfe667228 + React: 1f3737a983fdd26fb3d388ddbca41a26950fe929 + React-callinvoker: 5c15ac628eab5468fe0b4dc453495f4742761f00 + React-Core: e467bf49f10da6fe92d915d2311cd0fd9bfbe052 + React-CoreModules: 0299b3c0782edd3b37c8445ba07bf18ceb73812d + React-cxxreact: 54e253030b3b82b05575f19a1fb0e25c049f30ba + React-debug: 2086b55a5e55fb0abae58c42b8f280ebd708c956 + React-defaultsnativemodule: f80f41ea8c1216917fd224b553291360e0e6a175 + React-domnativemodule: b14aaaf4afbaa7e1dbc86ad78cbcc71eb59f1faf + React-Fabric: 409ce8a065374d737bdbc0fce506dcdda8f51e88 + React-FabricComponents: bd5faafffd07e56cf217d5417e80ec29348c19d9 + React-FabricImage: 04d01f3ecfed6121733613a5c794f684e81cb3fb + React-featureflags: 4503c901bf16b267b689e8a1aed24e951e0b091b + React-featureflagsnativemodule: 79c980bfc96bcdcc9bd793d49fe75bbfb0e417ad + React-graphics: c2febdc940fb3ebdaef082d940b70254ef49c7a1 + React-hermes: 91baa15c07e76b0768d6e10f4dac1c080a47eef4 + React-idlecallbacksnativemodule: 5daef402290b91e54a884101b032186c03fa1827 + React-ImageManager: b258354a48a92168edc41fdc0c14a4310cc4d576 + React-jserrorhandler: 45d858315f6474dad3912aadb3f6595004dc5f4f + React-jsi: 87fa67556d7a82125bc77930bf973717fb726d14 + React-jsiexecutor: 3a92052dd96cff1cd693fa3ef8d9738b1d05372a + React-jsinspector: 05aff7dd91b0685d351cdeb8c151c9f9ec97accd + React-jsitracing: 419fa21e8543f5a938b11b5a0bfc257b00dac7a5 + React-logger: 5cad0c76d056809523289e589309012215a393b5 + React-Mapbuffer: a381120aea722d2244d4e4b663a10d4c3b2d4e51 + React-microtasksnativemodule: d9b946675010659cddd1c7611c074216579c8ad3 + react-native-safe-area-context: 0f16e24dc808e9f0ced17f2bdcec692b2376fb68 + React-nativeconfig: 67fa7a63ea288cb5b1d0dd2deaf240405fec164f + React-NativeModulesApple: 34b7a4d7441a4ee78d18109ff107c1ccf7c074a9 + React-perflogger: d1149037ac466ad2141d4ae541ca16cb73b2343b + React-performancetimeline: 6b46b0a17727a3ec22ec4777d156d6b6efc4f8eb + React-RCTActionSheet: ad84d5a0bd1ad1782f0b78b280c6f329ad79a53a + React-RCTAnimation: 64ed42bb43b33b0d861126f83048429606390903 + React-RCTAppDelegate: de8150cd7e748bd7a98ffc05c88f21c668407ab4 + React-RCTBlob: e74dfdbbfcd46d9d1eec3b3a0f045e655e3f7861 + React-RCTFabric: bc0327e719fb12f969ac0e17485ba274b9c2c335 + React-RCTImage: 1b6d8ad60f74a3cec4ee52e0ca55f1773afd03f4 + React-RCTLinking: 88b2384d876346fbb16839a60c1d20830b2e95fe + React-RCTNetwork: 88aa473814e796d3a7bc6a0b51e7ae5749bdc243 + React-RCTSettings: 0d73a1846aef87ef07c2026c186ea0d80602a130 + React-RCTText: bfdb776f849156f895909ee999b4b5f2f9cf9a0b + React-RCTVibration: 81c8bbcc841ce5a7ae6e1bd2ec949b30e58d1fcf + React-rendererconsistency: 65d4692825fda4d9516924b68c29d0f28da3158c + React-rendererdebug: ab3696594d3506acc22ecea4dd68ac258c529c2d + React-rncore: 6aca111c05a48c58189a005cb10a7b52780738dc + React-RuntimeApple: 5245e8cf30e417fe3e798ed991b938679656ab8f + React-RuntimeCore: c79d23b31aded614f4afeaac53f4da37c792c362 + React-runtimeexecutor: 732038d7c356ba74132f1d16253a410621d3c2c1 + React-RuntimeHermes: b3b1d7fc42d74141a71ae23fedbc4e07e5a7fbd2 + React-runtimescheduler: 6e804311c6c9512ffe7f4b68d012767b225c48a1 + React-timing: c2915214b94a62bdf77d2965c31f76bc25b362a5 + React-utils: 0342746d2cf989cf5e0d1b84c98cfa152edbdf3f + ReactCodegen: e1c019dc68733dd2c5d3b263b4a6dc72002c0045 + ReactCommon: 81e0744ee33adfd6d586141b927024f488bc49ea + RNCAsyncStorage: 71c8f2a45fd37d39669192239d13a9d291c55b43 + RNDateTimePicker: 9ec036a62ad0f5a2d04c1c7829338e87b4e423e7 + RNGestureHandler: 16ef3dc2d7ecb09f240f25df5255953c4098819b + RNReanimated: a2692304a6568bc656c04c8ffea812887d37436e + RNScreens: 351f431ef2a042a1887d4d90e1c1024b8ae9d123 + SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + Yoga: 90d80701b27946c4b23461c00a7207f300a6ff71 + +PODFILE CHECKSUM: 7a1e301a83e6f4d8dfc028d3e9e39c7d9f898275 + +COCOAPODS: 1.16.2 diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json new file mode 100644 index 0000000..417e2e5 --- /dev/null +++ b/ios/Podfile.properties.json @@ -0,0 +1,5 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "newArchEnabled": "true" +} diff --git a/ios/Tracker.xcodeproj/project.pbxproj b/ios/Tracker.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e0b1d2f --- /dev/null +++ b/ios/Tracker.xcodeproj/project.pbxproj @@ -0,0 +1,556 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 96905EF65AED1B983A6B3ABC /* libPods-Tracker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Tracker.a */; }; + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + BCCC0724A8B340D48AD4B25F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 05D90B38B4C547DDBB9550B1 /* GoogleService-Info.plist */; }; + E9D9FAF830F23893D64DF5A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A99EF54B41BC47756D6BA6A1 /* PrivacyInfo.xcprivacy */; }; + F3CC2E1692364FFF9397B950 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F088CB15334B51A76D29B1 /* noop-file.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05D90B38B4C547DDBB9550B1 /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Tracker/GoogleService-Info.plist"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* Tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tracker.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Tracker/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Tracker/AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Tracker/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tracker/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Tracker/main.m; sourceTree = ""; }; + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Tracker.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tracker.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2E3173556A471DD304B334 /* Pods-Tracker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tracker.debug.xcconfig"; path = "Target Support Files/Pods-Tracker/Pods-Tracker.debug.xcconfig"; sourceTree = ""; }; + 7A4D352CD337FB3A3BF06240 /* Pods-Tracker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tracker.release.xcconfig"; path = "Target Support Files/Pods-Tracker/Pods-Tracker.release.xcconfig"; sourceTree = ""; }; + A99EF54B41BC47756D6BA6A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Tracker/PrivacyInfo.xcprivacy; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Tracker/SplashScreen.storyboard; sourceTree = ""; }; + B3F088CB15334B51A76D29B1 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Tracker/noop-file.swift"; sourceTree = ""; }; + B9FE635DDFA843C0BE3DCDC1 /* Tracker-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Tracker-Bridging-Header.h"; path = "Tracker/Tracker-Bridging-Header.h"; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Tracker/ExpoModulesProvider.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 96905EF65AED1B983A6B3ABC /* libPods-Tracker.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* Tracker */ = { + isa = PBXGroup; + children = ( + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB71A68108700A75B9A /* main.m */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 05D90B38B4C547DDBB9550B1 /* GoogleService-Info.plist */, + B3F088CB15334B51A76D29B1 /* noop-file.swift */, + B9FE635DDFA843C0BE3DCDC1 /* Tracker-Bridging-Header.h */, + A99EF54B41BC47756D6BA6A1 /* PrivacyInfo.xcprivacy */, + ); + name = Tracker; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Tracker.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* Tracker */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + D65327D7A22EEC0BE12398D9 /* Pods */, + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Tracker.app */, + ); + name = Products; + sourceTree = ""; + }; + 92DBD88DE9BF7D494EA9DA96 /* Tracker */ = { + isa = PBXGroup; + children = ( + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */, + ); + name = Tracker; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = Tracker/Supporting; + sourceTree = ""; + }; + D65327D7A22EEC0BE12398D9 /* Pods */ = { + isa = PBXGroup; + children = ( + 6C2E3173556A471DD304B334 /* Pods-Tracker.debug.xcconfig */, + 7A4D352CD337FB3A3BF06240 /* Pods-Tracker.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 92DBD88DE9BF7D494EA9DA96 /* Tracker */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* Tracker */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Tracker" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + B09193E465B09AAC2C78CEB9 /* [Expo] Configure project */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + 82C6D3D937084994D9FCE93F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Tracker; + productName = Tracker; + productReference = 13B07F961A680F5B00A75B9A /* Tracker.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Tracker" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Tracker */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + BCCC0724A8B340D48AD4B25F /* GoogleService-Info.plist in Resources */, + E9D9FAF830F23893D64DF5A1 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Tracker-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Tracker/Pods-Tracker-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tracker/Pods-Tracker-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 82C6D3D937084994D9FCE93F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Tracker/Pods-Tracker-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tracker/Pods-Tracker-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B09193E465B09AAC2C78CEB9 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Tracker/expo-configure-project.sh\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, + F3CC2E1692364FFF9397B950 /* noop-file.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Tracker.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Tracker/Tracker.entitlements; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = Tracker/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.hiteshjoshi.tracker; + PRODUCT_NAME = Tracker; + SWIFT_OBJC_BRIDGING_HEADER = "Tracker/Tracker-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Tracker.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Tracker/Tracker.entitlements; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = Tracker/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.hiteshjoshi.tracker; + PRODUCT_NAME = Tracker; + SWIFT_OBJC_BRIDGING_HEADER = "Tracker/Tracker-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Tracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Tracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/Tracker.xcodeproj/xcshareddata/xcschemes/Tracker.xcscheme b/ios/Tracker.xcodeproj/xcshareddata/xcschemes/Tracker.xcscheme new file mode 100644 index 0000000..fce3c36 --- /dev/null +++ b/ios/Tracker.xcodeproj/xcshareddata/xcschemes/Tracker.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Tracker.xcworkspace/contents.xcworkspacedata b/ios/Tracker.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0f1322d --- /dev/null +++ b/ios/Tracker.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Tracker/AppDelegate.h b/ios/Tracker/AppDelegate.h new file mode 100644 index 0000000..1658a43 --- /dev/null +++ b/ios/Tracker/AppDelegate.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface AppDelegate : EXAppDelegateWrapper + +@end diff --git a/ios/Tracker/AppDelegate.mm b/ios/Tracker/AppDelegate.mm new file mode 100644 index 0000000..b27f832 --- /dev/null +++ b/ios/Tracker/AppDelegate.mm @@ -0,0 +1,62 @@ +#import "AppDelegate.h" + +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ + return [self bundleURL]; +} + +- (NSURL *)bundleURL +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +@end diff --git a/ios/Tracker/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/Tracker/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..2732229 Binary files /dev/null and b/ios/Tracker/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/Tracker/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/Tracker/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/ios/Tracker/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/Tracker/Images.xcassets/Contents.json b/ios/Tracker/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/ios/Tracker/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/ios/Tracker/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/Tracker/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..15f02ab --- /dev/null +++ b/ios/Tracker/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "components": { + "alpha": "1.000", + "blue": "1.00000000000000", + "green": "1.00000000000000", + "red": "1.00000000000000" + }, + "color-space": "srgb" + }, + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/Tracker/Info.plist b/ios/Tracker/Info.plist new file mode 100644 index 0000000..f544bed --- /dev/null +++ b/ios/Tracker/Info.plist @@ -0,0 +1,87 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Tracker + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + tracker + com.hiteshjoshi.tracker + + + + CFBundleURLSchemes + + com.googleusercontent.apps.648416025794-300vtsu65kjgajdfe13jes1kj69bu44p + + + + CFBundleURLSchemes + + exp+tracker + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Automatic + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/Tracker/PrivacyInfo.xcprivacy b/ios/Tracker/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..c6b452e --- /dev/null +++ b/ios/Tracker/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + 0A2A.1 + 3B52.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/ios/Tracker/SplashScreen.storyboard b/ios/Tracker/SplashScreen.storyboard new file mode 100644 index 0000000..81f3d72 --- /dev/null +++ b/ios/Tracker/SplashScreen.storyboard @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/Tracker/Supporting/Expo.plist b/ios/Tracker/Supporting/Expo.plist new file mode 100644 index 0000000..750be02 --- /dev/null +++ b/ios/Tracker/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/ios/Tracker/Tracker-Bridging-Header.h b/ios/Tracker/Tracker-Bridging-Header.h new file mode 100644 index 0000000..e11d920 --- /dev/null +++ b/ios/Tracker/Tracker-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/ios/Tracker/Tracker.entitlements b/ios/Tracker/Tracker.entitlements new file mode 100644 index 0000000..018a6e2 --- /dev/null +++ b/ios/Tracker/Tracker.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + \ No newline at end of file diff --git a/ios/Tracker/main.m b/ios/Tracker/main.m new file mode 100644 index 0000000..25181b6 --- /dev/null +++ b/ios/Tracker/main.m @@ -0,0 +1,10 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} + diff --git a/ios/Tracker/noop-file.swift b/ios/Tracker/noop-file.swift new file mode 100644 index 0000000..b2ffafb --- /dev/null +++ b/ios/Tracker/noop-file.swift @@ -0,0 +1,4 @@ +// +// @generated +// A blank Swift file must be created for native modules with Swift files to work correctly. +// diff --git a/models/AuthTypes.ts b/models/AuthTypes.ts new file mode 100644 index 0000000..eb7fe4c --- /dev/null +++ b/models/AuthTypes.ts @@ -0,0 +1,90 @@ +import { User } from 'firebase/auth'; + +// Basic user information in a more readable format +export interface UserInfo { + uid: string; + email: string | null; + displayName: string | null; + photoURL: string | null; +} + +// Auth response types +export interface GoogleAuthResponse { + type: 'success' | 'error' | 'dismiss'; + params?: { + id_token?: string; + code?: string; + [key: string]: any; + }; + error?: Error; +} + +// Authentication state +export interface AuthState { + userInfo: User | null; + isLoading: boolean; + loadingMessage: string; + isAuthenticated: boolean; +} + +// Authentication actions +export type AuthAction = + | { type: 'SIGN_IN_START', payload: string } + | { type: 'SIGN_IN_SUCCESS', payload: User } + | { type: 'SIGN_IN_FAILURE', payload: string } + | { type: 'SIGN_OUT' } + | { type: 'SET_LOADING', payload: { isLoading: boolean, message?: string } }; + +// Props for the AuthProvider component +export interface AuthProviderProps { + children: React.ReactNode; +} + +// Context value type +export interface AuthContextValue { + userInfo: User | null; + isLoading: boolean; + loadingMessage: string; + isAuthenticated: boolean; + signInWithGoogle: () => Promise; + signOut: () => Promise; + refreshUserData: () => Promise; +} + +// Storage keys +export const USER_STORAGE_KEY = '@user'; + +// Error types +export enum AuthErrorType { + SIGN_IN_CANCELLED = 'sign_in_cancelled', + SIGN_IN_FAILED = 'sign_in_failed', + NO_CREDENTIALS = 'no_credentials_received', + NETWORK_ERROR = 'network_error', + UNKNOWN_ERROR = 'unknown_error' +} + +// Error messages +export const AUTH_ERROR_MESSAGES: Record = { + [AuthErrorType.SIGN_IN_CANCELLED]: 'Sign in was cancelled', + [AuthErrorType.SIGN_IN_FAILED]: 'Authentication failed. Please try again.', + [AuthErrorType.NO_CREDENTIALS]: 'No authentication credentials received from Google.', + [AuthErrorType.NETWORK_ERROR]: 'Network error. Please check your connection and try again.', + [AuthErrorType.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.' +}; + +// Authentication provider configuration +export interface AuthProviderConfig { + webClientId?: string; + androidClientId?: string; + iosClientId?: string; + scopes?: string[]; + responseType?: 'token' | 'id_token'; + redirectUri?: string; +} + +// Session data interface +export interface SessionData { + user: UserInfo; + lastLoginAt: number; + expiresAt?: number; +} \ No newline at end of file diff --git a/models/types.ts b/models/types.ts index 8164ab6..f5c52fd 100644 --- a/models/types.ts +++ b/models/types.ts @@ -1,5 +1,5 @@ // models/types.ts -export type CollectionType = 'goals' | 'goodDeeds' | 'reflections'; +export type CollectionType = 'goals' | 'goodDeeds' | 'reflections' | 'habits'; export interface BaseItem { id: string; @@ -23,4 +23,41 @@ export interface GoodDeed extends BaseItem { export interface Reflection extends BaseItem { text: string; timestamp: Date; +} + +export interface Habit extends BaseItem { + name: string; + description?: string; + status: 'completed' | 'failed' | 'untracked'; + streak: number; + lastCompleted: Date | null; + reminderTime?: string; + reminderDays?: { + monday: boolean; + tuesday: boolean; + wednesday: boolean; + thursday: boolean; + friday: boolean; + saturday: boolean; + sunday: boolean; + }; + longestStreak: number; + // New field to track completion status for each date + completionHistory?: Record; +} + +export const PUSH_TOKEN_KEY = '@push_token'; +export const NOTIFICATION_IDS_KEY = '@notification_ids'; + +// Add this interface for the notification context if you don't already have it +// Do not modify your existing Habit interface, as it already has reminderTime and reminderDays +export interface NotificationContextType { + scheduleDailyReminder: (habit: Habit) => Promise; + cancelReminder: (habitId: string) => Promise; + rescheduleAllReminders: (habits: Habit[]) => Promise; +} + +// Add this interface for the notification provider props +export interface NotificationProviderProps { + children: React.ReactNode; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f03cff0..783bc64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,19 @@ "dependencies": { "@expo/vector-icons": "^14.0.2", "@react-native-async-storage/async-storage": "^1.23.1", + "@react-native-community/datetimepicker": "8.2.0", "date-fns": "^4.1.0", "expo": "~52.0.33", "expo-auth-session": "~6.0.3", "expo-constants": "~17.0.5", "expo-crypto": "~14.0.2", "expo-dev-client": "~5.0.12", + "expo-device": "~7.0.2", "expo-font": "~13.0.3", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.5", + "expo-navigation-bar": "~4.0.8", + "expo-notifications": "~0.29.13", "expo-random": "^14.0.1", "expo-router": "~4.0.17", "expo-splash-screen": "~0.29.21", @@ -3546,6 +3550,12 @@ "node": ">=6" } }, + "node_modules/@ide/backoff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4272,6 +4282,29 @@ "react-native": "^0.0.0-0 || >=0.60 <1.0" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.2.0.tgz", + "integrity": "sha512-qrUPhiBvKGuG9Y+vOqsc56RPFcHa1SU2qbAMT0hfGkoFIj3FodE0VuPVrEa8fgy7kcD5NQmkZIKgHOBLV0+hWg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": ">=50.0.0", + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native/assets-registry": { "version": "0.76.7", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz", @@ -5630,6 +5663,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/ast-types": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", @@ -5904,6 +5950,12 @@ "@babel/core": "^7.0.0" } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6994,6 +7046,23 @@ "node": ">=8" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", @@ -7765,6 +7834,44 @@ "expo": "*" } }, + "node_modules/expo-device": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.2.tgz", + "integrity": "sha512-0PkTixE4Qi8VQBjixnj4aw2f6vE4tUZH7GK8zHROGKlBypZKcWmsA+W/Vp3RC5AyREjX71pO/hjKTSo/vF0E2w==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/expo-file-system": { "version": "18.0.11", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.11.tgz", @@ -7909,6 +8016,41 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-navigation-bar": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/expo-navigation-bar/-/expo-navigation-bar-4.0.8.tgz", + "integrity": "sha512-rmQkCCwfYeR29GTPwoNuN7eWE8bYCW5UGJ/5CnZQxIaiBmOeepoDXu50AzEM5ZFPMK6J12nvyu1yj0ujbLUpsQ==", + "license": "MIT", + "dependencies": { + "@react-native/normalize-colors": "0.76.7", + "debug": "^4.3.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-notifications": { + "version": "0.29.13", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.29.13.tgz", + "integrity": "sha512-GHye6XeI1uEeVttJO/hGwUyA5cgQsxR3mi5q37yOE7cZN3cMj36pIfEEmjXEr0nWIWSzoJ0w8c2QxNj5xfP1pA==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.6.4", + "@ide/backoff": "^1.0.0", + "abort-controller": "^3.0.0", + "assert": "^2.0.0", + "badgin": "^1.1.5", + "expo-application": "~6.0.2", + "expo-constants": "~17.0.5" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-random": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/expo-random/-/expo-random-14.0.1.tgz", @@ -9216,6 +9358,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12015,6 +12173,51 @@ "node": ">=0.10.0" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 17f0ee4..e6b9bf9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@expo/vector-icons": "^14.0.2", "@react-native-async-storage/async-storage": "^1.23.1", + "@react-native-community/datetimepicker": "8.2.0", "date-fns": "^4.1.0", "expo": "~52.0.33", "expo-auth-session": "~6.0.3", @@ -39,7 +40,10 @@ "react-native-gesture-handler": "~2.20.2", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", - "react-native-screens": "~4.4.0" + "react-native-screens": "~4.4.0", + "expo-notifications": "~0.29.13", + "expo-device": "~7.0.2", + "expo-navigation-bar": "~4.0.8" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/services/firebaseService.ts b/services/firebaseService.ts index 93841cd..d6d7b95 100644 --- a/services/firebaseService.ts +++ b/services/firebaseService.ts @@ -3,6 +3,7 @@ import { db } from '../config/firebase'; import { collection, doc, + getDoc, getDocs, addDoc, updateDoc, @@ -51,6 +52,22 @@ export class FirebaseService { await deleteDoc(docRef); } + // Get document by ID + async getDocById(itemId: string): Promise { + const docRef = doc(this.collectionRef, itemId); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + return { + id: docSnap.id, + ...this.convertTimestamps(docSnap.data()) + } as T; + } + + return null; + } + + // Get items for a specific date async getItemsByDate(userId: string, date: Date, additionalQueries: QueryConstraint[] = []): Promise { // Create Firestore timestamps for start and end of the selected day diff --git a/services/habitService.ts b/services/habitService.ts new file mode 100644 index 0000000..78f89c7 --- /dev/null +++ b/services/habitService.ts @@ -0,0 +1,255 @@ +// Enhanced HabitService.ts with notification support + +import { FirebaseService } from './firebaseService'; +import { Habit } from '../models/types'; +import { + collection, + query, + where, + Timestamp, + orderBy, + QueryConstraint +} from 'firebase/firestore'; +import { startOfDay, endOfDay, isSameDay, format, differenceInDays, addDays } from 'date-fns'; +import { NotificationService } from './notificationService'; + +export class HabitService extends FirebaseService { + constructor() { + super('habits'); + } + + // Add a new habit with notification support + async addHabit(userId: string, habitData: Partial): Promise { + const defaultReminderDays = { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: true, + sunday: true + }; + + // Create a new object with only the fields we want to save + const { description, ...restData } = habitData; + + // Get current date + const now = new Date(); + + // Basic data without description + const cleanedData: Partial = { + ...restData, + streak: 0, + status: 'untracked' as 'untracked' | 'completed' | 'failed', + lastCompleted: null, + longestStreak: 0, + reminderDays: habitData.reminderDays || defaultReminderDays, + completionHistory: {}, // Add completion history object + date: now, // Add date from BaseItem + createdAt: now, + updatedAt: now + }; + + // Only add description if it has a value + if (description && description.trim().length > 0) { + cleanedData.description = description; + } + + // Add the habit to Firestore + const habitId = await this.addItem(userId, cleanedData); + + return habitId; + } + + // Update habit with notification handling + async updateHabit(habitId: string, updates: Partial): Promise { + // Handle notifications if the reminder time changes + if ('reminderTime' in updates || 'reminderDays' in updates) { + // Get the current habit + const currentHabit = await this.getItemById(habitId); + + // Cancel existing notifications + await NotificationService.cancelHabitNotifications(habitId); + + // Schedule new notifications if there's a reminder time + if (updates.reminderTime && currentHabit) { + const updatedHabit = { ...currentHabit, ...updates }; + await NotificationService.scheduleHabitNotification(updatedHabit as Habit); + } + } + + // Update the habit in Firestore + await this.updateItem(habitId, updates); + } + + // Delete habit with notification cleanup + async deleteHabit(habitId: string): Promise { + // Cancel any scheduled notifications + await NotificationService.cancelHabitNotifications(habitId); + + // Delete the habit from Firestore + await this.deleteItem(habitId); + } + + // Update habit status for a specific date + async updateHabitStatusForDate( + habitId: string, + status: 'completed' | 'failed' | 'untracked', + date: Date, + habit?: Habit + ): Promise { + if (!habit) { + // Fetch habit if not provided and call this method again + const habitDoc = await this.getItemById(habitId); + if (habitDoc) { + await this.updateHabitStatusForDate(habitId, status, date, habitDoc); + } + return; + } + + // Initialize completionHistory if it doesn't exist + const completionHistory = habit.completionHistory || {}; + + // Format date as YYYY-MM-DD for storage + const dateStr = format(date, 'yyyy-MM-dd'); + + // Update completion history + completionHistory[dateStr] = status; + + // Basic updates + let updates: Partial = { + completionHistory, + status: status // Current day status also gets updated + }; + + // If marking as completed + if (status === 'completed') { + const now = new Date(); + + // Update lastCompleted if tracking today + if (isSameDay(date, now)) { + updates.lastCompleted = now; + } + + // Recalculate streak based on the updated completion history + updates.streak = this.calculateStreak(completionHistory); + + // Update longest streak if current streak exceeds it + if (updates.streak > habit.longestStreak) { + updates.longestStreak = updates.streak; + } + } + // If marking as failed and it's today, reset streak + else if (status === 'failed' && isSameDay(date, new Date())) { + updates.streak = 0; + } + // If changing a past date, we need to recalculate the streak + else { + updates.streak = this.calculateStreak(completionHistory); + + // Update longest streak if current streak exceeds it + if (updates.streak > habit.longestStreak) { + updates.longestStreak = updates.streak; + } + } + + await this.updateItem(habitId, updates); + } + + // Calculate streak based on completion history + private calculateStreak(completionHistory: Record): number { + if (!completionHistory) return 0; + + // Get today as date string + const todayStr = format(new Date(), 'yyyy-MM-dd'); + + // Get all dates from completion history + const dates = Object.keys(completionHistory) + .filter(dateStr => completionHistory[dateStr] === 'completed') + .sort((a, b) => (a < b ? 1 : -1)); // Sort in descending order (newest first) + + if (dates.length === 0) return 0; + + // Check if today is completed + const isTodayCompleted = completionHistory[todayStr] === 'completed'; + + // If today isn't tracked or explicitly failed, start counting from yesterday + const startDate = isTodayCompleted ? todayStr : format(addDays(new Date(), -1), 'yyyy-MM-dd'); + const startDateIndex = dates.indexOf(startDate); + + // If the start date isn't in our completed dates, there's no streak + if (startDateIndex === -1) return 0; + + let streak = 1; // Start with 1 for the start date + let currentDate = new Date(startDate); + + // Loop backwards through dates to count consecutive completed days + for (let i = 1; i <= 366; i++) { // Check up to a year + const prevDate = addDays(currentDate, -1); + const prevDateStr = format(prevDate, 'yyyy-MM-dd'); + + if (completionHistory[prevDateStr] === 'completed') { + streak++; + currentDate = prevDate; + } else { + break; // Break on first non-completed day + } + } + + return streak; + } + + // Get habits sorted by name + async getUserHabits(userId: string): Promise { + const additionalQueries: QueryConstraint[] = [orderBy('name')]; + return this.getUserItems(userId, additionalQueries); + } + + // Get active habits for today + async getActiveHabitsForToday(userId: string): Promise { + const habits = await this.getUserHabits(userId); + const today = new Date(); + const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, ... + + // Map day number to day name + const dayMap: {[key: number]: string} = { + 0: 'sunday', + 1: 'monday', + 2: 'tuesday', + 3: 'wednesday', + 4: 'thursday', + 5: 'friday', + 6: 'saturday' + }; + + const todayName = dayMap[dayOfWeek]; + + // Filter habits active for today based on reminderDays + return habits.filter(habit => + habit.reminderDays ? habit.reminderDays[todayName as keyof typeof habit.reminderDays] : true + ); + } + + // Original update habit status method (kept for backward compatibility) + async updateHabitStatus( + habitId: string, + status: 'completed' | 'failed' | 'untracked', + habit?: Habit + ): Promise { + // Use the new method with today's date + await this.updateHabitStatusForDate(habitId, status, new Date(), habit); + } + + // Helper method to get a habit by ID + async getItemById(habitId: string): Promise { + try { + const habit = await this.getDocById(habitId); + return habit as Habit; + } catch (error) { + console.error("Error getting habit by ID:", error); + return null; + } + } +} + +export const habitService = new HabitService(); \ No newline at end of file diff --git a/services/notificationService.ts b/services/notificationService.ts new file mode 100644 index 0000000..f9a3f92 --- /dev/null +++ b/services/notificationService.ts @@ -0,0 +1,238 @@ +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import Constants from 'expo-constants'; +import { Platform } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { format, parse, addDays, setHours, setMinutes, setSeconds } from 'date-fns'; +import { Habit, PUSH_TOKEN_KEY, NOTIFICATION_IDS_KEY } from '../models/types'; + +// Configure notification behavior +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + }), +}); + +export class NotificationService { + // Register for push notifications + static async registerForPushNotificationsAsync() { + let token; + + // Check if the device is a physical device (not an emulator) + if (Device.isDevice) { + // Check if we already have permission + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + + // If we don't have permission, ask for it + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + + // If we still don't have permission, return null + if (finalStatus !== 'granted') { + console.log('Failed to get push token for push notification!'); + return null; + } + + // Get the token if we have permission + token = await Notifications.getExpoPushTokenAsync({ + projectId: Constants.expoConfig?.extra?.eas?.projectId, + }); + + // Save the token to AsyncStorage + if (token) { + await AsyncStorage.setItem(PUSH_TOKEN_KEY, token.data); + } + } else { + console.log('Must use physical device for push notifications'); + } + + // Set up special notification channel for Android + if (Platform.OS === 'android') { + Notifications.setNotificationChannelAsync('habit-reminders', { + name: 'Habit Reminders', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } + + return token; + } + + // Schedule a notification for a habit + static async scheduleHabitNotification(habit: Habit) { + // Only schedule if the habit has a reminder time + if (!habit.reminderTime) { + console.log('No reminder time set for habit:', habit.name); + return null; + } + + try { + // Parse the time (format is "hh:mm a", e.g. "08:30 AM") + const timeString = habit.reminderTime; + const parsedTime = parse(timeString, 'hh:mm a', new Date()); + + const hours = parsedTime.getHours(); + const minutes = parsedTime.getMinutes(); + + // Calculate upcoming trigger dates for each selected day + const now = new Date(); + const notificationIds = []; + const daysToSchedule = []; + + // Determine which days to schedule based on reminderDays + if (!habit.reminderDays || + (habit.reminderDays.monday && habit.reminderDays.tuesday && + habit.reminderDays.wednesday && habit.reminderDays.thursday && + habit.reminderDays.friday && habit.reminderDays.saturday && + habit.reminderDays.sunday)) { + // If all days are selected or no specific days, schedule daily for the next 7 days + for (let i = 0; i < 7; i++) { + daysToSchedule.push(i); + } + } else { + // Otherwise, schedule only for selected days + if (habit.reminderDays?.monday) daysToSchedule.push(1); // Monday + if (habit.reminderDays?.tuesday) daysToSchedule.push(2); // Tuesday + if (habit.reminderDays?.wednesday) daysToSchedule.push(3); // Wednesday + if (habit.reminderDays?.thursday) daysToSchedule.push(4); // Thursday + if (habit.reminderDays?.friday) daysToSchedule.push(5); // Friday + if (habit.reminderDays?.saturday) daysToSchedule.push(6); // Saturday + if (habit.reminderDays?.sunday) daysToSchedule.push(0); // Sunday + } + + // Schedule notifications for each day + for (const dayOfWeek of daysToSchedule) { + // Find the next occurrence of this day of the week + let nextDate = new Date(now); + + // Keep advancing the date until we hit the right day of the week + while (nextDate.getDay() !== dayOfWeek) { + nextDate = addDays(nextDate, 1); + } + + // Set the time to the habit reminder time + nextDate = setHours(nextDate, hours); + nextDate = setMinutes(nextDate, minutes); + nextDate = setSeconds(nextDate, 0); + + // If the time has already passed today, move to next week + if (nextDate < now) { + nextDate = addDays(nextDate, 7); + } + + // Calculate seconds until the notification should trigger + const secondsUntilTrigger = Math.floor((nextDate.getTime() - now.getTime()) / 1000); + + // Schedule the notification using seconds as trigger with the correct type + const notificationId = await Notifications.scheduleNotificationAsync({ + content: { + title: 'Habit Reminder', + body: `Time to complete your habit: ${habit.name}`, + data: { habitId: habit.id }, + }, + trigger: { + type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, + seconds: secondsUntilTrigger, + }, + }); + + notificationIds.push(notificationId); + } + + // Store all notification IDs with the habit ID + await this.saveNotificationId(habit.id, notificationIds); + + return notificationIds; + } catch (error) { + console.error('Error scheduling notification:', error); + return null; + } + } + + // Cancel all notifications for a habit + static async cancelHabitNotifications(habitId: string) { + try { + // Get all notification IDs for this habit + const notificationIdsJSON = await AsyncStorage.getItem(NOTIFICATION_IDS_KEY); + if (!notificationIdsJSON) return; + + const allNotificationIds = JSON.parse(notificationIdsJSON); + const notificationIds = allNotificationIds[habitId]; + + if (!notificationIds) return; + + // Cancel each notification + if (Array.isArray(notificationIds)) { + for (const id of notificationIds) { + await Notifications.cancelScheduledNotificationAsync(id); + } + } else { + await Notifications.cancelScheduledNotificationAsync(notificationIds); + } + + // Remove the notification IDs from storage + delete allNotificationIds[habitId]; + await AsyncStorage.setItem(NOTIFICATION_IDS_KEY, JSON.stringify(allNotificationIds)); + } catch (error) { + console.error('Error canceling notifications:', error); + } + } + + // Save notification ID(s) for a habit + static async saveNotificationId(habitId: string, notificationId: string | string[]) { + try { + // Get all existing notification IDs + const notificationIdsJSON = await AsyncStorage.getItem(NOTIFICATION_IDS_KEY); + const allNotificationIds = notificationIdsJSON ? JSON.parse(notificationIdsJSON) : {}; + + // Save the new notification ID(s) + allNotificationIds[habitId] = notificationId; + + // Save back to AsyncStorage + await AsyncStorage.setItem(NOTIFICATION_IDS_KEY, JSON.stringify(allNotificationIds)); + } catch (error) { + console.error('Error saving notification ID:', error); + } + } + + // Reschedule notifications for all habits + static async rescheduleAllHabitNotifications(habits: Habit[]) { + try { + // Cancel all existing notifications first + await Notifications.cancelAllScheduledNotificationsAsync(); + + // Clear notification IDs storage + await AsyncStorage.setItem(NOTIFICATION_IDS_KEY, JSON.stringify({})); + + // Schedule new notifications for each habit + for (const habit of habits) { + if (habit.reminderTime) { + await this.scheduleHabitNotification(habit); + } + } + } catch (error) { + console.error('Error rescheduling all notifications:', error); + } + } + + // Set up notification response handling + static setNotificationResponseHandler(callback: (habitId: string) => void) { + // This handles when a user taps on a notification + const subscription = Notifications.addNotificationResponseReceivedListener(response => { + const habitId = response.notification.request.content.data?.habitId; + if (habitId) { + callback(habitId); + } + }); + + return subscription; + } +} + +export default NotificationService; \ No newline at end of file