Initial commit: Expo app with Better Auth integration

- Complete Expo React Native app setup with TypeScript
- Better Auth authentication system integration
- Secure storage implementation for session tokens
- Authentication flow with login/logout functionality
- API client configuration for backend communication
- Responsive UI components with themed styling
- Expo Router navigation setup
- Development configuration and scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2025-10-14 10:32:52 +08:00
commit 7e3f94bae3
71 changed files with 20800 additions and 0 deletions

15
app/(auth)/_layout.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Stack } from "expo-router";
export default function AuthLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: "#fff" },
}}
>
<Stack.Screen name="login" />
<Stack.Screen name="register" />
</Stack>
);
}

180
app/(auth)/login.tsx Normal file
View File

@@ -0,0 +1,180 @@
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { useAuth } from "@/hooks/use-auth";
import { authClient } from "@/lib/auth/client";
import { router } from "expo-router";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { storage } from '../../lib/storage';
export default function LoginScreen() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { isAuthenticated, isLoading: authLoading } = useAuth();
useEffect(() => {
if (!authLoading && isAuthenticated) {
console.log("[Login] User authenticated, redirecting to tabs...");
router.replace("/(tabs)");
}
}, [isAuthenticated, authLoading]);
const handleLogin = async () => {
console.log("[Login] handleLogin called");
if (!username.trim() || !password.trim()) {
console.log("[Login] Validation failed: empty username or password");
Alert.alert("错误", "请输入用户名和密码");
return;
}
console.log("[Login] Starting login with username:", username.trim());
setIsLoading(true);
try {
console.log("[Login] Calling authClient.signIn.username...");
await new Promise<void>(async (resolve, reject) => {
await authClient.signIn.username({
username: username.trim(),
password,
}, {
onSuccess: async (ctx) => {
const authToken = ctx.response.headers.get("set-auth-token")
if (authToken) {
await storage.setItem(`bestaibest.better-auth.session_token`, authToken);
resolve()
} else {
console.error("Bearer token not set");
reject()
}
}
});
})
} catch (error: any) {
Alert.alert("登录失败", error?.message || "用户名或密码错误");
} finally {
setIsLoading(false);
console.log("[Login] handleLogin completed");
}
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
>
<ThemedView style={styles.content}>
<ThemedText type="title" style={styles.title}>
</ThemedText>
<ThemedText style={styles.subtitle}></ThemedText>
<View style={styles.form}>
<TextInput
style={styles.input}
placeholder="用户名"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="密码"
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!isLoading}
/>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={styles.buttonText}></ThemedText>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.registerLink}
onPress={() => router.push("/(auth)/register")}
disabled={isLoading}
>
<ThemedText style={styles.registerText}></ThemedText>
</TouchableOpacity>
</View>
</ThemedView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
padding: 24,
justifyContent: "center",
},
title: {
marginBottom: 8,
textAlign: "center",
},
subtitle: {
marginBottom: 32,
textAlign: "center",
opacity: 0.7,
},
form: {
gap: 16,
},
input: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: "#fff",
},
button: {
backgroundColor: "#007AFF",
padding: 16,
borderRadius: 8,
alignItems: "center",
marginTop: 8,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
registerLink: {
alignItems: "center",
marginTop: 8,
},
registerText: {
color: "#007AFF",
fontSize: 14,
},
});

191
app/(auth)/register.tsx Normal file
View File

@@ -0,0 +1,191 @@
import { useState } from "react";
import {
View,
TextInput,
StyleSheet,
Alert,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
ActivityIndicator,
ScrollView,
} from "react-native";
import { router } from "expo-router";
import { authClient } from "@/lib/auth/client";
import { ThemedView } from "@/components/themed-view";
import { ThemedText } from "@/components/themed-text";
export default function RegisterScreen() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleRegister = async () => {
if (!username.trim() || !email.trim() || !password.trim()) {
Alert.alert("错误", "请填写所有必填字段");
return;
}
if (password !== confirmPassword) {
Alert.alert("错误", "两次输入的密码不一致");
return;
}
if (password.length < 6) {
Alert.alert("错误", "密码长度至少为 6 位");
return;
}
setIsLoading(true);
try {
await authClient.signUp.email({
email: email.trim(),
password,
name: username.trim(),
});
Alert.alert("注册成功", "请登录您的账号", [
{ text: "确定", onPress: () => router.replace("/(auth)/login") },
]);
} catch (error: any) {
Alert.alert("注册失败", error?.message || "注册失败,请稍后重试");
} finally {
setIsLoading(false);
}
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
<ThemedView style={styles.content}>
<ThemedText type="title" style={styles.title}>
</ThemedText>
<ThemedText style={styles.subtitle}></ThemedText>
<View style={styles.form}>
<TextInput
style={styles.input}
placeholder="用户名"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="邮箱"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoCorrect={false}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="密码(至少 6 位)"
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="确认密码"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
editable={!isLoading}
/>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={styles.buttonText}></ThemedText>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.loginLink}
onPress={() => router.back()}
disabled={isLoading}
>
<ThemedText style={styles.loginText}></ThemedText>
</TouchableOpacity>
</View>
</ThemedView>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
},
content: {
flex: 1,
padding: 24,
justifyContent: "center",
},
title: {
marginBottom: 8,
textAlign: "center",
},
subtitle: {
marginBottom: 32,
textAlign: "center",
opacity: 0.7,
},
form: {
gap: 16,
},
input: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: "#fff",
},
button: {
backgroundColor: "#007AFF",
padding: 16,
borderRadius: 8,
alignItems: "center",
marginTop: 8,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
loginLink: {
alignItems: "center",
marginTop: 8,
},
loginText: {
color: "#007AFF",
fontSize: 14,
},
});