Anmeldung und Kalendar

This commit is contained in:
Ismail Ali
2025-07-15 20:38:04 +02:00
parent 2a34091404
commit 1cc8951c12
51 changed files with 19961 additions and 177 deletions

63
App.tsx Normal file
View File

@@ -0,0 +1,63 @@
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import React from "react";
import AuthScreen from "./src/screens/AuthScreen";
import LoginScreen from "./src/screens/LoginScreen";
import RegisterScreen from "./src/screens/RegisterScreen";
import WelcomeScreen from "./src/screens/WelcomeScreen";
import { RootStackParamList } from "./src/types";
const Stack = createStackNavigator<RootStackParamList>();
const App: React.FC = () => {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="AuthScreen"
screenOptions={{
headerStyle: {
backgroundColor: "#007AFF",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen
name="AuthScreen"
component={AuthScreen}
options={{
title: "Authentifizierung",
headerShown: false,
}}
/>
<Stack.Screen
name="LoginScreen"
component={LoginScreen}
options={{
title: "Anmelden",
}}
/>
<Stack.Screen
name="RegisterScreen"
component={RegisterScreen}
options={{
title: "Registrieren",
}}
/>
<Stack.Screen
name="WelcomeScreen"
component={WelcomeScreen}
options={{
title: "Willkommen",
headerLeft: () => null,
gestureEnabled: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;

View File

@@ -33,7 +33,8 @@
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
} }
] ],
"expo-sqlite"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

240
app/(tabs)/calendar.tsx Normal file
View File

@@ -0,0 +1,240 @@
import * as Calendar from "expo-calendar";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
FlatList,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface EventItem {
id: string;
title: string;
startDate: string;
endDate: string;
}
const db = SQLite.openDatabaseSync("events.db");
export default function CalendarTab() {
const [source, setSource] = useState<"sqlite" | "iphone">("sqlite");
const [events, setEvents] = useState<EventItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedYear, setSelectedYear] = useState<number>(
new Date().getFullYear()
);
const [permissionError, setPermissionError] = useState<string | null>(null);
const [calendarType, setCalendarType] = useState<"calendar" | "reminder">(
"calendar"
);
useEffect(() => {
if (source === "sqlite") {
loadEventsFromDB();
} else {
loadEventsFromDevice();
}
}, [source, selectedYear, calendarType]);
const loadEventsFromDB = async () => {
setLoading(true);
try {
const stmt = await db.prepareAsync("SELECT * FROM events;");
const loadedEvents: EventItem[] = [];
const result = await stmt.executeAsync([]);
for await (const row of result) {
loadedEvents.push(row as EventItem);
}
setEvents(loadedEvents);
await stmt.finalizeAsync();
} catch (e) {
setEvents([]);
} finally {
setLoading(false);
}
};
const loadEventsFromDevice = async () => {
setLoading(true);
setPermissionError(null);
try {
let perm;
if (calendarType === "reminder") {
perm = await Calendar.requestRemindersPermissionsAsync();
} else {
perm = await Calendar.requestCalendarPermissionsAsync();
}
if (!perm.granted) {
setEvents([]);
setPermissionError(
`Keine Berechtigung für ${
calendarType === "reminder" ? "Erinnerungen" : "Kalender"
}-Zugriff. Bitte in den iOS-Einstellungen aktivieren.`
);
return;
}
let calendars = await Calendar.getCalendarsAsync();
console.log(
"Gefundene Kalender:",
calendars.map((c) => ({ id: c.id, title: c.title, type: c.type }))
);
if (calendarType === "calendar") {
calendars = calendars.filter((c) => String(c.type) === "calendar");
} else {
calendars = calendars.filter((c) => String(c.type) === "reminder");
}
const start = new Date(selectedYear, 0, 1);
const end = new Date(selectedYear, 11, 31, 23, 59, 59);
let allEvents: EventItem[] = [];
for (const cal of calendars) {
const calEvents = await Calendar.getEventsAsync([cal.id], start, end);
console.log(
`Events für Kalender ${cal.title}:`,
calEvents.map((e) => ({
id: e.id,
title: e.title,
start: e.startDate,
end: e.endDate,
}))
);
allEvents = allEvents.concat(
calEvents.map((e) => ({
id: e.id,
title: e.title || "(Kein Titel)",
startDate:
typeof e.startDate === "string"
? e.startDate
: new Date(e.startDate).toISOString(),
endDate:
typeof e.endDate === "string"
? e.endDate
: new Date(e.endDate).toISOString(),
}))
);
}
setEvents(allEvents);
} catch (e: any) {
setEvents([]);
setPermissionError(
"Fehler beim Zugriff auf " +
(calendarType === "reminder" ? "Erinnerungen" : "Kalender") +
": " +
(e?.message || JSON.stringify(e))
);
} finally {
setLoading(false);
}
};
const renderItem = ({ item }: { item: EventItem }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.time}>
{new Date(item.startDate).toLocaleString()} -{" "}
{new Date(item.endDate).toLocaleString()}
</Text>
</View>
);
return (
<SafeAreaView style={styles.container}>
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
source === "sqlite" && styles.activeButton,
]}
onPress={() => setSource("sqlite")}
>
<Text style={styles.switchText}>SQLite</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
source === "iphone" && styles.activeButton,
]}
onPress={() => setSource("iphone")}
>
<Text style={styles.switchText}>
iPhone ({calendarType === "calendar" ? "Kalender" : "Erinnerungen"})
</Text>
</TouchableOpacity>
</View>
{source === "iphone" && (
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "calendar" && styles.activeButton,
]}
onPress={() => setCalendarType("calendar")}
>
<Text style={styles.switchText}>Kalender</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "reminder" && styles.activeButton,
]}
onPress={() => setCalendarType("reminder")}
>
<Text style={styles.switchText}>Erinnerungen</Text>
</TouchableOpacity>
</View>
)}
<Text style={styles.header}>Kalender Events ({selectedYear})</Text>
<FlatList
data={events}
keyExtractor={(item) => item.id}
renderItem={renderItem}
ListEmptyComponent={
permissionError ? (
<Text style={{ textAlign: "center", color: "red" }}>
{permissionError}
</Text>
) : (
<Text style={{ textAlign: "center" }}>Keine Events gefunden.</Text>
)
}
refreshing={loading}
onRefresh={
source === "sqlite" ? loadEventsFromDB : loadEventsFromDevice
}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff", padding: 10 },
header: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 16,
textAlign: "center",
},
item: {
marginBottom: 12,
padding: 12,
borderRadius: 8,
backgroundColor: "#f2f2f2",
},
title: { fontSize: 16, fontWeight: "600" },
time: { fontSize: 14, color: "#555" },
switchRow: {
flexDirection: "row",
justifyContent: "center",
marginBottom: 16,
},
switchButton: {
padding: 10,
borderRadius: 8,
backgroundColor: "#eee",
marginHorizontal: 8,
},
activeButton: { backgroundColor: "#007AFF" },
switchText: { color: "#333", fontWeight: "bold" },
});

View File

@@ -1,110 +0,0 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Collapsible } from '@/components/Collapsible';
import { ExternalLink } from '@/components/ExternalLink';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { IconSymbol } from '@/components/ui/IconSymbol';
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Explore</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Custom fonts">
<ThemedText>
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '}
<ThemedText style={{ fontFamily: 'SpaceMono' }}>
custom fonts such as this one.
</ThemedText>
</ThemedText>
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
});

View File

