From 9d6efdd43bb0fbb71c78d9eb9f00912b21ed1829 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Fri, 15 May 2026 18:26:51 +0200 Subject: [PATCH] Configure Expo mobile project and update Android/iOS bundles Initialize iOS and Android native projects for the Time to Leave app. Rename Android package to com.floegger.timetoleave and add iOS project files. Add FontAwesome icons, calendar types, and dependencies. --- .expo/README.md | 13 + .expo/devices.json | 3 + android/app/build.gradle | 6 +- android/app/src/main/AndroidManifest.xml | 28 +- .../timetoleave/MainActivity.kt | 2 +- .../timetoleave/MainApplication.kt | 2 +- .../app/src/main/res/values-night/colors.xml | 1 + android/app/src/main/res/values/colors.xml | 3 +- android/app/src/main/res/values/strings.xml | 4 +- android/app/src/main/res/values/styles.xml | 9 +- android/gradle.properties | 4 + android/settings.gradle | 2 +- apps/mobile/package.json | 5 +- apps/mobile/src/navigation/AppNavigator.tsx | 100 +++- .../src/screens/CalendarImportScreen.tsx | 325 ++++++++++++- apps/mobile/src/services/calendar.ts | 157 ++++++- apps/mobile/src/store/eventStore.ts | 37 +- apps/web/public/bars-solid.svg | 1 + ios/.gitignore | 30 ++ ios/.xcode.env | 11 + ios/Podfile | 63 +++ ios/Podfile.properties.json | 4 + ios/timetoleave.xcodeproj/project.pbxproj | 436 ++++++++++++++++++ .../xcschemes/timetoleave.xcscheme | 88 ++++ ios/timetoleave/AppDelegate.swift | 70 +++ .../App-Icon-1024x1024@1x.png | Bin 0 -> 5856 bytes .../AppIcon.appiconset/Contents.json | 14 + ios/timetoleave/Images.xcassets/Contents.json | 6 + .../Contents.json | 20 + ios/timetoleave/Info.plist | 79 ++++ ios/timetoleave/SplashScreen.storyboard | 39 ++ ios/timetoleave/Supporting/Expo.plist | 12 + ios/timetoleave/timetoleave-Bridging-Header.h | 3 + ios/timetoleave/timetoleave.entitlements | 5 + package-lock.json | 225 ++++++++- package.json | 11 +- packages/core/src/types.ts | 32 ++ tsconfig.json | 23 +- 38 files changed, 1815 insertions(+), 58 deletions(-) create mode 100644 .expo/README.md create mode 100644 .expo/devices.json rename android/app/src/main/java/com/{ => floegger}/timetoleave/MainActivity.kt (98%) rename android/app/src/main/java/com/{ => floegger}/timetoleave/MainApplication.kt (98%) create mode 100644 android/app/src/main/res/values-night/colors.xml create mode 100644 apps/web/public/bars-solid.svg create mode 100644 ios/.gitignore create mode 100644 ios/.xcode.env create mode 100644 ios/Podfile create mode 100644 ios/Podfile.properties.json create mode 100644 ios/timetoleave.xcodeproj/project.pbxproj create mode 100644 ios/timetoleave.xcodeproj/xcshareddata/xcschemes/timetoleave.xcscheme create mode 100644 ios/timetoleave/AppDelegate.swift create mode 100644 ios/timetoleave/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png create mode 100644 ios/timetoleave/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/timetoleave/Images.xcassets/Contents.json create mode 100644 ios/timetoleave/Images.xcassets/SplashScreenBackground.colorset/Contents.json create mode 100644 ios/timetoleave/Info.plist create mode 100644 ios/timetoleave/SplashScreen.storyboard create mode 100644 ios/timetoleave/Supporting/Expo.plist create mode 100644 ios/timetoleave/timetoleave-Bridging-Header.h create mode 100644 ios/timetoleave/timetoleave.entitlements diff --git a/.expo/README.md b/.expo/README.md new file mode 100644 index 0000000..ce8c4b6 --- /dev/null +++ b/.expo/README.md @@ -0,0 +1,13 @@ +> Why do I have a folder named ".expo" in my project? + +The ".expo" folder is created when an Expo project is started using "expo start" command. + +> What do the files contain? + +- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. +- "settings.json": contains the server configuration that is used to serve the application manifest. + +> Should I commit the ".expo" folder? + +No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. +Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/.expo/devices.json b/.expo/devices.json new file mode 100644 index 0000000..5efff6c --- /dev/null +++ b/.expo/devices.json @@ -0,0 +1,3 @@ +{ + "devices": [] +} diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ee439c..c252e6f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -87,13 +87,13 @@ android { buildToolsVersion rootProject.ext.buildToolsVersion compileSdk rootProject.ext.compileSdkVersion - namespace "com.timetoleave" + namespace 'com.floegger.timetoleave' defaultConfig { - applicationId "com.timetoleave" + applicationId 'com.floegger.timetoleave' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "1.0" + versionName "1.0.0" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 150248b..93c892f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,29 +1,31 @@ - - + - - - - - - - - + + + - - + + + + + + + + + + - + \ No newline at end of file diff --git a/android/app/src/main/java/com/timetoleave/MainActivity.kt b/android/app/src/main/java/com/floegger/timetoleave/MainActivity.kt similarity index 98% rename from android/app/src/main/java/com/timetoleave/MainActivity.kt rename to android/app/src/main/java/com/floegger/timetoleave/MainActivity.kt index 1c2429c..ef7a483 100644 --- a/android/app/src/main/java/com/timetoleave/MainActivity.kt +++ b/android/app/src/main/java/com/floegger/timetoleave/MainActivity.kt @@ -1,4 +1,4 @@ -package com.timetoleave +package com.floegger.timetoleave import android.os.Build import android.os.Bundle diff --git a/android/app/src/main/java/com/timetoleave/MainApplication.kt b/android/app/src/main/java/com/floegger/timetoleave/MainApplication.kt similarity index 98% rename from android/app/src/main/java/com/timetoleave/MainApplication.kt rename to android/app/src/main/java/com/floegger/timetoleave/MainApplication.kt index 26f176e..c96fe71 100644 --- a/android/app/src/main/java/com/timetoleave/MainApplication.kt +++ b/android/app/src/main/java/com/floegger/timetoleave/MainApplication.kt @@ -1,4 +1,4 @@ -package com.timetoleave +package com.floegger.timetoleave import android.app.Application import android.content.res.Configuration diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 21cc155..a890727 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,5 @@ - #FFFFFF + #023c69 + #ffffff \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0df736a..5f9b506 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - time-to-leave - + time-to-leave + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 26f3404..cb4ceb7 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,8 +1,11 @@ - + - + \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 97271b9..8e39f82 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -59,3 +59,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true # Use legacy packaging to compress native libraries in the resulting APK. expo.useLegacyPackaging=false + +# Specifies whether the app is configured to use edge-to-edge via the app config or plugin +# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge. +expo.edgeToEdgeEnabled=true diff --git a/android/settings.gradle b/android/settings.gradle index a1e3f52..4190cae 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -31,7 +31,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> } expoAutolinking.useExpoModules() -rootProject.name = 'timetoleave' +rootProject.name = 'time-to-leave' expoAutolinking.useExpoVersionCatalog() diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 49a52c9..4a56225 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -13,6 +13,8 @@ "test": "jest" }, "dependencies": { + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-native-fontawesome": "^1.0.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.2.4", "@react-navigation/native-stack": "^7.14.14", @@ -27,7 +29,8 @@ "react": "19.1.0", "react-native": "0.81.5", "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "~4.16.0" + "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/apps/mobile/src/navigation/AppNavigator.tsx b/apps/mobile/src/navigation/AppNavigator.tsx index 2f8b5ab..0645653 100644 --- a/apps/mobile/src/navigation/AppNavigator.tsx +++ b/apps/mobile/src/navigation/AppNavigator.tsx @@ -1,8 +1,11 @@ import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { Image, Text, View } from 'react-native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faBars } from '@fortawesome/free-solid-svg-icons/faBars'; +import { Image, Modal, Pressable, StyleSheet, Text, View } from 'react-native'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { StatusBar } from 'expo-status-bar'; +import { useState } from 'react'; import { EventListScreen } from '../screens/EventListScreen'; import { EventDetailScreen } from '../screens/EventDetailScreen'; import { AddEventScreen } from '../screens/AddEventScreen'; @@ -14,6 +17,57 @@ import navLogo from '../../assets/nav-logo.png'; // ── Root Stack ── const Root = createNativeStackNavigator(); +const byPrefixAndName = { fas: { bars: faBars } }; + +type HeaderMenuProps = { + navigation: { + navigate: (screen: 'CalendarImport' | 'Settings') => void; + }; +}; + +function HeaderMenu({ navigation }: HeaderMenuProps) { + const [isOpen, setIsOpen] = useState(false); + + const navigateTo = (screen: 'CalendarImport' | 'Settings') => { + setIsOpen(false); + navigation.navigate(screen); + }; + + return ( + <> + setIsOpen(true)} + style={({ pressed }) => [styles.menuButton, pressed && styles.menuButtonPressed]} + > + + + + setIsOpen(false)}> + setIsOpen(false)}> + + navigateTo('CalendarImport')} + style={({ pressed }) => [styles.menuItem, pressed && styles.menuItemPressed]} + > + Calendar + + navigateTo('Settings')} + style={({ pressed }) => [styles.menuItem, pressed && styles.menuItemPressed]} + > + Settings + + + + + + ); +} /** * Root native stack navigator for the app. @@ -27,16 +81,17 @@ export default function AppNavigator() { ({ headerStyle: { backgroundColor: '#17112A' }, headerTintColor: '#F4F1EA', + headerRight: () => , headerTitle: () => ( Time To Leave ), - }} + })} > @@ -49,3 +104,42 @@ export default function AppNavigator() { ); } + +const styles = StyleSheet.create({ + menuButton: { + alignItems: 'center', + height: 40, + justifyContent: 'center', + width: 40, + }, + menuButtonPressed: { + opacity: 0.65, + }, + menuOverlay: { + alignItems: 'flex-end', + backgroundColor: 'rgba(9, 8, 22, 0.45)', + flex: 1, + paddingRight: 12, + paddingTop: 72, + }, + menuPanel: { + backgroundColor: '#17112A', + borderColor: '#3B3157', + borderRadius: 8, + borderWidth: 1, + minWidth: 168, + overflow: 'hidden', + }, + menuItem: { + paddingHorizontal: 18, + paddingVertical: 14, + }, + menuItemPressed: { + backgroundColor: '#2A2140', + }, + menuItemText: { + color: '#F4F1EA', + fontSize: 16, + fontWeight: '600', + }, +}); diff --git a/apps/mobile/src/screens/CalendarImportScreen.tsx b/apps/mobile/src/screens/CalendarImportScreen.tsx index ef09058..d0e4b1c 100644 --- a/apps/mobile/src/screens/CalendarImportScreen.tsx +++ b/apps/mobile/src/screens/CalendarImportScreen.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, ScrollView, @@ -11,9 +11,9 @@ import { import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import { api } from '../services/api'; -import { fetchNativeEvents } from '../services/calendar'; -import { addEvent, loadEvents } from '../store/eventStore'; -import type { Event as CalendarEvent } from '@timetoleave/core'; +import { fetchNativeEvents, getSelectableCalendars, groupCalendarsByType } from '../services/calendar'; +import { addEvent, getSelectedCalendarIds, hasCalendarSelection, loadEvents, saveSelectedCalendarIds } from '../store/eventStore'; +import type { Event as CalendarEvent, CalendarAccountType, SelectableCalendar } from '@timetoleave/core'; import type { RootStack } from '../types/navigation'; import { useColors } from '../hooks/useColors'; @@ -22,9 +22,81 @@ type ScreenProps = { route: RouteProp; }; +/** + * Display order for calendar source type groups. + */ +const GROUP_ORDER: CalendarAccountType[] = [ + 'caldav', + 'mobileme', + 'google', + 'exchange', + 'subscriptions', + 'local', + 'carddav', + 'activesync', + 'other', + 'none', +]; + +/** Badge color for each account type. */ +const BADGE_COLORS: Record = { + caldav: '#34C759', + mobileme: '#FF9500', + google: '#4285F4', + exchange: '#0078D4', + subscriptions: '#AF52DE', + local: '#8E8E93', + carddav: '#5AC8FA', + activesync: '#FF2D55', + other: '#8E8E93', + none: '#8E8E93', +}; + +/** Calendar checkbox row component. */ +function CalendarCheckbox({ + calendar, + selected, + onToggle, + colors, +}: { + calendar: SelectableCalendar; + selected: boolean; + onToggle: (id: string) => void; + colors: ReturnType; +}) { + const badgeColor = BADGE_COLORS[calendar.accountType] ?? BADGE_COLORS.other; + + return ( + onToggle(calendar.id)} + accessibilityRole="button" + accessibilityLabel={`${calendar.name} – ${calendar.sourceInfo.label}`} + > + + {selected && } + + + + {calendar.sourceInfo.emoji} {calendar.name} + + + + {calendar.sourceInfo.badge} + + + {calendar.sourceInfo.label} + + + + + ); +} + /** * Screen for importing events from either an ICS calendar URL or the device's - * native calendar. Deduplicates against already-imported events by ID. + * native calendar. Includes calendar selection so the user can choose which + * calendars to sync. Deduplicates against already-imported events by ID. */ export function CalendarImportScreen({ navigation }: ScreenProps) { const colors = useColors(); @@ -33,6 +105,57 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { const [error, setError] = useState(null); const [count, setCount] = useState(null); + // Calendar selection state + const [availableCalendars, setAvailableCalendars] = useState([]); + const [selectedCalendarIds, setSelectedCalendarIds] = useState>(new Set()); + const [calendarsLoaded, setCalendarsLoaded] = useState(false); + const [hasSelection, setHasSelection] = useState(false); + const initialLoadRef = useRef(false); + + // Load available calendars and persisted selection on mount + useEffect(() => { + if (initialLoadRef.current) return; + initialLoadRef.current = true; + + (async () => { + const [calendars, persistedIds, hasSel] = await Promise.all([ + getSelectableCalendars(), + getSelectedCalendarIds(), + hasCalendarSelection(), + ]); + setAvailableCalendars(calendars); + setSelectedCalendarIds(new Set(persistedIds)); + setHasSelection(hasSel); + setCalendarsLoaded(true); + })(); + }, []); + + const toggleCalendar = useCallback((id: string) => { + setSelectedCalendarIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }, []); + + const selectAll = useCallback(() => { + setSelectedCalendarIds(new Set(availableCalendars.map((c) => c.id))); + }, [availableCalendars]); + + const deselectAll = useCallback(() => { + setSelectedCalendarIds(new Set()); + }, []); + + const saveSelection = useCallback(async () => { + const ids = Array.from(selectedCalendarIds); + await saveSelectedCalendarIds(ids); + setHasSelection(ids.length > 0); + }, [selectedCalendarIds]); + const handleImport = async () => { if (!url.trim()) { setError('Bitte ICS-URL eingeben'); @@ -77,11 +200,19 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { setCount(null); try { + // Save the current selection before syncing + await saveSelection(); + // Fetch events from the next 30 days const now = new Date(); const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); - const nativeEvents = await fetchNativeEvents(now, thirtyDaysLater); + // If a selection was made, sync only selected calendars + const calendarIds = hasSelection + ? Array.from(selectedCalendarIds) + : undefined; + + const nativeEvents = await fetchNativeEvents(now, thirtyDaysLater, calendarIds); // Load existing events to avoid duplicates const existing = await loadEvents(); @@ -103,6 +234,9 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { } }; + // Group calendars by account type for display + const grouped = groupCalendarsByType(availableCalendars); + return ( @@ -111,6 +245,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender. + {/* ── ICS URL Import ── */} ICS-URL Import @@ -137,6 +272,75 @@ export function CalendarImportScreen({ navigation }: ScreenProps) { + {/* ── Calendar Selection ── */} + + + Kalender auswählen + + + Alle + + · + + Keine + + + + + + Wähle die Kalender aus, die gesynct werden sollen. Ohne Auswahl werden alle Kalender verwendet. + {'\n'} + CalDAV-Quellen (DAVx5, Apple Calendar, etc.) werden automatisch erkannt. + + + {!calendarsLoaded ? ( + + + Kalender werden geladen… + + ) : availableCalendars.length === 0 ? ( + + + Keine Kalender auf diesem Gerät gefunden. + + + ) : ( + <> + {/* Render groups in defined order, then any remaining types */} + {GROUP_ORDER.map((type) => { + const group = grouped.get(type); + if (!group || group.length === 0) return null; + + return ( + + + {group[0].sourceInfo.emoji} {group[0].sourceInfo.label} ({group.length}) + + {group.map((cal) => ( + + ))} + + ); + })} + + {hasSelection && ( + + + {selectedCalendarIds.size} von {availableCalendars.length} Kalendern ausgewählt + + + )} + + )} + + + {/* ── Device Calendar Sync ── */} Geräte-Kalender Sync @@ -183,6 +387,12 @@ const styles = StyleSheet.create({ heading: { fontSize: 22, fontWeight: '700', marginBottom: 4 }, description: { fontSize: 14, marginBottom: 20, lineHeight: 20 }, section: { marginBottom: 24 }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, sectionTitle: { fontSize: 16, fontWeight: '600', marginBottom: 8 }, sectionDesc: { fontSize: 13, marginBottom: 12, lineHeight: 18 }, input: { @@ -210,4 +420,107 @@ const styles = StyleSheet.create({ alignItems: 'center', }, backBtnText: { fontSize: 15 }, + // Calendar selection + selectionActions: { + flexDirection: 'row', + alignItems: 'center', + }, + linkText: { + fontSize: 13, + fontWeight: '600', + }, + dividerText: { + fontSize: 13, + marginHorizontal: 4, + }, + group: { + marginBottom: 12, + }, + groupLabel: { + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 0.5, + marginBottom: 6, + marginTop: 4, + }, + calendarRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 10, + borderWidth: 1.5, + marginBottom: 6, + }, + checkBox: { + width: 24, + height: 24, + borderRadius: 6, + borderWidth: 2, + borderColor: '#8E8E93', + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + checkBoxSelected: { + backgroundColor: '#8B5CF6', + borderColor: '#8B5CF6', + }, + checkMark: { + color: '#fff', + fontSize: 14, + fontWeight: '700', + }, + calendarInfo: { + flex: 1, + }, + calendarName: { + fontSize: 15, + fontWeight: '500', + marginBottom: 2, + }, + badgeRow: { + flexDirection: 'row', + alignItems: 'center', + }, + badge: { + paddingHorizontal: 6, + paddingVertical: 1, + borderRadius: 4, + marginRight: 6, + }, + badgeText: { + color: '#fff', + fontSize: 10, + fontWeight: '700', + }, + accountTypeText: { + fontSize: 11, + }, + loadingContainer: { + padding: 20, + alignItems: 'center', + }, + loadingText: { + marginTop: 8, + fontSize: 13, + }, + emptyContainer: { + padding: 20, + alignItems: 'center', + }, + emptyText: { + fontSize: 13, + textAlign: 'center', + }, + selectionInfo: { + padding: 10, + borderRadius: 8, + marginTop: 4, + }, + selectionInfoText: { + fontSize: 13, + textAlign: 'center', + }, }); diff --git a/apps/mobile/src/services/calendar.ts b/apps/mobile/src/services/calendar.ts index 448c5ca..fda858a 100644 --- a/apps/mobile/src/services/calendar.ts +++ b/apps/mobile/src/services/calendar.ts @@ -1,5 +1,5 @@ import * as Calendar from 'expo-calendar'; -import type { Event as CoreEvent } from '@timetoleave/core'; +import type { Event as CoreEvent, CalendarAccountType, CalendarSourceInfo, SelectableCalendar } from '@timetoleave/core'; /** * Native calendar integration for the mobile app. @@ -7,33 +7,166 @@ import type { Event as CoreEvent } from '@timetoleave/core'; * * Only events that have a non-empty `location` field are included, since the * app requires a destination to compute routes. + * + * Supports calendar selection so the user can pick which calendars to sync. + * CalDAV sources (DAVx5 on Android, Apple Calendar on iOS) are detected and + * labelled automatically. */ +// ── Source type metadata ── + +/** Maps expo-calendar account types to human-readable info. */ +const SOURCE_INFO: Record = { + caldav: { + label: 'CalDAV / DAVx', + badge: 'CalDAV', + emoji: '🔗', + }, + mobileme: { + label: 'Apple Calendar', + badge: 'Apple', + emoji: '🍎', + }, + google: { + label: 'Google Calendar', + badge: 'Google', + emoji: '📅', + }, + exchange: { + label: 'Microsoft Exchange', + badge: 'Exchange', + emoji: '🏢', + }, + subscriptions: { + label: 'Subscribed', + badge: 'Sub', + emoji: '📡', + }, + local: { + label: 'On My Device', + badge: 'Local', + emoji: '📱', + }, + carddav: { + label: 'CardDAV', + badge: 'CardDAV', + emoji: '👤', + }, + activesync: { + label: 'ActiveSync', + badge: 'Sync', + emoji: '🔄', + }, +}; + +const DEFAULT_SOURCE_INFO: CalendarSourceInfo = { + label: 'Other', + badge: 'Other', + emoji: '📆', +}; + +/** + * Resolve the account type string from expo-calendar into a known + * CalendarAccountType. Returns 'other' for unknown types. + */ +function resolveAccountType(type: string | undefined | null): CalendarAccountType { + if (!type) return 'other'; + const lower = type.toLowerCase(); + if (lower in SOURCE_INFO) return lower as CalendarAccountType; + return 'other'; +} + +/** + * Get source info for an expo-calendar source object. + * DAVx5 on Android appears as `caldav`, Apple Calendar on iOS as `mobileme`. + */ +function sourceInfoFor(source: { type?: string } | undefined | null): CalendarSourceInfo { + if (!source || !source.type) return DEFAULT_SOURCE_INFO; + return SOURCE_INFO[source.type.toLowerCase()] ?? DEFAULT_SOURCE_INFO; +} + +// ── Permission ── + /** Requests calendar read permission and verifies the calendar service is available. */ export async function ensureCalendarPermission(): Promise { const { status } = await Calendar.requestCalendarPermissionsAsync(); - if (status !== 'granted') - return false; - + if (status !== 'granted') return false; return Calendar.isAvailableAsync(); } +// ── Calendar listing ── + /** - * Fetch events from native calendars within a date range. - * Returns events converted to our internal Event format. + * Get all available calendars on the device, converted to a minimal format + * suitable for the selection UI. Grouped by account type. + * + * DAVx5 calendars (Android) will show as CalDAV type. + * Apple Calendar (iOS) will show as mobileme type. */ -export async function fetchNativeEvents( - startDate: Date, - endDate: Date, -): Promise { +export async function getSelectableCalendars(): Promise { const available = await ensureCalendarPermission(); if (!available) return []; const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); + + return calendars.map((cal) => { + const accountType = resolveAccountType(cal.source?.type); + const sourceInfo = sourceInfoFor(cal.source); + + return { + id: cal.id, + name: cal.name ?? 'Unnamed Calendar', + accountType, + sourceInfo, + editable: cal.allowsModifications ?? false, + }; + }); +} + +/** + * Group calendars by account type for display in the selection UI. + */ +export function groupCalendarsByType(calendars: SelectableCalendar[]): Map { + const groups = new Map(); + + for (const cal of calendars) { + const existing = groups.get(cal.accountType) ?? []; + existing.push(cal); + groups.set(cal.accountType, existing); + } + + return groups; +} + +// ── Event fetching ── + +/** + * Fetch events from native calendars within a date range. + * Returns events converted to our internal Event format. + * + * @param startDate - Start of the date range. + * @param endDate - End of the date range. + * @param calendarIds - Optional list of calendar IDs to fetch. If omitted, + * fetches from all calendars. + */ +export async function fetchNativeEvents( + startDate: Date, + endDate: Date, + calendarIds?: string[], +): Promise { + const available = await ensureCalendarPermission(); + if (!available) return []; + + let calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); + + if (calendarIds && calendarIds.length > 0) { + calendars = calendars.filter((c) => calendarIds.includes(c.id)); + } + if (calendars.length === 0) return []; - const calendarIds = calendars.map((c) => c.id); - const events = await Calendar.getEventsAsync(calendarIds, startDate, endDate); + const ids = calendarIds ?? calendars.map((c) => c.id); + const events = await Calendar.getEventsAsync(ids, startDate, endDate); return events .filter((evt) => evt.location && evt.location.trim().length > 0) diff --git a/apps/mobile/src/store/eventStore.ts b/apps/mobile/src/store/eventStore.ts index fbae81a..e0f95a6 100644 --- a/apps/mobile/src/store/eventStore.ts +++ b/apps/mobile/src/store/eventStore.ts @@ -16,6 +16,7 @@ import { SchedulableTriggerInputTypes } from 'expo-notifications'; const EVENTS_KEY = '@timetoleave_events'; const ORIGIN_KEY = '@timetoleave_origin'; const NOTIFICATIONS_KEY = '@timetoleave_notifications'; +const SELECTED_CALENDARS_KEY = '@timetoleave_selected_calendars'; // ── Default notification settings ───────────────────────────── @@ -190,8 +191,40 @@ export async function saveNotificationSettings( } // ─────────────────────────────────────────────────────────────── -// Reschedule all notifications (for origin/setting changes) -// ─────────────────────────────────────────────────────────────── +// Calendar Selection +// ──────────────────────────────────────────────────────────────── + +/** + * Get the list of calendar IDs the user has chosen to sync. + * Returns an empty array if no selection has been made yet (meaning + * all calendars should be synced). + */ +export async function getSelectedCalendarIds(): Promise { + const json = await AsyncStorage.getItem(SELECTED_CALENDARS_KEY); + if (!json) return []; + try { + return JSON.parse(json) as string[]; + } catch { + return []; + } +} + +/** + * Save the list of calendar IDs the user wants to sync. + * Pass an empty array to reset to "sync all calendars". + */ +export async function saveSelectedCalendarIds(ids: string[]): Promise { + await AsyncStorage.setItem(SELECTED_CALENDARS_KEY, JSON.stringify(ids)); +} + +/** + * Check whether the user has made an explicit calendar selection. + * Returns true if a non-empty list is stored. + */ +export async function hasCalendarSelection(): Promise { + const ids = await getSelectedCalendarIds(); + return ids.length > 0; +} export async function rescheduleAllNotifications(): Promise { const [events, settings] = await Promise.all([loadEvents(), loadNotificationSettings()]); diff --git a/apps/web/public/bars-solid.svg b/apps/web/public/bars-solid.svg new file mode 100644 index 0000000..61164ce --- /dev/null +++ b/apps/web/public/bars-solid.svg @@ -0,0 +1 @@ + \ 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..e4b772f --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,63 @@ +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 {} + +def ccache_enabled?(podfile_properties) + # Environment variable takes precedence + return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE'] + + # Fall back to Podfile properties + podfile_properties['apple.ccacheEnabled'] == 'true' +end + +ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] +ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false' +ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false' +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' + +prepare_react_native_project! + +target 'timetoleave' 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(\'expo/bin/autolinking\')', + 'expo-modules-autolinking', + '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 => ccache_enabled?(podfile_properties), + ) + end +end diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json new file mode 100644 index 0000000..de9f7b7 --- /dev/null +++ b/ios/Podfile.properties.json @@ -0,0 +1,4 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" +} diff --git a/ios/timetoleave.xcodeproj/project.pbxproj b/ios/timetoleave.xcodeproj/project.pbxproj new file mode 100644 index 0000000..80a2ed9 --- /dev/null +++ b/ios/timetoleave.xcodeproj/project.pbxproj @@ -0,0 +1,436 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* timetoleave.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = timetoleave.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = timetoleave/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = timetoleave/Info.plist; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = timetoleave/SplashScreen.storyboard; 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; }; + F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = timetoleave/AppDelegate.swift; sourceTree = ""; }; + F11748442D0722820044C1D9 /* timetoleave-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "timetoleave-Bridging-Header.h"; path = "timetoleave/timetoleave-Bridging-Header.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* timetoleave */ = { + isa = PBXGroup; + children = ( + F11748412D0307B40044C1D9 /* AppDelegate.swift */, + F11748442D0722820044C1D9 /* timetoleave-Bridging-Header.h */, + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + ); + name = timetoleave; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* timetoleave */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* timetoleave.app */, + ); + name = Products; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = timetoleave/Supporting; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* timetoleave */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "timetoleave" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = timetoleave; + productName = timetoleave; + productReference = 13B07F961A680F5B00A75B9A /* timetoleave.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 "timetoleave" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* timetoleave */, + ); + }; +/* 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + ); + 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-timetoleave-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-timetoleave/Pods-timetoleave-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-timetoleave/Pods-timetoleave-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = timetoleave/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.floegger.timetoleave"; + PRODUCT_NAME = "timetoleave"; + SWIFT_OBJC_BRIDGING_HEADER = "timetoleave/timetoleave-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + TARGETED_DEVICE_FAMILY = "1"; + CODE_SIGN_ENTITLEMENTS = timetoleave/timetoleave.entitlements; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = timetoleave/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.floegger.timetoleave"; + PRODUCT_NAME = "timetoleave"; + SWIFT_OBJC_BRIDGING_HEADER = "timetoleave/timetoleave-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + TARGETED_DEVICE_FAMILY = "1"; + CODE_SIGN_ENTITLEMENTS = timetoleave/timetoleave.entitlements; + }; + 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 = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + 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 = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "timetoleave" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "timetoleave" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/timetoleave.xcodeproj/xcshareddata/xcschemes/timetoleave.xcscheme b/ios/timetoleave.xcodeproj/xcshareddata/xcschemes/timetoleave.xcscheme new file mode 100644 index 0000000..3d4f184 --- /dev/null +++ b/ios/timetoleave.xcodeproj/xcshareddata/xcschemes/timetoleave.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/timetoleave/AppDelegate.swift b/ios/timetoleave/AppDelegate.swift new file mode 100644 index 0000000..a7887e1 --- /dev/null +++ b/ios/timetoleave/AppDelegate.swift @@ -0,0 +1,70 @@ +import Expo +import React +import ReactAppDependencyProvider + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + var window: UIWindow? + + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + +#if os(iOS) || os(tvOS) + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "main", + in: window, + launchOptions: launchOptions) +#endif + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + // Extension point for config-plugins + + override func sourceURL(for bridge: RCTBridge) -> URL? { + // needed to return the correct URL for expo-dev-client. + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/ios/timetoleave/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/timetoleave/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac881f6063fdf449befe03c655b937ae58472728 GIT binary patch literal 5856 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&zE~RK2WrGXsOza!(h>kP5~(2OD`A6d0Hk zKL4M;+FGXIoj6cQLKHKQJd+J18Y+2#q``VdAocxjVc}u zjnT9*no~xLg3*EzSRsrS1*1j5Xi+d)6pR)Hzz`TM3Py{9(V}3qC>SjYM#l<9M@~k^ zkp}yy+P-fO8bE`Ei~(QjOYEIMgMS6!!M}Il!N0wr!M`2g;jZtX4E+E+_;&|9Qdm8z gcr-Mqoi=zM#d80VU4Lc)FtIUsy85}Sb4q9e06!I^tN;K2 literal 0 HcmV?d00001 diff --git a/ios/timetoleave/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/timetoleave/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/ios/timetoleave/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/timetoleave/Images.xcassets/Contents.json b/ios/timetoleave/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/ios/timetoleave/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/ios/timetoleave/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/timetoleave/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..15f02ab --- /dev/null +++ b/ios/timetoleave/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/timetoleave/Info.plist b/ios/timetoleave/Info.plist new file mode 100644 index 0000000..865246d --- /dev/null +++ b/ios/timetoleave/Info.plist @@ -0,0 +1,79 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + time-to-leave + 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 + + com.floegger.timetoleave + + + + CFBundleURLSchemes + + exp+time-to-leave + + + + CFBundleVersion + 1 + ITSAppUsesNonExemptEncryption + + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + RCTNewArchEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/timetoleave/SplashScreen.storyboard b/ios/timetoleave/SplashScreen.storyboard new file mode 100644 index 0000000..6c99b2a --- /dev/null +++ b/ios/timetoleave/SplashScreen.storyboard @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/timetoleave/Supporting/Expo.plist b/ios/timetoleave/Supporting/Expo.plist new file mode 100644 index 0000000..750be02 --- /dev/null +++ b/ios/timetoleave/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/ios/timetoleave/timetoleave-Bridging-Header.h b/ios/timetoleave/timetoleave-Bridging-Header.h new file mode 100644 index 0000000..8361941 --- /dev/null +++ b/ios/timetoleave/timetoleave-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/timetoleave/timetoleave.entitlements b/ios/timetoleave/timetoleave.entitlements new file mode 100644 index 0000000..f683276 --- /dev/null +++ b/ios/timetoleave/timetoleave.entitlements @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1f74b70..002a64c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,28 @@ "workspaces": [ "apps/*", "packages/*" - ] + ], + "dependencies": { + "@react-native-async-storage/async-storage": "2.2.0", + "expo": "~54.0.34", + "expo-calendar": "~15.0.8", + "expo-dev-client": "~6.0.21", + "expo-location": "~19.0.8", + "expo-notifications": "~0.32.17", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5" + } }, "apps/mobile": { "name": "@timetoleave/mobile", "version": "1.0.0", "dependencies": { + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-native-fontawesome": "^1.0.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.2.4", "@react-navigation/native-stack": "^7.14.14", @@ -30,7 +46,8 @@ "react": "19.1.0", "react-native": "0.81.5", "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "~4.16.0" + "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -2678,6 +2695,58 @@ "excpretty": "build/cli.js" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-native-fontawesome": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-native-fontawesome/-/react-native-fontawesome-1.0.0.tgz", + "integrity": "sha512-whGM1GewA1kO0r+RxSY4cefcXNAiKGMh9PIkfid8lMd7ndO/yDAEHBiHTYVfeCICm7DwbmcAUWBJZ6HcFeN7fg==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "humps": "^2.0.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~7", + "react": "*", + "react-native": "*", + "react-native-svg": ">=11" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -7028,6 +7097,12 @@ "node": ">=0.6" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -7629,6 +7704,22 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -7643,6 +7734,18 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -7985,6 +8088,44 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -8009,6 +8150,35 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -10133,6 +10303,12 @@ "node": ">=10.17.0" } }, + "node_modules/humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -13991,6 +14167,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -15136,6 +15324,39 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.15.5", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.5.tgz", + "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/react-native-svg/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/react-native/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", diff --git a/package.json b/package.json index a4dbbbf..5d55907 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,17 @@ "react-test-renderer": "19.1.0" }, "dependencies": { + "@react-native-async-storage/async-storage": "2.2.0", "expo": "~54.0.34", + "expo-calendar": "~15.0.8", + "expo-dev-client": "~6.0.21", + "expo-location": "~19.0.8", + "expo-notifications": "~0.32.17", + "expo-status-bar": "~3.0.9", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5" } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 225790b..1510fd5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -144,3 +144,35 @@ export interface NearbyStop { lat: number; lng: number; } + +// ── Calendar Source Types ── + +/** Known calendar account types from expo-calendar. */ +export type CalendarAccountType = + | 'caldav' + | 'local' + | 'exchange' + | 'google' + | 'mobileme' + | 'subscriptions' + | 'carddav' + | 'activesync' + | 'other' + | 'none'; + +/** Human-readable label and visual badge for each account type. */ +export interface CalendarSourceInfo { + label: string; + badge: string; + /** Icon emoji shown next to the calendar name. */ + emoji: string; +} + +/** Minimal calendar descriptor used for selection UI. */ +export interface SelectableCalendar { + id: string; + name: string; + accountType: CalendarAccountType; + sourceInfo: CalendarSourceInfo; + editable: boolean; +} diff --git a/tsconfig.json b/tsconfig.json index fe3dc51..c7e43a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,11 +12,22 @@ "sourceMap": true }, "include": [], - "exclude": ["node_modules"], + "exclude": [ + "node_modules" + ], "references": [ - { "path": "apps/web" }, - { "path": "apps/mobile" }, - { "path": "packages/core" }, - { "path": "packages/api-client" } - ] + { + "path": "apps/web" + }, + { + "path": "apps/mobile" + }, + { + "path": "packages/core" + }, + { + "path": "packages/api-client" + } + ], + "extends": "expo/tsconfig.base" }