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 0000000..ac881f6
Binary files /dev/null and b/ios/timetoleave/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ
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"
}