@@ -1,75 +1,501 @@
import { Image } from 'expo-image'; import AsyncStorage from "@react-native-async-storage/async-storage";
import { Platform, StyleSheet } from 'react-native'; import * as LocalAuthentication from "expo-local-authentication";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import CalendarSync from "../../components/CalendarSync";
import { HelloWave } from '@/components/HelloWave'; // Types
import ParallaxScrollView from '@/components/ParallaxScrollView'; interface User {
import { ThemedText } from '@/components/ThemedText'; id: number;
import { ThemedView } from '@/components/ThemedView'; email: string;
firstName: string;
lastName: string;
createdAt: string;
}
// Simple Auth Component
export default function AuthScreen() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [authMode, setAuthMode] = useState<"login" | "register">("login");
// Form states
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [biometricSupported, setBiometricSupported] = useState(false);
const [showCalendar, setShowCalendar] = useState(false);
// Database
const [db, setDb] = useState<SQLite.SQLiteDatabase | null>(null);
useEffect(() => {
initApp();
}, []);
const initApp = async () => {
try {
// Initialize database
const database = await SQLite.openDatabaseAsync("AuthApp.db");
setDb(database);
await database.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
password TEXT NOT NULL,
createdAt TEXT NOT NULL
)
`);
// Check biometric support
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setBiometricSupported(compatible && enrolled);
// Check if user is logged in
const session = await AsyncStorage.getItem("@user_session");
if (session) {
const userData = JSON.parse(session);
setCurrentUser(userData.user);
setIsLoggedIn(true);
}
} catch (error) {
console.error("App initialization failed:", error);
} finally {
setLoading(false);
}
};
const handleRegister = async () => {
if (!email || !password || !firstName || !lastName) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (password.length < 6) {
Alert.alert(
"Fehler",
"Das Passwort muss mindestens 6 Zeichen lang sein."
);
return;
}
if (!db) return;
try {
setLoading(true);
const createdAt = new Date().toISOString();
const result = await db.runAsync(
"INSERT INTO users (email, firstName, lastName, password, createdAt) VALUES (?, ?, ?, ?, ?)",
[email, firstName, lastName, password, createdAt]
);
const newUser: User = {
id: result.lastInsertRowId,
email,
firstName,
lastName,
createdAt,
};
// Save session
await AsyncStorage.setItem(
"@user_session",
JSON.stringify({ user: newUser })
);
setCurrentUser(newUser);
setIsLoggedIn(true);
Alert.alert("Erfolg", "Registrierung erfolgreich!");
} catch (error: any) {
if (error.message?.includes("UNIQUE constraint failed")) {
Alert.alert(
"Fehler",
"Ein Benutzer mit dieser E-Mail existiert bereits."
);
} else {
Alert.alert("Fehler", "Registrierung fehlgeschlagen.");
}
} finally {
setLoading(false);
}
};
const handleLogin = async () => {
if (!email || !password) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (!db) return;
try {
setLoading(true);
const result = (await db.getFirstAsync(
"SELECT * FROM users WHERE email = ? AND password = ?",
[email, password]
)) as any;
if (result) {
const user: User = {
id: result.id,
email: result.email,
firstName: result.firstName,
lastName: result.lastName,
createdAt: result.createdAt,
};
// Save session
await AsyncStorage.setItem("@user_session", JSON.stringify({ user }));
setCurrentUser(user);
setIsLoggedIn(true);
Alert.alert("Erfolg", "Anmeldung erfolgreich!");
} else {
Alert.alert("Fehler", "Ungültige E-Mail oder Passwort.");
}
} catch (error) {
Alert.alert("Fehler", "Anmeldung fehlgeschlagen.");
} finally {
setLoading(false);
}
};
const handleBiometricAuth = async () => {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Biometrische Authentifizierung",
cancelLabel: "Abbrechen",
fallbackLabel: "Passcode verwenden",
});
if (result.success) {
// For demo, auto-login first user
if (!db) return;
const user = (await db.getFirstAsync(
"SELECT * FROM users LIMIT 1"
)) as any;
if (user) {
const userData: User = {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
createdAt: user.createdAt,
};
await AsyncStorage.setItem(
"@user_session",
JSON.stringify({ user: userData })
);
setCurrentUser(userData);
setIsLoggedIn(true);
}
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Authentifizierung fehlgeschlagen.");
}
};
const handleLogout = async () => {
try {
await AsyncStorage.removeItem("@user_session");
setCurrentUser(null);
setIsLoggedIn(false);
setEmail("");
setPassword("");
setFirstName("");
setLastName("");
} catch (error) {
Alert.alert("Fehler", "Abmeldung fehlgeschlagen.");
}
};
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Lade App...</Text>
</View>
</SafeAreaView>
);
}
if (isLoggedIn && currentUser) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.welcomeContainer}>
<Text style={styles.welcomeTitle}>Willkommen zurück!</Text>
<Text style={styles.userName}>
{currentUser.firstName} {currentUser.lastName}
</Text>
<View style={styles.userInfo}>
<Text style={styles.infoLabel}>E-Mail:</Text>
<Text style={styles.infoValue}>{currentUser.email}</Text>
<Text style={styles.infoLabel}>Mitglied seit:</Text>
<Text style={styles.infoValue}>
{new Date(currentUser.createdAt).toLocaleDateString("de-DE")}
</Text>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Abmelden</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitButton, { marginTop: 20 }]}
onPress={() => setShowCalendar((prev) => !prev)}
>
<Text style={styles.submitButtonText}>
{showCalendar ? "Kalender ausblenden" : "Kalender anzeigen"}
</Text>
</TouchableOpacity>
{showCalendar && <CalendarSync />}
</View>
</SafeAreaView>
);
}
export default function HomeScreen() {
return ( return (
<ParallaxScrollView <SafeAreaView style={styles.container}>
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} <KeyboardAvoidingView
headerImage={ style={styles.keyboardView}
<Image behavior={Platform.OS === "ios" ? "padding" : "height"}
source={require('@/assets/images/partial-react-logo.png')} >
style={styles.reactLogo} <View style={styles.authContainer}>
/> <Text style={styles.title}>
}> {authMode === "login" ? "Anmelden" : "Registrieren"}
<ThemedView style={styles.titleContainer}> </Text>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave /> {authMode === "register" && (
</ThemedView> <>
<ThemedView style={styles.stepContainer}> <TextInput
<ThemedText type="subtitle">Step 1: Try it</ThemedText> style={styles.input}
<ThemedText> placeholder="Vorname"
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes. value={firstName}
Press{' '} onChangeText={setFirstName}
<ThemedText type="defaultSemiBold"> autoCapitalize="words"
{Platform.select({ />
ios: 'cmd + d',
android: 'cmd + m', <TextInput
web: 'F12', style={styles.input}
})} placeholder="Nachname"
</ThemedText>{' '} value={lastName}
to open developer tools. onChangeText={setLastName}
</ThemedText> autoCapitalize="words"
</ThemedView> />
<ThemedView style={styles.stepContainer}> </>
<ThemedText type="subtitle">Step 2: Explore</ThemedText> )}
<ThemedText>
{`Tap the Explore tab to learn more about what's included in this starter app.`} <TextInput
</ThemedText> style={styles.input}
</ThemedView> placeholder="E-Mail"
<ThemedView style={styles.stepContainer}> value={email}
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText> onChangeText={setEmail}
<ThemedText> keyboardType="email-address"
{`When you're ready, run `} autoCapitalize="none"
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '} />
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '} <TextInput
<ThemedText type="defaultSemiBold">app-example</ThemedText>. style={styles.input}
</ThemedText> placeholder="Passwort"
</ThemedView> value={password}
</ParallaxScrollView> onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={styles.submitButton}
onPress={authMode === "login" ? handleLogin : handleRegister}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitButtonText}>
{authMode === "login" ? "Anmelden" : "Registrieren"}
</Text>
)}
</TouchableOpacity>
{biometricSupported && authMode === "login" && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricAuth}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.switchButton}
onPress={() =>
setAuthMode(authMode === "login" ? "register" : "login")
}
>
<Text style={styles.switchButtonText}>
{authMode === "login"
? "Noch kein Konto? Registrieren"
: "Bereits ein Konto? Anmelden"}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
titleContainer: { container: {
flexDirection: 'row', flex: 1,
alignItems: 'center', backgroundColor: "#f8f9fa",
gap: 8,
}, },
stepContainer: { keyboardView: {
gap: 8, flex: 1,
marginBottom: 8,
}, },
reactLogo: { authContainer: {
height: 178, flex: 1,
width: 290, justifyContent: "center",
bottom: 0, paddingHorizontal: 30,
left: 0, },
position: 'absolute', loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: "#666",
},
welcomeContainer: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 30,
},
welcomeTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 10,
},
userName: {
fontSize: 20,
color: "#666",
textAlign: "center",
marginBottom: 40,
},
userInfo: {
backgroundColor: "white",
padding: 20,
borderRadius: 10,
marginBottom: 30,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
infoLabel: {
fontSize: 14,
color: "#666",
marginTop: 10,
},
infoValue: {
fontSize: 16,
color: "#333",
fontWeight: "500",
marginBottom: 5,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
submitButton: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
submitButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#34C759",
paddingVertical: 15,
borderRadius: 10,
marginTop: 15,
alignItems: "center",
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
switchButton: {
marginTop: 20,
alignItems: "center",
},
switchButtonText: {
color: "#007AFF",
fontSize: 16,
},
logoutButton: {
backgroundColor: "#FF3B30",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
logoutButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
}, },
}); });

13
auth-app-backup/App.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { NavigationContainer } from "@react-navigation/native";
import React from "react";
import AppNavigator from "./src/navigation/AppNavigator";
const App: React.FC = () => {
return (
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
);
};
export default App;

111
auth-app-backup/README.md Normal file
View File

@@ -0,0 +1,111 @@
# React Native Authentication App
Eine vollständige React Native Authentifizierungs-App mit SQLite-Datenbank, biometrischer Authentifizierung (Face ID/Touch ID) und lokaler Speicherung.
## Features
-**Benutzerregistrierung und -anmeldung**
-**SQLite Datenbank** für lokale Benutzerdatenspeicherung
-**Face ID / Touch ID** Unterstützung für iOS
-**AsyncStorage** für Session-Management
-**Automatische Anmeldung** mit biometrischen Daten
-**Schöne, moderne UI** mit TypeScript
-**Navigation** zwischen verschiedenen Screens
-**Willkommensseite** nach erfolgreicher Anmeldung
## Installation
### 1. Dependencies installieren
```bash
npm install
```
### 2. iOS Setup (für Face ID/Touch ID)
```bash
cd ios && pod install && cd ..
```
### 3. Permissions in Info.plist hinzufügen (iOS)
Fügen Sie diese Zeilen in `ios/YourApp/Info.plist` hinzu:
```xml
<key>NSFaceIDUsageDescription</key>
<string>Diese App verwendet Face ID für sichere Authentifizierung.</string>
```
### 4. Android Setup
Für Android Touch ID/Fingerprint, fügen Sie diese Permissions in `android/app/src/main/AndroidManifest.xml` hinzu:
```xml
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
```
## Project Structure
```
auth-app
├── src
│ ├── components
│ │ ├── LoginForm.tsx
│ │ ├── RegisterForm.tsx
│ │ └── WelcomeScreen.tsx
│ ├── screens
│ │ ├── AuthScreen.tsx
│ │ ├── LoginScreen.tsx
│ │ ├── RegisterScreen.tsx
│ │ └── WelcomeScreen.tsx
│ ├── services
│ │ ├── database.ts
│ │ ├── auth.ts
│ │ └── biometrics.ts
│ ├── navigation
│ │ └── AppNavigator.tsx
│ ├── utils
│ │ └── storage.ts
│ └── types
│ └── index.ts
├── App.tsx
├── package.json
├── tsconfig.json
├── metro.config.js
└── react-native.config.js
```
## Installation
1. Clone the repository:
```
git clone <repository-url>
cd auth-app
```
2. Install dependencies:
```
npm install
```
3. Run the application:
```
npm start
```
## Usage
- Navigate to the login or registration screen to create a new account or log in.
- After successful authentication, you will be redirected to the welcome screen.
- If you are using an iOS device, you can enable FaceID for a quicker login experience.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.
## License
This project is licensed under the MIT License.

View File

@@ -0,0 +1,10 @@
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
};

11178
auth-app-backup/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "auth-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"react": "17.0.1",
"react-native": "0.64.2",
"@react-navigation/native": "^6.1.9",
"@react-navigation/stack": "^6.3.20",
"react-native-screens": "^3.27.0",
"react-native-safe-area-context": "^4.8.2",
"react-native-sqlite-storage": "^6.0.1",
"react-native-keychain": "^8.1.3",
"react-native-touch-id": "^4.4.1",
"@react-native-async-storage/async-storage": "2.1.2",
"react-native-vector-icons": "^10.0.3",
"expo-sqlite": "~15.2.14",
"expo-local-authentication": "~16.0.5"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-native": "^0.64.2",
"@types/react-native-sqlite-storage": "^5.0.2",
"@types/react-native-vector-icons": "^6.4.18",
"typescript": "^4.4.3",
"eslint": "^7.32.0",
"jest": "^27.0.6"
},
"jest": {
"preset": "react-native"
}
}

