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.
This commit is contained in:
2026-05-15 18:26:51 +02:00
parent b3bba8cf38
commit 9d6efdd43b
38 changed files with 1815 additions and 58 deletions
+13
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
{
"devices": []
}
+3 -3
View File
@@ -87,13 +87,13 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
namespace "com.timetoleave" namespace 'com.floegger.timetoleave'
defaultConfig { defaultConfig {
applicationId "com.timetoleave" applicationId 'com.floegger.timetoleave'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
} }
+15 -13
View File
@@ -1,29 +1,31 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- OPTIONAL PERMISSIONS, REMOVE WHATEVER YOU DO NOT NEED --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<!-- These require runtime permissions on M -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- END OPTIONAL PERMISSIONS -->
<queries> <queries>
<!-- Support checking for http(s) links via the Linking API -->
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" /> <data android:scheme="https"/>
</intent> </intent>
</queries> </queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true"> <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="exp+time-to-leave"/>
</intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>
@@ -1,4 +1,4 @@
package com.timetoleave package com.floegger.timetoleave
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -1,4 +1,4 @@
package com.timetoleave package com.floegger.timetoleave
import android.app.Application import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
@@ -0,0 +1 @@
<resources/>
+2 -1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources> <resources>
<color name="splashscreen_background">#FFFFFF</color> <color name="splashscreen_background">#FFFFFF</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
</resources> </resources>
+2 -2
View File
@@ -1,3 +1,3 @@
<resources> <resources>
<string name="app_name">time-to-leave</string> <string name="app_name">time-to-leave</string>
</resources> </resources>
+6 -3
View File
@@ -1,8 +1,11 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#ffffff</item>
</style> </style>
<style name="Theme.App.SplashScreen" parent="AppTheme"> <style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/splashscreen_logo</item> <item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style> </style>
</resources> </resources>
+4
View File
@@ -59,3 +59,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK. # Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false 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
+1 -1
View File
@@ -31,7 +31,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
} }
expoAutolinking.useExpoModules() expoAutolinking.useExpoModules()
rootProject.name = 'timetoleave' rootProject.name = 'time-to-leave'
expoAutolinking.useExpoVersionCatalog() expoAutolinking.useExpoVersionCatalog()
+4 -1
View File
@@ -13,6 +13,8 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "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-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.2.4", "@react-navigation/native": "^7.2.4",
"@react-navigation/native-stack": "^7.14.14", "@react-navigation/native-stack": "^7.14.14",
@@ -27,7 +29,8 @@
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "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": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
+97 -3
View File
@@ -1,8 +1,11 @@
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; 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 { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { EventListScreen } from '../screens/EventListScreen'; import { EventListScreen } from '../screens/EventListScreen';
import { EventDetailScreen } from '../screens/EventDetailScreen'; import { EventDetailScreen } from '../screens/EventDetailScreen';
import { AddEventScreen } from '../screens/AddEventScreen'; import { AddEventScreen } from '../screens/AddEventScreen';
@@ -14,6 +17,57 @@ import navLogo from '../../assets/nav-logo.png';
// ── Root Stack ── // ── Root Stack ──
const Root = createNativeStackNavigator<RootStack>(); const Root = createNativeStackNavigator<RootStack>();
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 (
<>
<Pressable
accessibilityLabel="Open menu"
accessibilityRole="button"
hitSlop={12}
onPress={() => setIsOpen(true)}
style={({ pressed }) => [styles.menuButton, pressed && styles.menuButtonPressed]}
>
<FontAwesomeIcon icon={byPrefixAndName.fas['bars']} color="#F4F1EA" size={22} />
</Pressable>
<Modal animationType="fade" transparent visible={isOpen} onRequestClose={() => setIsOpen(false)}>
<Pressable style={styles.menuOverlay} onPress={() => setIsOpen(false)}>
<View style={styles.menuPanel}>
<Pressable
accessibilityRole="menuitem"
onPress={() => navigateTo('CalendarImport')}
style={({ pressed }) => [styles.menuItem, pressed && styles.menuItemPressed]}
>
<Text style={styles.menuItemText}>Calendar</Text>
</Pressable>
<Pressable
accessibilityRole="menuitem"
onPress={() => navigateTo('Settings')}
style={({ pressed }) => [styles.menuItem, pressed && styles.menuItemPressed]}
>
<Text style={styles.menuItemText}>Settings</Text>
</Pressable>
</View>
</Pressable>
</Modal>
</>
);
}
/** /**
* Root native stack navigator for the app. * Root native stack navigator for the app.
@@ -27,16 +81,17 @@ export default function AppNavigator() {
<NavigationContainer> <NavigationContainer>
<Root.Navigator <Root.Navigator
initialRouteName="EventList" initialRouteName="EventList"
screenOptions={{ screenOptions={({ navigation }) => ({
headerStyle: { backgroundColor: '#17112A' }, headerStyle: { backgroundColor: '#17112A' },
headerTintColor: '#F4F1EA', headerTintColor: '#F4F1EA',
headerRight: () => <HeaderMenu navigation={navigation} />,
headerTitle: () => ( headerTitle: () => (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Image source={navLogo} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="contain" /> <Image source={navLogo} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="contain" />
<Text style={{ color: '#F4F1EA', fontSize: 18, fontWeight: '600' }}>Time To Leave</Text> <Text style={{ color: '#F4F1EA', fontSize: 18, fontWeight: '600' }}>Time To Leave</Text>
</View> </View>
), ),
}} })}
> >
<Root.Screen name="EventList" component={EventListScreen} /> <Root.Screen name="EventList" component={EventListScreen} />
<Root.Screen name="AddEvent" component={AddEventScreen} /> <Root.Screen name="AddEvent" component={AddEventScreen} />
@@ -49,3 +104,42 @@ export default function AppNavigator() {
</SafeAreaProvider> </SafeAreaProvider>
); );
} }
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',
},
});
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
ScrollView, ScrollView,
@@ -11,9 +11,9 @@ import {
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native'; import type { RouteProp } from '@react-navigation/native';
import { api } from '../services/api'; import { api } from '../services/api';
import { fetchNativeEvents } from '../services/calendar'; import { fetchNativeEvents, getSelectableCalendars, groupCalendarsByType } from '../services/calendar';
import { addEvent, loadEvents } from '../store/eventStore'; import { addEvent, getSelectedCalendarIds, hasCalendarSelection, loadEvents, saveSelectedCalendarIds } from '../store/eventStore';
import type { Event as CalendarEvent } from '@timetoleave/core'; import type { Event as CalendarEvent, CalendarAccountType, SelectableCalendar } from '@timetoleave/core';
import type { RootStack } from '../types/navigation'; import type { RootStack } from '../types/navigation';
import { useColors } from '../hooks/useColors'; import { useColors } from '../hooks/useColors';
@@ -22,9 +22,81 @@ type ScreenProps = {
route: RouteProp<RootStack, 'CalendarImport'>; route: RouteProp<RootStack, 'CalendarImport'>;
}; };
/**
* 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<string, string> = {
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<typeof useColors>;
}) {
const badgeColor = BADGE_COLORS[calendar.accountType] ?? BADGE_COLORS.other;
return (
<TouchableOpacity
style={[styles.calendarRow, { borderColor: selected ? colors.accent : colors.border }]}
onPress={() => onToggle(calendar.id)}
accessibilityRole="button"
accessibilityLabel={`${calendar.name} ${calendar.sourceInfo.label}`}
>
<View style={[styles.checkBox, selected && styles.checkBoxSelected]}>
{selected && <Text style={styles.checkMark}></Text>}
</View>
<View style={styles.calendarInfo}>
<Text style={[styles.calendarName, { color: colors.text }]}>
{calendar.sourceInfo.emoji} {calendar.name}
</Text>
<View style={styles.badgeRow}>
<View style={[styles.badge, { backgroundColor: badgeColor }]}>
<Text style={styles.badgeText}>{calendar.sourceInfo.badge}</Text>
</View>
<Text style={[styles.accountTypeText, { color: colors.subtext }]}>
{calendar.sourceInfo.label}
</Text>
</View>
</View>
</TouchableOpacity>
);
}
/** /**
* Screen for importing events from either an ICS calendar URL or the device's * 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) { export function CalendarImportScreen({ navigation }: ScreenProps) {
const colors = useColors(); const colors = useColors();
@@ -33,6 +105,57 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [count, setCount] = useState<number | null>(null); const [count, setCount] = useState<number | null>(null);
// Calendar selection state
const [availableCalendars, setAvailableCalendars] = useState<SelectableCalendar[]>([]);
const [selectedCalendarIds, setSelectedCalendarIds] = useState<Set<string>>(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 () => { const handleImport = async () => {
if (!url.trim()) { if (!url.trim()) {
setError('Bitte ICS-URL eingeben'); setError('Bitte ICS-URL eingeben');
@@ -77,11 +200,19 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
setCount(null); setCount(null);
try { try {
// Save the current selection before syncing
await saveSelection();
// Fetch events from the next 30 days // Fetch events from the next 30 days
const now = new Date(); const now = new Date();
const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); 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 // Load existing events to avoid duplicates
const existing = await loadEvents(); 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 ( return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}> <ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.content}> <View style={styles.content}>
@@ -111,6 +245,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender. Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender.
</Text> </Text>
{/* ── ICS URL Import ── */}
<View style={styles.section}> <View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>ICS-URL Import</Text> <Text style={[styles.sectionTitle, { color: colors.text }]}>ICS-URL Import</Text>
@@ -137,6 +272,75 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* ── Calendar Selection ── */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Kalender auswählen</Text>
<View style={styles.selectionActions}>
<TouchableOpacity onPress={selectAll} accessibilityLabel="Alle auswählen">
<Text style={[styles.linkText, { color: colors.accent }]}>Alle</Text>
</TouchableOpacity>
<Text style={[styles.dividerText, { color: colors.subtext }]}> · </Text>
<TouchableOpacity onPress={deselectAll} accessibilityLabel="Alle abwählen">
<Text style={[styles.linkText, { color: colors.accent }]}>Keine</Text>
</TouchableOpacity>
</View>
</View>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}>
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.
</Text>
{!calendarsLoaded ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color={colors.accent} />
<Text style={[styles.loadingText, { color: colors.subtext }]}>Kalender werden geladen</Text>
</View>
) : availableCalendars.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.subtext }]}>
Keine Kalender auf diesem Gerät gefunden.
</Text>
</View>
) : (
<>
{/* 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 (
<View key={type} style={styles.group}>
<Text style={[styles.groupLabel, { color: colors.subtext }]}>
{group[0].sourceInfo.emoji} {group[0].sourceInfo.label} ({group.length})
</Text>
{group.map((cal) => (
<CalendarCheckbox
key={cal.id}
calendar={cal}
selected={selectedCalendarIds.has(cal.id)}
onToggle={toggleCalendar}
colors={colors}
/>
))}
</View>
);
})}
{hasSelection && (
<View style={[styles.selectionInfo, { backgroundColor: colors.highlight }]}>
<Text style={[styles.selectionInfoText, { color: colors.text }]}>
{selectedCalendarIds.size} von {availableCalendars.length} Kalendern ausgewählt
</Text>
</View>
)}
</>
)}
</View>
{/* ── Device Calendar Sync ── */}
<View style={styles.section}> <View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Geräte-Kalender Sync</Text> <Text style={[styles.sectionTitle, { color: colors.text }]}>Geräte-Kalender Sync</Text>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}> <Text style={[styles.sectionDesc, { color: colors.subtext }]}>
@@ -183,6 +387,12 @@ const styles = StyleSheet.create({
heading: { fontSize: 22, fontWeight: '700', marginBottom: 4 }, heading: { fontSize: 22, fontWeight: '700', marginBottom: 4 },
description: { fontSize: 14, marginBottom: 20, lineHeight: 20 }, description: { fontSize: 14, marginBottom: 20, lineHeight: 20 },
section: { marginBottom: 24 }, section: { marginBottom: 24 },
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
sectionTitle: { fontSize: 16, fontWeight: '600', marginBottom: 8 }, sectionTitle: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
sectionDesc: { fontSize: 13, marginBottom: 12, lineHeight: 18 }, sectionDesc: { fontSize: 13, marginBottom: 12, lineHeight: 18 },
input: { input: {
@@ -210,4 +420,107 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
backBtnText: { fontSize: 15 }, 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',
},
}); });
+145 -12
View File
@@ -1,5 +1,5 @@
import * as Calendar from 'expo-calendar'; 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. * 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 * Only events that have a non-empty `location` field are included, since the
* app requires a destination to compute routes. * 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<string, CalendarSourceInfo> = {
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. */ /** Requests calendar read permission and verifies the calendar service is available. */
export async function ensureCalendarPermission(): Promise<boolean> { export async function ensureCalendarPermission(): Promise<boolean> {
const { status } = await Calendar.requestCalendarPermissionsAsync(); const { status } = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') if (status !== 'granted') return false;
return false;
return Calendar.isAvailableAsync(); return Calendar.isAvailableAsync();
} }
// ── Calendar listing ──
/** /**
* Fetch events from native calendars within a date range. * Get all available calendars on the device, converted to a minimal format
* Returns events converted to our internal Event 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( export async function getSelectableCalendars(): Promise<SelectableCalendar[]> {
startDate: Date,
endDate: Date,
): Promise<CoreEvent[]> {
const available = await ensureCalendarPermission(); const available = await ensureCalendarPermission();
if (!available) return []; if (!available) return [];
const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); 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<CalendarAccountType, SelectableCalendar[]> {
const groups = new Map<CalendarAccountType, SelectableCalendar[]>();
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<CoreEvent[]> {
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 []; if (calendars.length === 0) return [];
const calendarIds = calendars.map((c) => c.id); const ids = calendarIds ?? calendars.map((c) => c.id);
const events = await Calendar.getEventsAsync(calendarIds, startDate, endDate); const events = await Calendar.getEventsAsync(ids, startDate, endDate);
return events return events
.filter((evt) => evt.location && evt.location.trim().length > 0) .filter((evt) => evt.location && evt.location.trim().length > 0)
+35 -2
View File
@@ -16,6 +16,7 @@ import { SchedulableTriggerInputTypes } from 'expo-notifications';
const EVENTS_KEY = '@timetoleave_events'; const EVENTS_KEY = '@timetoleave_events';
const ORIGIN_KEY = '@timetoleave_origin'; const ORIGIN_KEY = '@timetoleave_origin';
const NOTIFICATIONS_KEY = '@timetoleave_notifications'; const NOTIFICATIONS_KEY = '@timetoleave_notifications';
const SELECTED_CALENDARS_KEY = '@timetoleave_selected_calendars';
// ── Default notification settings ───────────────────────────── // ── 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<string[]> {
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<void> {
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<boolean> {
const ids = await getSelectedCalendarIds();
return ids.length > 0;
}
export async function rescheduleAllNotifications(): Promise<void> { export async function rescheduleAllNotifications(): Promise<void> {
const [events, settings] = await Promise.all([loadEvents(), loadNotificationSettings()]); const [events, settings] = await Promise.all([loadEvents(), loadNotificationSettings()]);
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>

After

Width:  |  Height:  |  Size: 528 B

+30
View File
@@ -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/
+11
View File
@@ -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)
+63
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true"
}
+436
View File
@@ -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 = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = timetoleave/Info.plist; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = timetoleave/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
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 = "<group>"; };
F11748442D0722820044C1D9 /* timetoleave-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "timetoleave-Bridging-Header.h"; path = "timetoleave/timetoleave-Bridging-Header.h"; sourceTree = "<group>"; };
/* 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 = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* timetoleave */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* timetoleave.app */,
);
name = Products;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
BB2F792C24A3F905000567C9 /* Expo.plist */,
);
name = Supporting;
path = timetoleave/Supporting;
sourceTree = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "timetoleave.app"
BlueprintName = "timetoleave"
ReferencedContainer = "container:timetoleave.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "timetoleaveTests.xctest"
BlueprintName = "timetoleaveTests"
ReferencedContainer = "container:timetoleave.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "timetoleave.app"
BlueprintName = "timetoleave"
ReferencedContainer = "container:timetoleave.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "timetoleave.app"
BlueprintName = "timetoleave"
ReferencedContainer = "container:timetoleave.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+70
View File
@@ -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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,14 @@
{
"images": [
{
"filename": "App-Icon-1024x1024@1x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "expo"
}
}
@@ -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"
}
}
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>time-to-leave</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.floegger.timetoleave</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+time-to-leave</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>RCTNewArchEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EXPO-SCENE-1">
<objects>
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews/>
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
<constraints>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="centerY" secondItem="EXPO-ContainerView" secondAttribute="centerY" id="0VC-Wk-OaO"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="centerX" secondItem="EXPO-ContainerView" secondAttribute="centerX" id="zR4-NK-mVN"/>
</constraints>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="SplashScreenLogo" width="100" height="90.333335876464844"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<false/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
</dict>
</plist>
@@ -0,0 +1,3 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
+223 -2
View File
@@ -10,12 +10,28 @@
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "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": { "apps/mobile": {
"name": "@timetoleave/mobile", "name": "@timetoleave/mobile",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "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-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.2.4", "@react-navigation/native": "^7.2.4",
"@react-navigation/native-stack": "^7.14.14", "@react-navigation/native-stack": "^7.14.14",
@@ -30,7 +46,8 @@
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "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": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
@@ -2678,6 +2695,58 @@
"excpretty": "build/cli.js" "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": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -7028,6 +7097,12 @@
"node": ">=0.6" "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": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -7629,6 +7704,22 @@
"node": ">= 8" "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": { "node_modules/css-tree": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "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": "^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": { "node_modules/css.escape": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -7985,6 +8088,44 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/domexception": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@@ -8009,6 +8150,35 @@
"node": ">=12" "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": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -10133,6 +10303,12 @@
"node": ">=10.17.0" "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": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -13991,6 +14167,18 @@
"node": ">=8" "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": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -15136,6 +15324,39 @@
"react-native": "*" "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": { "node_modules/react-native/node_modules/ansi-styles": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+10 -1
View File
@@ -24,8 +24,17 @@
"react-test-renderer": "19.1.0" "react-test-renderer": "19.1.0"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"expo": "~54.0.34", "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": "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"
} }
} }
+32
View File
@@ -144,3 +144,35 @@ export interface NearbyStop {
lat: number; lat: number;
lng: 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;
}
+17 -6
View File
@@ -12,11 +12,22 @@
"sourceMap": true "sourceMap": true
}, },
"include": [], "include": [],
"exclude": ["node_modules"], "exclude": [
"node_modules"
],
"references": [ "references": [
{ "path": "apps/web" }, {
{ "path": "apps/mobile" }, "path": "apps/web"
{ "path": "packages/core" }, },
{ "path": "packages/api-client" } {
] "path": "apps/mobile"
},
{
"path": "packages/core"
},
{
"path": "packages/api-client"
}
],
"extends": "expo/tsconfig.base"
} }