View File

@@ -0,0 +1,11 @@
module.exports = {
project: {
ios: {
project: './ios/auth-app.xcodeproj',
},
android: {
sourceDir: './android/app/src/main',
},
},
assets: ['./assets/fonts'],
};

View File

@@ -0,0 +1,66 @@
import React, { useState } from 'react';
import { View, TextInput, Button, Text, StyleSheet, Alert } from 'react-native';
import { authenticateUser } from '../services/auth';
import { storeUserSession } from '../utils/storage';
import { useNavigation } from '@react-navigation/native';
const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigation = useNavigation();
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
try {
const response = await authenticateUser(email, password);
if (response.success) {
await storeUserSession(response.data);
navigation.navigate('WelcomeScreen');
} else {
Alert.alert('Error', response.message);
}
} catch (error) {
Alert.alert('Error', 'An error occurred during login');
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Login" onPress={handleLogin} />
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
paddingHorizontal: 10,
},
});
export default LoginForm;

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { View, TextInput, Button, Text, StyleSheet } from 'react-native';
import { registerUser } from '../services/auth';
const RegisterForm = ({ navigation }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleRegister = async () => {
setError('');
try {
await registerUser(username, password);
navigation.navigate('WelcomeScreen');
} catch (err) {
setError(err.message);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Register</Text>
{error ? <Text style={styles.error}>{error}</Text> : null}
<TextInput
style={styles.input}
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
value={password}
onChangeText={setPassword}
/>
<Button title="Register" onPress={handleRegister} />
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
title: {
fontSize: 24,
marginBottom: 20,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
paddingLeft: 8,
},
error: {
color: 'red',
marginBottom: 10,
},
});
export default RegisterForm;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const WelcomeScreen = () => {
return (
<View style={styles.container}>
<Text style={styles.welcomeText}>Willkommen in der App!</Text>
<Text style={styles.instructionText}>Sie können sich jetzt anmelden oder registrieren.</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
welcomeText: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 10,
},
instructionText: {
fontSize: 16,
textAlign: 'center',
},
});
export default WelcomeScreen;

View File

@@ -0,0 +1,91 @@
import { createStackNavigator } from "@react-navigation/stack";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, View } from "react-native";
import AuthScreen from "../screens/AuthScreen";
import LoginScreen from "../screens/LoginScreen";
import RegisterScreen from "../screens/RegisterScreen";
import WelcomeScreen from "../screens/WelcomeScreen";
import AuthService from "../services/auth";
import { RootStackParamList } from "../types";
const Stack = createStackNavigator<RootStackParamList>();
const AppNavigator = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
initializeApp();
}, []);
const initializeApp = async () => {
try {
await AuthService.initializeAuth();
const loggedIn = await AuthService.isLoggedIn();
setIsAuthenticated(loggedIn);
} catch (error) {
console.error("App initialization failed:", error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
}
return (
<Stack.Navigator
initialRouteName={isAuthenticated ? "WelcomeScreen" : "AuthScreen"}
screenOptions={{
headerStyle: {
backgroundColor: "#007AFF",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen
name="AuthScreen"
component={AuthScreen}
options={{
title: "Authentifizierung",
headerShown: false,
}}
/>
<Stack.Screen
name="LoginScreen"
component={LoginScreen}
options={{
title: "Anmelden",
headerBackTitleVisible: false,
}}
/>
<Stack.Screen
name="RegisterScreen"
component={RegisterScreen}
options={{
title: "Registrieren",
headerBackTitleVisible: false,
}}
/>
<Stack.Screen
name="WelcomeScreen"
component={WelcomeScreen}
options={{
title: "Willkommen",
headerLeft: () => null,
gestureEnabled: false,
}}
/>
</Stack.Navigator>
);
};
export default AppNavigator;

View File

@@ -0,0 +1,172 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useEffect, useState } from "react";
import {
Alert,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RootStackParamList } from "../types";
import StorageService from "../utils/storage";
type AuthScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"AuthScreen"
>;
interface Props {
navigation: AuthScreenNavigationProp;
}
const AuthScreen: React.FC<Props> = ({ navigation }) => {
const [biometricSupported, setBiometricSupported] = useState(false);
const [biometricEnabled, setBiometricEnabled] = useState(false);
useEffect(() => {
checkBiometricSupport();
}, []);
const checkBiometricSupport = async () => {
try {
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricSupported(isSupported);
setBiometricEnabled(isEnabled);
} catch (error) {
console.error("Biometric check failed:", error);
}
};
const handleBiometricLogin = async () => {
try {
const result = await AuthService.biometricLogin();
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Fehler", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Anmeldung fehlgeschlagen.");
}
};
const navigateToLogin = () => {
navigation.navigate("LoginScreen");
};
const navigateToRegister = () => {
navigation.navigate("RegisterScreen");
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Willkommen</Text>
<Text style={styles.subtitle}>
Melden Sie sich an oder erstellen Sie ein neues Konto
</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryButton}
onPress={navigateToLogin}
>
<Text style={styles.primaryButtonText}>Anmelden</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryButton}
onPress={navigateToRegister}
>
<Text style={styles.secondaryButtonText}>Registrieren</Text>
</TouchableOpacity>
{biometricSupported && biometricEnabled && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricLogin}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
content: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 20,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#333",
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: "#666",
textAlign: "center",
marginBottom: 40,
},
buttonContainer: {
width: "100%",
maxWidth: 300,
},
primaryButton: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
marginBottom: 15,
alignItems: "center",
},
primaryButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
secondaryButton: {
backgroundColor: "transparent",
paddingVertical: 15,
borderRadius: 10,
borderWidth: 2,
borderColor: "#007AFF",
marginBottom: 15,
alignItems: "center",
},
secondaryButtonText: {
color: "#007AFF",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#4CAF50",
paddingVertical: 15,
borderRadius: 10,
marginTop: 20,
alignItems: "center",
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
});
export default AuthScreen;

View File

@@ -0,0 +1,248 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { LoginCredentials, RootStackParamList } from "../types";
import StorageService from "../utils/storage";
type LoginScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"LoginScreen"
>;
interface Props {
navigation: LoginScreenNavigationProp;
}
const LoginScreen: React.FC<Props> = ({ navigation }) => {
const [credentials, setCredentials] = useState<LoginCredentials>({
email: "",
password: "",
});
const [loading, setLoading] = useState(false);
const [biometricAvailable, setBiometricAvailable] = useState(false);
React.useEffect(() => {
checkBiometricAvailability();
loadLastEmail();
}, []);
const checkBiometricAvailability = async () => {
try {
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricAvailable(isSupported && isEnabled);
} catch (error) {
console.error("Biometric check failed:", error);
}
};
const loadLastEmail = async () => {
try {
const lastEmail = await StorageService.getLastLoginEmail();
if (lastEmail) {
setCredentials((prev) => ({ ...prev, email: lastEmail }));
}
} catch (error) {
console.error("Load last email failed:", error);
}
};
const handleLogin = async () => {
if (!credentials.email || !credentials.password) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
setLoading(true);
try {
const result = await AuthService.login(credentials);
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Anmeldung fehlgeschlagen", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setLoading(false);
}
};
const handleBiometricLogin = async () => {
try {
const result = await AuthService.biometricLogin();
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Fehler", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Anmeldung fehlgeschlagen.");
}
};
const navigateToRegister = () => {
navigation.navigate("RegisterScreen");
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={styles.content}>
<Text style={styles.title}>Anmelden</Text>
<View style={styles.formContainer}>
<TextInput
style={styles.input}
placeholder="E-Mail-Adresse"
value={credentials.email}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, email: text }))
}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
placeholder="Passwort"
value={credentials.password}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, password: text }))
}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>Anmelden</Text>
)}
</TouchableOpacity>
{biometricAvailable && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricLogin}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
<View style={styles.registerContainer}>
<Text style={styles.registerText}>Noch kein Konto? </Text>
<TouchableOpacity onPress={navigateToRegister}>
<Text style={styles.registerLink}>Registrieren</Text>
</TouchableOpacity>
</View>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
formContainer: {
width: "100%",
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
borderWidth: 1,
borderColor: "#ddd",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
buttonDisabled: {
backgroundColor: "#ccc",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#4CAF50",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 15,
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
registerContainer: {
flexDirection: "row",
justifyContent: "center",
marginTop: 30,
},
registerText: {
fontSize: 16,
color: "#666",
},
registerLink: {
fontSize: 16,
color: "#007AFF",
fontWeight: "600",
},
});
export default LoginScreen;

View File

@@ -0,0 +1,272 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RegisterCredentials, RootStackParamList } from "../types";
type RegisterScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"RegisterScreen"
>;
interface Props {
navigation: RegisterScreenNavigationProp;
}
const RegisterScreen: React.FC<Props> = ({ navigation }) => {
const [credentials, setCredentials] = useState<RegisterCredentials>({
email: "",
password: "",
firstName: "",
lastName: "",
});
const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleRegister = async () => {
if (
!credentials.email ||
!credentials.password ||
!credentials.firstName ||
!credentials.lastName
) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (credentials.password !== confirmPassword) {
Alert.alert("Fehler", "Die Passwörter stimmen nicht überein.");
return;
}
if (credentials.password.length < 6) {
Alert.alert(
"Fehler",
"Das Passwort muss mindestens 6 Zeichen lang sein."
);
return;
}
setLoading(true);
try {
const result = await AuthService.register(credentials);
if (result.success && result.user) {
// Ask user if they want to enable biometric authentication
const biometricSupported =
await BiometricsService.isBiometricSupported();
if (biometricSupported) {
Alert.alert(
"Biometrische Authentifizierung",
"Möchten Sie Face ID / Touch ID für zukünftige Anmeldungen aktivieren?",
[
{
text: "Später",
style: "cancel",
onPress: () =>
navigation.navigate("WelcomeScreen", { user: result.user! }),
},
{
text: "Aktivieren",
onPress: async () => {
const enabled = await AuthService.enableBiometricAuth();
if (enabled) {
Alert.alert(
"Erfolg",
"Biometrische Authentifizierung wurde aktiviert."
);
}
navigation.navigate("WelcomeScreen", { user: result.user! });
},
},
]
);
} else {
navigation.navigate("WelcomeScreen", { user: result.user });
}
} else {
Alert.alert("Registrierung fehlgeschlagen", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setLoading(false);
}
};
const navigateToLogin = () => {
navigation.navigate("LoginScreen");
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.content}>
<Text style={styles.title}>Registrieren</Text>
<View style={styles.formContainer}>
<TextInput
style={styles.input}
placeholder="Vorname"
value={credentials.firstName}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, firstName: text }))
}
autoCapitalize="words"
/>
<TextInput
style={styles.input}
placeholder="Nachname"
value={credentials.lastName}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, lastName: text }))
}
autoCapitalize="words"
/>
<TextInput
style={styles.input}
placeholder="E-Mail-Adresse"
value={credentials.email}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, email: text }))
}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
placeholder="Passwort"
value={credentials.password}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, password: text }))
}
secureTextEntry
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Passwort bestätigen"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>Registrieren</Text>
)}
</TouchableOpacity>
<View style={styles.loginContainer}>
<Text style={styles.loginText}>Bereits ein Konto? </Text>
<TouchableOpacity onPress={navigateToLogin}>
<Text style={styles.loginLink}>Anmelden</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
keyboardView: {
flex: 1,
},
scrollContainer: {
flexGrow: 1,
},
content: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
formContainer: {
width: "100%",
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
borderWidth: 1,
borderColor: "#ddd",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
buttonDisabled: {
backgroundColor: "#ccc",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
loginContainer: {
flexDirection: "row",
justifyContent: "center",
marginTop: 30,
},
loginText: {
fontSize: 16,
color: "#666",
},
loginLink: {
fontSize: 16,
color: "#007AFF",
fontWeight: "600",
},
});
export default RegisterScreen;

View File

@@ -0,0 +1,297 @@
import { RouteProp } from "@react-navigation/native";
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useEffect, useState } from "react";
import {
Alert,
SafeAreaView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RootStackParamList, User } from "../types";
import StorageService from "../utils/storage";
type WelcomeScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"WelcomeScreen"
>;
type WelcomeScreenRouteProp = RouteProp<RootStackParamList, "WelcomeScreen">;
interface Props {
navigation: WelcomeScreenNavigationProp;
route: WelcomeScreenRouteProp;
}
const WelcomeScreen: React.FC<Props> = ({ navigation, route }) => {
const [user, setUser] = useState<User | null>(null);
const [biometricSupported, setBiometricSupported] = useState(false);
const [biometricEnabled, setBiometricEnabled] = useState(false);
useEffect(() => {
initializeScreen();
}, []);
const initializeScreen = async () => {
try {
// Get user from params or current session
const currentUser =
route.params?.user || (await AuthService.getCurrentUser());
setUser(currentUser);
// Check biometric support and status
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricSupported(isSupported);
setBiometricEnabled(isEnabled);
} catch (error) {
console.error("Screen initialization failed:", error);
}
};
const handleLogout = async () => {
Alert.alert("Abmelden", "Möchten Sie sich wirklich abmelden?", [
{
text: "Abbrechen",
style: "cancel",
},
{
text: "Abmelden",
style: "destructive",
onPress: async () => {
try {
await AuthService.logout();
navigation.navigate("AuthScreen");
} catch (error) {
Alert.alert("Fehler", "Abmeldung fehlgeschlagen.");
}
},
},
]);
};
const toggleBiometric = async (value: boolean) => {
try {
if (value) {
const enabled = await AuthService.enableBiometricAuth();
if (enabled) {
setBiometricEnabled(true);
Alert.alert(
"Erfolg",
"Biometrische Authentifizierung wurde aktiviert."
);
} else {
Alert.alert(
"Fehler",
"Biometrische Authentifizierung konnte nicht aktiviert werden."
);
}
} else {
await AuthService.disableBiometricAuth();
setBiometricEnabled(false);
Alert.alert(
"Deaktiviert",
"Biometrische Authentifizierung wurde deaktiviert."
);
}
} catch (error) {
Alert.alert("Fehler", "Ein Fehler ist aufgetreten.");
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
};
if (!user) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.errorText}>
Benutzerinformationen konnten nicht geladen werden.
</Text>
<TouchableOpacity style={styles.button} onPress={handleLogout}>
<Text style={styles.buttonText}>Zurück zur Anmeldung</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.welcomeTitle}>Willkommen zurück!</Text>
<Text style={styles.userName}>
{user.firstName} {user.lastName}
</Text>
</View>
<View style={styles.userInfo}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>E-Mail:</Text>
<Text style={styles.infoValue}>{user.email}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Mitglied seit:</Text>
<Text style={styles.infoValue}>{formatDate(user.createdAt)}</Text>
</View>
</View>
{biometricSupported && (
<View style={styles.settingsSection}>
<Text style={styles.sectionTitle}>Einstellungen</Text>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>
Biometrische Authentifizierung
</Text>
<Switch
value={biometricEnabled}
onValueChange={toggleBiometric}
thumbColor={biometricEnabled ? "#007AFF" : "#f4f3f4"}
trackColor={{ false: "#767577", true: "#81b0ff" }}
/>
</View>
</View>
)}
<View style={styles.footer}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Abmelden</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
content: {
flex: 1,
padding: 20,
},
header: {
alignItems: "center",
marginBottom: 40,
paddingTop: 20,
},
welcomeTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
marginBottom: 10,
},
userName: {
fontSize: 20,
color: "#007AFF",
fontWeight: "600",
},
userInfo: {
backgroundColor: "white",
borderRadius: 10,
padding: 20,
marginBottom: 30,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 15,
},
infoLabel: {
fontSize: 16,
color: "#666",
fontWeight: "500",
},
infoValue: {
fontSize: 16,
color: "#333",
fontWeight: "400",
},
settingsSection: {
backgroundColor: "white",
borderRadius: 10,
padding: 20,
marginBottom: 30,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
sectionTitle: {
fontSize: 18,
fontWeight: "bold",
color: "#333",
marginBottom: 15,
},
settingRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
settingLabel: {
fontSize: 16,
color: "#333",
},
footer: {
flex: 1,
justifyContent: "flex-end",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
logoutButton: {
backgroundColor: "#FF3B30",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
logoutButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
errorText: {
fontSize: 16,
color: "#FF3B30",
textAlign: "center",
marginBottom: 20,
},
});
export default WelcomeScreen;

View File

@@ -0,0 +1,204 @@
import {
AuthResponse,
LoginCredentials,
RegisterCredentials,
User,
} from "../types";
import StorageService from "../utils/storage";
import BiometricsService from "./biometrics";
import DatabaseService from "./database";
class AuthService {
async initializeAuth(): Promise<void> {
try {
await DatabaseService.initDatabase();
} catch (error) {
console.error("Auth initialization failed:", error);
throw error;
}
}
async register(credentials: RegisterCredentials): Promise<AuthResponse> {
try {
// Check if user already exists
const existingUser = await DatabaseService.getUserByEmail(
credentials.email
);
if (existingUser) {
return {
success: false,
message: "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.",
};
}
// Create new user
const newUser = await DatabaseService.registerUser(credentials);
// Save user session
await StorageService.saveUserSession(newUser);
await StorageService.saveLastLoginEmail(credentials.email);
return {
success: true,
message: "Registrierung erfolgreich!",
user: newUser,
};
} catch (error) {
console.error("Registration failed:", error);
return {
success: false,
message: "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
};
}
}
async login(credentials: LoginCredentials): Promise<AuthResponse> {
try {
const user = await DatabaseService.loginUser(
credentials.email,
credentials.password
);
if (!user) {
return {
success: false,
message: "Ungültige E-Mail-Adresse oder Passwort.",
};
}
// Save user session
await StorageService.saveUserSession(user);
await StorageService.saveLastLoginEmail(credentials.email);
return {
success: true,
message: "Anmeldung erfolgreich!",
user,
};
} catch (error) {
console.error("Login failed:", error);
return {
success: false,
message: "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
};
}
}
async biometricLogin(): Promise<AuthResponse> {
try {
const isBiometricEnabled = await StorageService.isBiometricEnabled();
if (!isBiometricEnabled) {
return {
success: false,
message: "Biometrische Authentifizierung ist nicht aktiviert.",
};
}
const lastLoginEmail = await StorageService.getLastLoginEmail();
if (!lastLoginEmail) {
return {
success: false,
message: "Keine gespeicherte Anmeldeinformation gefunden.",
};
}
const isAuthenticated =
await BiometricsService.authenticateWithBiometrics();
if (!isAuthenticated) {
return {
success: false,
message: "Biometrische Authentifizierung fehlgeschlagen.",
};
}
const user = await DatabaseService.getUserByEmail(lastLoginEmail);
if (!user) {
return {
success: false,
message: "Benutzer nicht gefunden.",
};
}
await StorageService.saveUserSession(user);
return {
success: true,
message: "Biometrische Anmeldung erfolgreich!",
user,
};
} catch (error) {
console.error("Biometric login failed:", error);
return {
success: false,
message: "Biometrische Anmeldung fehlgeschlagen.",
};
}
}
async logout(): Promise<void> {
try {
await StorageService.clearUserSession();
await StorageService.clearAuthToken();
} catch (error) {
console.error("Logout failed:", error);
throw error;
}
}
async isLoggedIn(): Promise<boolean> {
try {
const session = await StorageService.getUserSession();
return session !== null;
} catch (error) {
console.error("Check login status failed:", error);
return false;
}
}
async getCurrentUser(): Promise<User | null> {
try {
const session = await StorageService.getUserSession();
return session ? session.user : null;
} catch (error) {
console.error("Get current user failed:", error);
return null;
}
}
async enableBiometricAuth(): Promise<boolean> {
try {
const isSupported = await BiometricsService.isBiometricSupported();
if (!isSupported) {
return false;
}
const isAuthenticated =
await BiometricsService.authenticateWithBiometrics();
if (isAuthenticated) {
await StorageService.setBiometricEnabled(true);
return true;
}
return false;
} catch (error) {
console.error("Enable biometric auth failed:", error);
return false;
}
}
async disableBiometricAuth(): Promise<void> {
try {
await StorageService.setBiometricEnabled(false);
} catch (error) {
console.error("Disable biometric auth failed:", error);
throw error;
}
}
}
export default new AuthService();

View File

@@ -0,0 +1,77 @@
import { Platform } from "react-native";
import TouchID from "react-native-touch-id";
class BiometricsService {
async isBiometricSupported(): Promise<boolean> {
try {
const biometryType = await TouchID.isSupported();
return biometryType !== false;
} catch (error) {
console.error("Biometric support check failed:", error);
return false;
}
}
async getBiometricType(): Promise<string | null> {
try {
const biometryType = await TouchID.isSupported();
return biometryType;
} catch (error) {
console.error("Get biometric type failed:", error);
return null;
}
}
async authenticateWithBiometrics(): Promise<boolean> {
try {
const biometryType = await this.getBiometricType();
if (!biometryType) {
throw new Error("Biometric authentication not supported");
}
const config = {
title: "Biometrische Authentifizierung",
subTitle: "Verwenden Sie Ihren Fingerabdruck oder Face ID",
color: "#007AFF",
imageColor: "#007AFF",
imageErrorColor: "#FF0000",
sensorDescription: "Berühren Sie den Sensor",
sensorErrorDescription: "Fehlgeschlagen",
cancelText: "Abbrechen",
fallbackLabel:
Platform.OS === "ios" ? "Passcode verwenden" : "PIN verwenden",
unifiedErrors: false,
passcodeFallback: true,
};
await TouchID.authenticate(
"Authentifizierung für sicheren Zugang",
config
);
return true;
} catch (error) {
console.error("Biometric authentication failed:", error);
return false;
}
}
async showBiometricPrompt(
reason: string = "Für sicheren Zugang authentifizieren"
): Promise<boolean> {
try {
const isSupported = await this.isBiometricSupported();
if (!isSupported) {
throw new Error("Biometric authentication not available");
}
return await this.authenticateWithBiometrics();
} catch (error) {
console.error("Biometric prompt failed:", error);
return false;
}
}
}
export default new BiometricsService();

View File

@@ -0,0 +1,138 @@
import SQLite from "react-native-sqlite-storage";
import { RegisterCredentials, User } from "../types";
SQLite.DEBUG(true);
SQLite.enablePromise(true);
class DatabaseService {
private db: SQLite.SQLiteDatabase | null = null;
async initDatabase(): Promise<void> {
try {
this.db = await SQLite.openDatabase({
name: "AuthApp.db",
location: "default",
});
await this.db.executeSql(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
createdAt TEXT NOT NULL
)
`);
console.log("Database initialized successfully");
} catch (error) {
console.error("Database initialization failed:", error);
throw error;
}
}
async registerUser(credentials: RegisterCredentials): Promise<User> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const createdAt = new Date().toISOString();
const result = await this.db.executeSql(
"INSERT INTO users (email, password, firstName, lastName, createdAt) VALUES (?, ?, ?, ?, ?)",
[
credentials.email,
credentials.password,
credentials.firstName,
credentials.lastName,
createdAt,
]
);
const userId = result[0].insertId;
return {
id: userId,
email: credentials.email,
password: credentials.password,
firstName: credentials.firstName,
lastName: credentials.lastName,
createdAt,
};
} catch (error) {
console.error("User registration failed:", error);
throw error;
}
}
async loginUser(email: string, password: string): Promise<User | null> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.executeSql(
"SELECT * FROM users WHERE email = ? AND password = ?",
[email, password]
);
if (result[0].rows.length > 0) {
const userData = result[0].rows.item(0);
return {
id: userData.id,
email: userData.email,
password: userData.password,
firstName: userData.firstName,
lastName: userData.lastName,
createdAt: userData.createdAt,
};
}
return null;
} catch (error) {
console.error("User login failed:", error);
throw error;
}
}
async getUserByEmail(email: string): Promise<User | null> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.executeSql(
"SELECT * FROM users WHERE email = ?",
[email]
);
if (result[0].rows.length > 0) {
const userData = result[0].rows.item(0);
return {
id: userData.id,
email: userData.email,
password: userData.password,
firstName: userData.firstName,
lastName: userData.lastName,
createdAt: userData.createdAt,
};
}
return null;
} catch (error) {
console.error("Get user by email failed:", error);
throw error;
}
}
async closeDatabase(): Promise<void> {
if (this.db) {
await this.db.close();
this.db = null;
}
}
}
export default new DatabaseService();

View File

@@ -0,0 +1,40 @@
export interface User {
id: number;
email: string;
password: string;
firstName: string;
lastName: string;
createdAt: string;
}
export interface AuthState {
isAuthenticated: boolean;
user: User | null;
loading: boolean;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterCredentials {
email: string;
password: string;
firstName: string;
lastName: string;
}
export interface AuthResponse {
success: boolean;
message: string;
user?: User;
}
export type RootStackParamList = {
AuthScreen: undefined;
LoginScreen: undefined;
RegisterScreen: undefined;
WelcomeScreen: { user: User };
MainApp: undefined;
};

View File

@@ -0,0 +1,122 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { User } from "../types";
const STORAGE_KEYS = {
USER_SESSION: "@user_session",
AUTH_TOKEN: "@auth_token",
BIOMETRIC_ENABLED: "@biometric_enabled",
LAST_LOGIN_EMAIL: "@last_login_email",
};
class StorageService {
async saveUserSession(user: User): Promise<void> {
try {
const sessionData = {
user,
loginTime: new Date().toISOString(),
};
const jsonValue = JSON.stringify(sessionData);
await AsyncStorage.setItem(STORAGE_KEYS.USER_SESSION, jsonValue);
} catch (error) {
console.error("Failed to save user session:", error);
throw error;
}
}
async getUserSession(): Promise<{ user: User; loginTime: string } | null> {
try {
const jsonValue = await AsyncStorage.getItem(STORAGE_KEYS.USER_SESSION);
return jsonValue ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error("Failed to retrieve user session:", error);
return null;
}
}
async clearUserSession(): Promise<void> {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.USER_SESSION);
} catch (error) {
console.error("Failed to clear user session:", error);
throw error;
}
}
async saveAuthToken(token: string): Promise<void> {
try {
await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token);
} catch (error) {
console.error("Failed to save auth token:", error);
throw error;
}
}
async getAuthToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
} catch (error) {
console.error("Failed to retrieve auth token:", error);
return null;
}
}
async clearAuthToken(): Promise<void> {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
} catch (error) {
console.error("Failed to clear auth token:", error);
throw error;
}
}
async setBiometricEnabled(enabled: boolean): Promise<void> {
try {
await AsyncStorage.setItem(
STORAGE_KEYS.BIOMETRIC_ENABLED,
JSON.stringify(enabled)
);
} catch (error) {
console.error("Failed to save biometric setting:", error);
throw error;
}
}
async isBiometricEnabled(): Promise<boolean> {
try {
const value = await AsyncStorage.getItem(STORAGE_KEYS.BIOMETRIC_ENABLED);
return value ? JSON.parse(value) : false;
} catch (error) {
console.error("Failed to retrieve biometric setting:", error);
return false;
}
}
async saveLastLoginEmail(email: string): Promise<void> {
try {
await AsyncStorage.setItem(STORAGE_KEYS.LAST_LOGIN_EMAIL, email);
} catch (error) {
console.error("Failed to save last login email:", error);
throw error;
}
}
async getLastLoginEmail(): Promise<string | null> {
try {
return await AsyncStorage.getItem(STORAGE_KEYS.LAST_LOGIN_EMAIL);
} catch (error) {
console.error("Failed to retrieve last login email:", error);
return null;
}
}
async clearAllData(): Promise<void> {
try {
await AsyncStorage.multiRemove(Object.values(STORAGE_KEYS));
} catch (error) {
console.error("Failed to clear all storage data:", error);
throw error;
}
}
}
export default new StorageService();

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es6",
"dom"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-native"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
],
"extends": "expo/tsconfig.base"
}

0
auth-app/App.tsx Normal file
View File

0
auth-app/README.md Normal file
View File

0
auth-app/package.json Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

135
components/CalendarSync.tsx Normal file
View File

@@ -0,0 +1,135 @@
import { Picker } from "@react-native-picker/picker";
import * as Calendar from "expo-calendar";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
const db = SQLite.openDatabaseSync("events.db");
export interface EventItem {
id: string;
title: string;
startDate: string;
endDate: string;
}
const CalendarSync: React.FC = () => {
const [events, setEvents] = useState<EventItem[]>([]);
const [selectedYear, setSelectedYear] = useState<number>(
new Date().getFullYear()
);
// Initialisierung: SQLite laden, dann Kalender synchronisieren
useEffect(() => {
initDB();
loadEventsFromDB();
syncCalendarEvents(selectedYear);
}, [selectedYear]);
// Erstellt die Tabelle, falls nicht vorhanden
const initDB = async () => {
await db.execAsync(
"CREATE TABLE IF NOT EXISTS events (id TEXT PRIMARY KEY, title TEXT, startDate TEXT, endDate TEXT);"
);
};
const loadEventsFromDB = async () => {
const stmt = await db.prepareAsync("SELECT * FROM events;");
const loadedEvents: EventItem[] = [];
const result = await stmt.executeAsync([]);
for await (const row of result) {
loadedEvents.push(row as EventItem);
}
setEvents(loadedEvents);
await stmt.finalizeAsync();
};
// Synchronisiert Kalender-Events und speichert sie in SQLite
const syncCalendarEvents = async (year: number) => {
const perm = await Calendar.requestCalendarPermissionsAsync();
if (!perm.granted) return;
const calendars = await Calendar.getCalendarsAsync();
const start = new Date(year, 0, 1);
const end = new Date(year, 11, 31, 23, 59, 59);
let allEvents: EventItem[] = [];
for (const cal of calendars) {
const calEvents = await Calendar.getEventsAsync([cal.id], start, end);
allEvents = allEvents.concat(
calEvents.map((e) => ({
id: e.id,
title: e.title || "(Kein Titel)",
startDate:
typeof e.startDate === "string"
? e.startDate
: new Date(e.startDate).toISOString(),
endDate:
typeof e.endDate === "string"
? e.endDate
: new Date(e.endDate).toISOString(),
}))
);
}
// Speichern in SQLite
await db.execAsync("DELETE FROM events;");
for (const ev of allEvents) {
await db.execAsync(
`INSERT OR REPLACE INTO events (id, title, startDate, endDate) VALUES ('${
ev.id
}', '${ev.title.replace(/'/g, "''")}', '${ev.startDate}', '${
ev.endDate
}');`
);
}
await loadEventsFromDB();
};
// UI
const renderItem = ({ item }: { item: EventItem }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.time}>
{new Date(item.startDate).toLocaleString()} -{" "}
{new Date(item.endDate).toLocaleString()}
</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>Kalender Events ({selectedYear})</Text>
<Picker
selectedValue={selectedYear}
style={{ height: 50, width: 180, marginBottom: 16 }}
onValueChange={(year) => setSelectedYear(year)}
>
{[...Array(10)].map((_, i) => {
const year = new Date().getFullYear() - 5 + i;
return (
<Picker.Item key={year} label={year.toString()} value={year} />
);
})}
</Picker>
<FlatList
data={events}
keyExtractor={(item) => item.id}
renderItem={renderItem}
ListEmptyComponent={<Text>Keine Events gefunden.</Text>}
/>
</View>
);
};
export default CalendarSync;
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: "#fff" },
header: { fontSize: 20, fontWeight: "bold", marginBottom: 16 },
item: {
marginBottom: 12,
padding: 12,
borderRadius: 8,
backgroundColor: "#f2f2f2",
},
title: { fontSize: 16, fontWeight: "600" },
time: { fontSize: 14, color: "#555" },
});

View File

@@ -0,0 +1,195 @@
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
Button,
FlatList,
Modal,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
const db = SQLite.openDatabaseSync("reminders.db");
export interface Reminder {
id: string;
title: string;
description: string;
date: string;
}
const RemindersList: React.FC = () => {
const [reminders, setReminders] = useState<Reminder[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [date, setDate] = useState("");
useEffect(() => {
initDB();
loadReminders();
}, []);
const initDB = async () => {
await db.execAsync(
"CREATE TABLE IF NOT EXISTS reminders (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, date TEXT);"
);
};
const loadReminders = async () => {
const stmt = await db.prepareAsync(
"SELECT * FROM reminders ORDER BY date ASC;"
);
const loaded: Reminder[] = [];
const result = await stmt.executeAsync([]);
for await (const row of result) {
loaded.push(row as Reminder);
}
setReminders(loaded);
await stmt.finalizeAsync();
};
const saveReminder = async () => {
if (!title || !date) return;
await db.execAsync(
`INSERT INTO reminders (title, description, date) VALUES ('${title.replace(/'/g, "''")}', '${description.replace(/'/g, "''")}', '${date}');`
);
setTitle("");
setDescription("");
setDate("");
setModalVisible(false);
await loadReminders();
};
const renderItem = ({ item }: { item: Reminder }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.desc}>{item.description}</Text>
<Text style={styles.date}>{new Date(item.date).toLocaleString()}</Text>
</View>
);
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.addButton}
onPress={() => setModalVisible(true)}
>
<Text style={styles.addButtonText}></Text>
</TouchableOpacity>
<FlatList
data={reminders}
keyExtractor={(item) => item.id.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text style={{ textAlign: "center" }}>Keine Erinnerungen.</Text>
}
/>
<Modal
visible={modalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Neue Erinnerung</Text>
<TextInput
style={styles.input}
placeholder="Titel"
value={title}
onChangeText={setTitle}
/>
<TextInput
style={styles.input}
placeholder="Beschreibung"
value={description}
onChangeText={setDescription}
/>
<TextInput
style={styles.input}
placeholder="Datum (YYYY-MM-DD HH:mm)"
value={date}
onChangeText={setDate}
/>
<View style={styles.modalButtons}>
<Button
title="Abbrechen"
onPress={() => setModalVisible(false)}
/>
<Button title="Speichern" onPress={saveReminder} />
</View>
</View>
</View>
</Modal>
</View>
);
};
export default RemindersList;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff" },
addButton: {
position: "absolute",
top: Platform.OS === "ios" ? 50 : 20,
right: 30,
zIndex: 10,
backgroundColor: "#007AFF",
width: 48,
height: 48,
borderRadius: 24,
alignItems: "center",
justifyContent: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4,
},
addButtonText: { color: "#fff", fontSize: 32, fontWeight: "bold" },
item: {
margin: 12,
padding: 16,
borderRadius: 8,
backgroundColor: "#f2f2f2",
},
title: { fontSize: 16, fontWeight: "600" },
desc: { fontSize: 14, color: "#555", marginTop: 4 },
date: { fontSize: 13, color: "#888", marginTop: 8 },
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.2)",
justifyContent: "center",
alignItems: "center",
},
modalContent: {
width: 320,
backgroundColor: "#fff",
borderRadius: 12,
padding: 20,
elevation: 8,
},
modalTitle: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 16,
textAlign: "center",
},
input: {
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 8,
padding: 10,
marginBottom: 12,
fontSize: 16,
},
modalButtons: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 10,
},
});

4066
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,18 +12,24 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-picker/picker": "2.11.1",
"@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/stack": "^7.1.1",
"expo": "~53.0.17", "expo": "~53.0.17",
"expo-blur": "~14.1.5", "expo-blur": "~14.1.5",
"expo-calendar": "~14.1.4",
"expo-constants": "~17.1.7", "expo-constants": "~17.1.7",
"expo-font": "~13.3.2", "expo-font": "~13.3.2",
"expo-haptics": "~14.1.4", "expo-haptics": "~14.1.4",
"expo-image": "~2.3.2", "expo-image": "~2.3.2",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-local-authentication": "~16.0.5",
"expo-router": "~5.1.3", "expo-router": "~5.1.3",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-sqlite": "~15.2.14",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5", "expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.10", "expo-system-ui": "~5.0.10",
@@ -41,9 +47,10 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"typescript": "~5.8.3",
"eslint": "^9.25.0", "eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0" "eslint-config-expo": "~9.2.0",
"expo-module-scripts": "^4.1.9",
"typescript": "~5.8.3"
}, },
"private": true "private": true
} }

172
src/screens/AuthScreen.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useEffect, useState } from "react";
import {
Alert,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RootStackParamList } from "../types";
import StorageService from "../utils/storage";
type AuthScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"AuthScreen"
>;
interface Props {
navigation: AuthScreenNavigationProp;
}
const AuthScreen: React.FC<Props> = ({ navigation }) => {
const [biometricSupported, setBiometricSupported] = useState(false);
const [biometricEnabled, setBiometricEnabled] = useState(false);
useEffect(() => {
checkBiometricSupport();
}, []);
const checkBiometricSupport = async () => {
try {
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricSupported(isSupported);
setBiometricEnabled(isEnabled);
} catch (error) {
console.error("Biometric check failed:", error);
}
};
const handleBiometricLogin = async () => {
try {
const result = await AuthService.biometricLogin();
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Fehler", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Anmeldung fehlgeschlagen.");
}
};
const navigateToLogin = () => {
navigation.navigate("LoginScreen");
};
const navigateToRegister = () => {
navigation.navigate("RegisterScreen");
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Willkommen</Text>
<Text style={styles.subtitle}>
Melden Sie sich an oder erstellen Sie ein neues Konto
</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryButton}
onPress={navigateToLogin}
>
<Text style={styles.primaryButtonText}>Anmelden</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryButton}
onPress={navigateToRegister}
>
<Text style={styles.secondaryButtonText}>Registrieren</Text>
</TouchableOpacity>
{biometricSupported && biometricEnabled && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricLogin}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
content: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 20,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#333",
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: "#666",
textAlign: "center",
marginBottom: 40,
},
buttonContainer: {
width: "100%",
maxWidth: 300,
},
primaryButton: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
marginBottom: 15,
alignItems: "center",
},
primaryButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
secondaryButton: {
backgroundColor: "transparent",
paddingVertical: 15,
borderRadius: 10,
borderWidth: 2,
borderColor: "#007AFF",
marginBottom: 15,
alignItems: "center",
},
secondaryButtonText: {
color: "#007AFF",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#4CAF50",
paddingVertical: 15,
borderRadius: 10,
marginTop: 20,
alignItems: "center",
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
});
export default AuthScreen;

248
src/screens/LoginScreen.tsx Normal file
View File

@@ -0,0 +1,248 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { LoginCredentials, RootStackParamList } from "../types";
import StorageService from "../utils/storage";
type LoginScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"LoginScreen"
>;
interface Props {
navigation: LoginScreenNavigationProp;
}
const LoginScreen: React.FC<Props> = ({ navigation }) => {
const [credentials, setCredentials] = useState<LoginCredentials>({
email: "",
password: "",
});
const [loading, setLoading] = useState(false);
const [biometricAvailable, setBiometricAvailable] = useState(false);
React.useEffect(() => {
checkBiometricAvailability();
loadLastEmail();
}, []);
const checkBiometricAvailability = async () => {
try {
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricAvailable(isSupported && isEnabled);
} catch (error) {
console.error("Biometric check failed:", error);
}
};
const loadLastEmail = async () => {
try {
const lastEmail = await StorageService.getLastLoginEmail();
if (lastEmail) {
setCredentials((prev) => ({ ...prev, email: lastEmail }));
}
} catch (error) {
console.error("Load last email failed:", error);
}
};
const handleLogin = async () => {
if (!credentials.email || !credentials.password) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
setLoading(true);
try {
const result = await AuthService.login(credentials);
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Anmeldung fehlgeschlagen", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setLoading(false);
}
};
const handleBiometricLogin = async () => {
try {
const result = await AuthService.biometricLogin();
if (result.success && result.user) {
navigation.navigate("WelcomeScreen", { user: result.user });
} else {
Alert.alert("Fehler", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Biometrische Anmeldung fehlgeschlagen.");
}
};
const navigateToRegister = () => {
navigation.navigate("RegisterScreen");
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={styles.content}>
<Text style={styles.title}>Anmelden</Text>
<View style={styles.formContainer}>
<TextInput
style={styles.input}
placeholder="E-Mail-Adresse"
value={credentials.email}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, email: text }))
}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
placeholder="Passwort"
value={credentials.password}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, password: text }))
}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>Anmelden</Text>
)}
</TouchableOpacity>
{biometricAvailable && (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricLogin}
>
<Text style={styles.biometricButtonText}>
Mit Face ID / Touch ID anmelden
</Text>
</TouchableOpacity>
)}
<View style={styles.registerContainer}>
<Text style={styles.registerText}>Noch kein Konto? </Text>
<TouchableOpacity onPress={navigateToRegister}>
<Text style={styles.registerLink}>Registrieren</Text>
</TouchableOpacity>
</View>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
formContainer: {
width: "100%",
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
borderWidth: 1,
borderColor: "#ddd",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
buttonDisabled: {
backgroundColor: "#ccc",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
biometricButton: {
backgroundColor: "#4CAF50",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 15,
},
biometricButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
registerContainer: {
flexDirection: "row",
justifyContent: "center",
marginTop: 30,
},
registerText: {
fontSize: 16,
color: "#666",
},
registerLink: {
fontSize: 16,
color: "#007AFF",
fontWeight: "600",
},
});
export default LoginScreen;

View File

@@ -0,0 +1,272 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RegisterCredentials, RootStackParamList } from "../types";
type RegisterScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"RegisterScreen"
>;
interface Props {
navigation: RegisterScreenNavigationProp;
}
const RegisterScreen: React.FC<Props> = ({ navigation }) => {
const [credentials, setCredentials] = useState<RegisterCredentials>({
email: "",
password: "",
firstName: "",
lastName: "",
});
const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleRegister = async () => {
if (
!credentials.email ||
!credentials.password ||
!credentials.firstName ||
!credentials.lastName
) {
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
return;
}
if (credentials.password !== confirmPassword) {
Alert.alert("Fehler", "Die Passwörter stimmen nicht überein.");
return;
}
if (credentials.password.length < 6) {
Alert.alert(
"Fehler",
"Das Passwort muss mindestens 6 Zeichen lang sein."
);
return;
}
setLoading(true);
try {
const result = await AuthService.register(credentials);
if (result.success && result.user) {
// Ask user if they want to enable biometric authentication
const biometricSupported =
await BiometricsService.isBiometricSupported();
if (biometricSupported) {
Alert.alert(
"Biometrische Authentifizierung",
"Möchten Sie Face ID / Touch ID für zukünftige Anmeldungen aktivieren?",
[
{
text: "Später",
style: "cancel",
onPress: () =>
navigation.navigate("WelcomeScreen", { user: result.user! }),
},
{
text: "Aktivieren",
onPress: async () => {
const enabled = await AuthService.enableBiometricAuth();
if (enabled) {
Alert.alert(
"Erfolg",
"Biometrische Authentifizierung wurde aktiviert."
);
}
navigation.navigate("WelcomeScreen", { user: result.user! });
},
},
]
);
} else {
navigation.navigate("WelcomeScreen", { user: result.user });
}
} else {
Alert.alert("Registrierung fehlgeschlagen", result.message);
}
} catch (error) {
Alert.alert("Fehler", "Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setLoading(false);
}
};
const navigateToLogin = () => {
navigation.navigate("LoginScreen");
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.content}>
<Text style={styles.title}>Registrieren</Text>
<View style={styles.formContainer}>
<TextInput
style={styles.input}
placeholder="Vorname"
value={credentials.firstName}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, firstName: text }))
}
autoCapitalize="words"
/>
<TextInput
style={styles.input}
placeholder="Nachname"
value={credentials.lastName}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, lastName: text }))
}
autoCapitalize="words"
/>
<TextInput
style={styles.input}
placeholder="E-Mail-Adresse"
value={credentials.email}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, email: text }))
}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
placeholder="Passwort"
value={credentials.password}
onChangeText={(text) =>
setCredentials((prev) => ({ ...prev, password: text }))
}
secureTextEntry
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Passwort bestätigen"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
autoCapitalize="none"
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>Registrieren</Text>
)}
</TouchableOpacity>
<View style={styles.loginContainer}>
<Text style={styles.loginText}>Bereits ein Konto? </Text>
<TouchableOpacity onPress={navigateToLogin}>
<Text style={styles.loginLink}>Anmelden</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
keyboardView: {
flex: 1,
},
scrollContainer: {
flexGrow: 1,
},
content: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
textAlign: "center",
marginBottom: 40,
},
formContainer: {
width: "100%",
},
input: {
backgroundColor: "white",
paddingHorizontal: 15,
paddingVertical: 15,
borderRadius: 10,
fontSize: 16,
marginBottom: 15,
borderWidth: 1,
borderColor: "#ddd",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
marginTop: 10,
},
buttonDisabled: {
backgroundColor: "#ccc",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
loginContainer: {
flexDirection: "row",
justifyContent: "center",
marginTop: 30,
},
loginText: {
fontSize: 16,
color: "#666",
},
loginLink: {
fontSize: 16,
color: "#007AFF",
fontWeight: "600",
},
});
export default RegisterScreen;

View File

@@ -0,0 +1,297 @@
import { RouteProp } from "@react-navigation/native";
import { StackNavigationProp } from "@react-navigation/stack";
import React, { useEffect, useState } from "react";
import {
Alert,
SafeAreaView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from "react-native";
import AuthService from "../services/auth";
import BiometricsService from "../services/biometrics";
import { RootStackParamList, User } from "../types";
import StorageService from "../utils/storage";
type WelcomeScreenNavigationProp = StackNavigationProp<
RootStackParamList,
"WelcomeScreen"
>;
type WelcomeScreenRouteProp = RouteProp<RootStackParamList, "WelcomeScreen">;
interface Props {
navigation: WelcomeScreenNavigationProp;
route: WelcomeScreenRouteProp;
}
const WelcomeScreen: React.FC<Props> = ({ navigation, route }) => {
const [user, setUser] = useState<User | null>(null);
const [biometricSupported, setBiometricSupported] = useState(false);
const [biometricEnabled, setBiometricEnabled] = useState(false);
useEffect(() => {
initializeScreen();
}, []);
const initializeScreen = async () => {
try {
// Get user from params or current session
const currentUser =
route.params?.user || (await AuthService.getCurrentUser());
setUser(currentUser);
// Check biometric support and status
const isSupported = await BiometricsService.isBiometricSupported();
const isEnabled = await StorageService.isBiometricEnabled();
setBiometricSupported(isSupported);
setBiometricEnabled(isEnabled);
} catch (error) {
console.error("Screen initialization failed:", error);
}
};
const handleLogout = async () => {
Alert.alert("Abmelden", "Möchten Sie sich wirklich abmelden?", [
{
text: "Abbrechen",
style: "cancel",
},
{
text: "Abmelden",
style: "destructive",
onPress: async () => {
try {
await AuthService.logout();
navigation.navigate("AuthScreen");
} catch (error) {
Alert.alert("Fehler", "Abmeldung fehlgeschlagen.");
}
},
},
]);
};
const toggleBiometric = async (value: boolean) => {
try {
if (value) {
const enabled = await AuthService.enableBiometricAuth();
if (enabled) {
setBiometricEnabled(true);
Alert.alert(
"Erfolg",
"Biometrische Authentifizierung wurde aktiviert."
);
} else {
Alert.alert(
"Fehler",
"Biometrische Authentifizierung konnte nicht aktiviert werden."
);
}
} else {
await AuthService.disableBiometricAuth();
setBiometricEnabled(false);
Alert.alert(
"Deaktiviert",
"Biometrische Authentifizierung wurde deaktiviert."
);
}
} catch (error) {
Alert.alert("Fehler", "Ein Fehler ist aufgetreten.");
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
};
if (!user) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.errorText}>
Benutzerinformationen konnten nicht geladen werden.
</Text>
<TouchableOpacity style={styles.button} onPress={handleLogout}>
<Text style={styles.buttonText}>Zurück zur Anmeldung</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.welcomeTitle}>Willkommen zurück!</Text>
<Text style={styles.userName}>
{user.firstName} {user.lastName}
</Text>
</View>
<View style={styles.userInfo}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>E-Mail:</Text>
<Text style={styles.infoValue}>{user.email}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Mitglied seit:</Text>
<Text style={styles.infoValue}>{formatDate(user.createdAt)}</Text>
</View>
</View>
{biometricSupported && (
<View style={styles.settingsSection}>
<Text style={styles.sectionTitle}>Einstellungen</Text>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>
Biometrische Authentifizierung
</Text>
<Switch
value={biometricEnabled}
onValueChange={toggleBiometric}
thumbColor={biometricEnabled ? "#007AFF" : "#f4f3f4"}
trackColor={{ false: "#767577", true: "#81b0ff" }}
/>
</View>
</View>
)}
<View style={styles.footer}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Abmelden</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
content: {
flex: 1,
padding: 20,
},
header: {
alignItems: "center",
marginBottom: 40,
paddingTop: 20,
},
welcomeTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#333",
marginBottom: 10,
},
userName: {
fontSize: 20,
color: "#007AFF",
fontWeight: "600",
},
userInfo: {
backgroundColor: "white",
borderRadius: 10,
padding: 20,
marginBottom: 30,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 15,
},
infoLabel: {
fontSize: 16,
color: "#666",
fontWeight: "500",
},
infoValue: {
fontSize: 16,
color: "#333",
fontWeight: "400",
},
settingsSection: {
backgroundColor: "white",
borderRadius: 10,
padding: 20,
marginBottom: 30,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
sectionTitle: {
fontSize: 18,
fontWeight: "bold",
color: "#333",
marginBottom: 15,
},
settingRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
settingLabel: {
fontSize: 16,
color: "#333",
},
footer: {
flex: 1,
justifyContent: "flex-end",
},
button: {
backgroundColor: "#007AFF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
buttonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
logoutButton: {
backgroundColor: "#FF3B30",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
},
logoutButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
errorText: {
fontSize: 16,
color: "#FF3B30",
textAlign: "center",
marginBottom: 20,
},
});
export default WelcomeScreen;

204
src/services/auth.ts Normal file
View File

@@ -0,0 +1,204 @@
import {
AuthResponse,
LoginCredentials,
RegisterCredentials,
User,
} from "../types";
import StorageService from "../utils/storage";
import BiometricsService from "./biometrics";
import DatabaseService from "./database";
class AuthService {
async initializeAuth(): Promise<void> {
try {
await DatabaseService.initDatabase();
} catch (error) {
console.error("Auth initialization failed:", error);
throw error;
}
}
async register(credentials: RegisterCredentials): Promise<AuthResponse> {
try {
// Check if user already exists
const existingUser = await DatabaseService.getUserByEmail(
credentials.email
);
if (existingUser) {
return {
success: false,
message: "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.",
};
}
// Create new user
const newUser = await DatabaseService.registerUser(credentials);
// Save user session
await StorageService.saveUserSession(newUser);
await StorageService.saveLastLoginEmail(credentials.email);
return {
success: true,
message: "Registrierung erfolgreich!",
user: newUser,
};
} catch (error) {
console.error("Registration failed:", error);
return {
success: false,
message: "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
};
}
}
async login(credentials: LoginCredentials): Promise<AuthResponse> {
try {
const user = await DatabaseService.loginUser(
credentials.email,
credentials.password
);
if (!user) {
return {
success: false,
message: "Ungültige E-Mail-Adresse oder Passwort.",
};
}
// Save user session
await StorageService.saveUserSession(user);
await StorageService.saveLastLoginEmail(credentials.email);
return {
success: true,
message: "Anmeldung erfolgreich!",
user,
};
} catch (error) {
console.error("Login failed:", error);
return {
success: false,
message: "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
};
}
}
async biometricLogin(): Promise<AuthResponse> {
try {
const isBiometricEnabled = await StorageService.isBiometricEnabled();
if (!isBiometricEnabled) {
return {
success: false,
message: "Biometrische Authentifizierung ist nicht aktiviert.",
};
}
const lastLoginEmail = await StorageService.getLastLoginEmail();
if (!lastLoginEmail) {
return {
success: false,
message: "Keine gespeicherte Anmeldeinformation gefunden.",
};
}
const isAuthenticated =
await BiometricsService.authenticateWithBiometrics();
if (!isAuthenticated) {
return {
success: false,
message: "Biometrische Authentifizierung fehlgeschlagen.",
};
}
const user = await DatabaseService.getUserByEmail(lastLoginEmail);
if (!user) {
return {
success: false,
message: "Benutzer nicht gefunden.",
};
}
await StorageService.saveUserSession(user);
return {
success: true,
message: "Biometrische Anmeldung erfolgreich!",
user,
};
} catch (error) {
console.error("Biometric login failed:", error);
return {
success: false,
message: "Biometrische Anmeldung fehlgeschlagen.",
};
}
}
async logout(): Promise<void> {
try {
await StorageService.clearUserSession();
await StorageService.clearAuthToken();
} catch (error) {
console.error("Logout failed:", error);
throw error;
}
}
async isLoggedIn(): Promise<boolean> {
try {
const session = await StorageService.getUserSession();
return session !== null;
} catch (error) {
console.error("Check login status failed:", error);
return false;
}
}
async getCurrentUser(): Promise<User | null> {
try {
const session = await StorageService.getUserSession();
return session ? session.user : null;
} catch (error) {
console.error("Get current user failed:", error);
return null;
}
}
async enableBiometricAuth(): Promise<boolean> {
try {
const isSupported = await BiometricsService.isBiometricSupported();
if (!isSupported) {
return false;
}
const isAuthenticated =
await BiometricsService.authenticateWithBiometrics();
if (isAuthenticated) {
await StorageService.setBiometricEnabled(true);
return true;
}
return false;
} catch (error) {
console.error("Enable biometric auth failed:", error);
return false;
}
}
async disableBiometricAuth(): Promise<void> {
try {
await StorageService.setBiometricEnabled(false);
} catch (error) {
console.error("Disable biometric auth failed:", error);
throw error;
}
}
}
export default new AuthService();

View File

@@ -0,0 +1,66 @@
import * as LocalAuthentication from "expo-local-authentication";
class BiometricsService {
async isBiometricSupported(): Promise<boolean> {
try {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
return compatible && enrolled;
} catch (error) {
console.error("Biometric support check failed:", error);
return false;
}
}
async getBiometricType(): Promise<LocalAuthentication.AuthenticationType[]> {
try {
const types =
await LocalAuthentication.supportedAuthenticationTypesAsync();
return types;
} catch (error) {
console.error("Get biometric type failed:", error);
return [];
}
}
async authenticateWithBiometrics(): Promise<boolean> {
try {
const biometryType = await this.isBiometricSupported();
if (!biometryType) {
throw new Error("Biometric authentication not supported");
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Biometrische Authentifizierung",
cancelLabel: "Abbrechen",
fallbackLabel: "Passcode verwenden",
requireConfirmation: true,
});
return result.success;
} catch (error) {
console.error("Biometric authentication failed:", error);
return false;
}
}
async showBiometricPrompt(
reason: string = "Für sicheren Zugang authentifizieren"
): Promise<boolean> {
try {
const isSupported = await this.isBiometricSupported();
if (!isSupported) {
throw new Error("Biometric authentication not available");
}
return await this.authenticateWithBiometrics();
} catch (error) {
console.error("Biometric prompt failed:", error);
return false;
}
}
}
export default new BiometricsService();

130
src/services/database.ts Normal file
View File

@@ -0,0 +1,130 @@
import * as SQLite from "expo-sqlite";
import { RegisterCredentials, User } from "../types";
class DatabaseService {
private db: SQLite.SQLiteDatabase | null = null;
async initDatabase(): Promise<void> {
try {
this.db = await SQLite.openDatabaseAsync("AuthApp.db");
await this.db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
createdAt TEXT NOT NULL
)
`);
console.log("Database initialized successfully");
} catch (error) {
console.error("Database initialization failed:", error);
throw error;
}
}
async registerUser(credentials: RegisterCredentials): Promise<User> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const createdAt = new Date().toISOString();
const result = await this.db.runAsync(
"INSERT INTO users (email, password, firstName, lastName, createdAt) VALUES (?, ?, ?, ?, ?)",
[
credentials.email,
credentials.password,
credentials.firstName,
credentials.lastName,
createdAt,
]
);
const userId = result.lastInsertRowId;
return {
id: userId,
email: credentials.email,
password: credentials.password,
firstName: credentials.firstName,
lastName: credentials.lastName,
createdAt,
};
} catch (error) {
console.error("User registration failed:", error);
throw error;
}
}
async loginUser(email: string, password: string): Promise<User | null> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = (await this.db.getFirstAsync(
"SELECT * FROM users WHERE email = ? AND password = ?",
[email, password]
)) as any;
if (result) {
return {
id: result.id,
email: result.email,
password: result.password,
firstName: result.firstName,
lastName: result.lastName,
createdAt: result.createdAt,
};
}
return null;
} catch (error) {
console.error("User login failed:", error);
throw error;
}
}
async getUserByEmail(email: string): Promise<User | null> {
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = (await this.db.getFirstAsync(
"SELECT * FROM users WHERE email = ?",
[email]
)) as any;
if (result) {
return {
id: result.id,
email: result.email,
password: result.password,
firstName: result.firstName,
lastName: result.lastName,
createdAt: result.createdAt,
};
}
return null;
} catch (error) {
console.error("Get user by email failed:", error);
throw error;
}
}
async closeDatabase(): Promise<void> {
if (this.db) {
await this.db.closeAsync();
this.db = null;
}
}
}
export default new DatabaseService();

40
src/types/index.ts Normal file
View File

@@ -0,0 +1,40 @@
export interface User {
id: number;
email: string;
password: string;
firstName: string;
lastName: string;
createdAt: string;
}
export interface AuthState {
isAuthenticated: boolean;
user: User | null;
loading: boolean;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterCredentials {
email: string;
password: string;
firstName: string;
lastName: string;
}
export interface AuthResponse {
success: boolean;
message: string;
user?: User;
}
export type RootStackParamList = {
AuthScreen: undefined;
LoginScreen: undefined;
RegisterScreen: undefined;
WelcomeScreen: { user: User };
MainApp: undefined;
};

122
src/utils/storage.ts Normal file
View File

@@ -0,0 +1,122 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { User } from "../types";
const STORAGE_KEYS = {
USER_SESSION: "@user_session",
AUTH_TOKEN: "@auth_token",
BIOMETRIC_ENABLED: "@biometric_enabled",
LAST_LOGIN_EMAIL: "@last_login_email",
};
class StorageService {
async saveUserSession(user: User): Promise<void> {
try {
const sessionData = {
user,
loginTime: new Date().toISOString(),
};
const jsonValue = JSON.stringify(sessionData);
await AsyncStorage.setItem(STORAGE_KEYS.USER_SESSION, jsonValue);
} catch (error) {
console.error("Failed to save user session:", error);
throw error;
}
}
async getUserSession(): Promise<{ user: User; loginTime: string } | null> {
try {
const jsonValue = await AsyncStorage.getItem(STORAGE_KEYS.USER_SESSION);
return jsonValue ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error("Failed to retrieve user session:", error);
return null;
}
}
async clearUserSession(): Promise<void> {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.USER_SESSION);
} catch (error) {
console.error("Failed to clear user session:", error);
throw error;
}
}
async saveAuthToken(token: string): Promise<void> {
try {
await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token);
} catch (error) {
console.error("Failed to save auth token:", error);
throw error;
}
}
async getAuthToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
} catch (error) {
console.error("Failed to retrieve auth token:", error);
return null;
}
}
async clearAuthToken(): Promise<void> {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
} catch (error) {
console.error("Failed to clear auth token:", error);
throw error;
}
}
async setBiometricEnabled(enabled: boolean): Promise<void> {
try {
await AsyncStorage.setItem(
STORAGE_KEYS.BIOMETRIC_ENABLED,
JSON.stringify(enabled)
);
} catch (error) {
console.error("Failed to save biometric setting:", error);
throw error;
}
}
async isBiometricEnabled(): Promise<boolean> {
try {
const value = await AsyncStorage.getItem(STORAGE_KEYS.BIOMETRIC_ENABLED);
return value ? JSON.parse(value) : false;
} catch (error) {
console.error("Failed to retrieve biometric setting:", error);
return false;
}
}
async saveLastLoginEmail(email: string): Promise<void> {
try {
await AsyncStorage.setItem(STORAGE_KEYS.LAST_LOGIN_EMAIL, email);
} catch (error) {
console.error("Failed to save last login email:", error);
throw error;
}
}
async getLastLoginEmail(): Promise<string | null> {
try {
return await AsyncStorage.getItem(STORAGE_KEYS.LAST_LOGIN_EMAIL);
} catch (error) {
console.error("Failed to retrieve last login email:", error);
return null;
}
}
async clearAllData(): Promise<void> {
try {
await AsyncStorage.multiRemove(Object.values(STORAGE_KEYS));
} catch (error) {
console.error("Failed to clear all storage data:", error);
throw error;
}
}
}
export default new StorageService();