Files
physical-expo/app/profile.tsx
2026-01-16 09:58:07 +08:00

475 lines
13 KiB
TypeScript

import * as AppleAuthentication from "expo-apple-authentication";
import Constants from "expo-constants";
import { Image } from "expo-image";
import { Stack } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import {
Modal,
Platform,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useTheme } from "@/context/ThemeContext";
import { changeLanguage } from "@/i18n";
import { appleSignIn, fetchUserProfile, logout } from "@/lib/api";
import { storage } from "@/lib/storage";
import type { UserProfile } from "@/types/api";
export default function ProfileScreen() {
const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } =
useTheme();
const { t, i18n } = useTranslation();
const isDark = theme === "dark";
const [appleAvailable, setAppleAvailable] = React.useState(false);
const [user, setUser] = React.useState<UserProfile | null>(null);
const [loginModalVisible, setLoginModalVisible] = React.useState(false);
const currentLanguage = i18n.language;
const toggleLanguage = () => {
const nextLang = currentLanguage.startsWith("en") ? "zh" : "en";
changeLanguage(nextLang);
};
const handleAppleSignIn = async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
console.log("Apple credential:", {
fullName: credential.fullName,
email: credential.email,
user: credential.user,
});
const deviceId = Constants.deviceId || Constants.sessionId || "unknown";
const platformVersion = Platform.Version.toString();
const options = {
authorizationCode: credential.authorizationCode || "",
identityToken: credential.identityToken || "",
deviceInfo: {
deviceId,
platform: Platform.OS,
platformVersion,
},
user:
credential.fullName && (credential.fullName.givenName || credential.fullName.familyName)
? {
name: {
firstName: credential.fullName.givenName || undefined,
lastName: credential.fullName.familyName || undefined,
},
}
: undefined,
};
console.log("登录参数", options);
const res = await appleSignIn(options);
console.log("登录响应", res);
await storage.setAccessToken(res.accessToken);
await storage.setRefreshToken(res.refreshToken);
if (res.user) {
await storage.setUser(res.user);
setUser(res.user);
} else {
const profile = await fetchUserProfile();
await storage.setUser(profile);
setUser(profile);
}
setLoginModalVisible(false);
} catch (error: any) {
if (error?.code === "ERR_REQUEST_CANCELED") {
return;
}
console.error("Apple sign in error:", error);
}
};
const handleLoginPress = () => {
setLoginModalVisible(true);
};
React.useEffect(() => {
AppleAuthentication.isAvailableAsync().then(setAppleAvailable);
}, []);
React.useEffect(() => {
const loadUser = async () => {
const savedUser = await storage.getUser();
if (savedUser) {
setUser(savedUser);
try {
const profile = await fetchUserProfile();
await storage.setUser(profile);
setUser(profile);
} catch {
setUser(savedUser);
}
}
};
loadUser();
}, []);
const handleLogout = async () => {
try {
await logout();
} catch (error) {
console.error("Logout error:", error);
} finally {
await storage.clear();
setUser(null);
}
};
const isLoggedIn = !!user;
const iconColor = isDark ? "#FFFFFF" : "#000000";
const textColor = isDark ? "#FFFFFF" : "#000000";
const subTextColor = isDark ? "#AAAAAA" : "#666666";
return (
<>
<Stack.Screen
options={{
title: t("profile.title"),
headerShown: true,
// Ensure header matches theme to avoid white flash
headerStyle: {
backgroundColor: isDark ? "#000" : "#f2f2f7",
},
headerTintColor: textColor,
headerShadowVisible: false,
// Present the screen as a normal card and slide from right
presentation: "card",
animation: "slide_from_right",
// Set the scene/content background to match theme during transition
contentStyle: { backgroundColor: isDark ? "#000" : "#f2f2f7" },
}}
/>
<ScrollView
style={[
styles.container,
{ backgroundColor: isDark ? "#000" : "#f2f2f7" },
]}
>
<View
style={[
styles.section,
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
{isLoggedIn ? (
<>
<View style={styles.profileHeader}>
<Image
source={{ uri: user?.avatar || "https://via.placeholder.com/100" }}
style={styles.avatar}
contentFit="cover"
/>
<View style={styles.profileInfo}>
<ThemedText type="title">
{user?.nickname || t("profile.name")}
</ThemedText>
<ThemedText style={{ color: subTextColor }}>
{user?.appleId || ""}
</ThemedText>
</View>
</View>
<TouchableOpacity
style={[
styles.logoutButton,
{
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
},
]}
onPress={handleLogout}
>
<ThemedText style={{ color: "#FF3B30" }}></ThemedText>
</TouchableOpacity>
</>
) : (
<TouchableOpacity
style={styles.loginCard}
onPress={handleLoginPress}
activeOpacity={0.8}
>
<View style={styles.loginIcon} />
<View style={styles.loginInfo}>
<ThemedText style={styles.loginTitle}></ThemedText>
<ThemedText style={{ color: subTextColor }}>
</ThemedText>
</View>
</TouchableOpacity>
)}
</View>
<ThemedText style={styles.sectionTitle}>
{t("settings.title")}
</ThemedText>
<View
style={[
styles.section,
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
<View style={styles.settingItem}>
<View style={styles.settingLabel}>
<IconSymbol
name="sunny"
size={20}
color={iconColor}
style={{ marginRight: 10 }}
/>
<ThemedText>{t("settings.theme")}</ThemedText>
</View>
<View style={styles.settingControl}>
<TouchableOpacity onPress={toggleTheme} style={styles.button}>
<ThemedText>
{theme === "light" ? t("settings.light") : t("settings.dark")}
</ThemedText>
</TouchableOpacity>
</View>
</View>
<View
style={[
styles.settingItem,
{
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
},
]}
>
<View style={styles.settingLabel}>
<IconSymbol
name="globe"
size={20}
color={iconColor}
style={{ marginRight: 10 }}
/>
<ThemedText>{t("settings.language")}</ThemedText>
</View>
<View style={styles.settingControl}>
<TouchableOpacity onPress={toggleLanguage} style={styles.button}>
<ThemedText>
{currentLanguage.startsWith("zh")
? t("settings.chinese")
: t("settings.english")}
</ThemedText>
</TouchableOpacity>
</View>
</View>
</View>
{/* <ThemedText style={styles.sectionTitle}>登录</ThemedText>
<View
style={[
styles.section,
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
{appleAvailable && (
<View style={styles.settingItem}>
<AppleAuthentication.AppleAuthenticationButton
buttonType={
AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN
}
buttonStyle={
isDark
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
}
cornerRadius={8}
style={{ width: "100%", height: 44 }}
onPress={handleAppleSignIn}
/>
</View>
)}
<View
style={[
styles.settingItem,
{
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
},
]}
>
<TouchableOpacity style={styles.googleButton} onPress={() => {}}>
<ThemedText>Google 登录</ThemedText>
</TouchableOpacity>
</View>
</View> */}
</ScrollView>
<Modal
visible={loginModalVisible}
transparent
animationType="fade"
onRequestClose={() => setLoginModalVisible(false)}
>
<TouchableOpacity
style={styles.modalMask}
activeOpacity={1}
onPress={() => setLoginModalVisible(false)}
>
<View
style={[
styles.modalCard,
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
<ThemedText style={styles.modalTitle}></ThemedText>
{appleAvailable && (
<AppleAuthentication.AppleAuthenticationButton
buttonType={
AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN
}
buttonStyle={
isDark
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
}
cornerRadius={8}
style={{ width: "100%", height: 44 }}
onPress={handleAppleSignIn}
/>
)}
<TouchableOpacity style={styles.googleButton} onPress={() => {}}>
<ThemedText>Google </ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={styles.modalCancel}
onPress={() => setLoginModalVisible(false)}
>
<ThemedText></ThemedText>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
section: {
marginTop: 20,
paddingVertical: 10,
paddingHorizontal: 16,
borderRadius: 10, // iOS style groups
marginHorizontal: 16,
},
sectionTitle: {
marginLeft: 32,
marginTop: 20,
marginBottom: 5,
fontSize: 13,
textTransform: "uppercase",
opacity: 0.6,
},
profileHeader: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 10,
},
avatar: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#ccc",
},
profileInfo: {
marginLeft: 15,
},
settingItem: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 12,
},
settingLabel: {
flexDirection: "row",
alignItems: "center",
},
settingControl: {
flexDirection: "row",
alignItems: "center",
},
button: {
paddingHorizontal: 10,
paddingVertical: 5,
},
googleButton: {
width: "100%",
height: 44,
borderRadius: 8,
borderWidth: 1,
borderColor: "#c6c6c8",
alignItems: "center",
justifyContent: "center",
},
loginCard: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
},
loginIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: "#c6c6c8",
},
loginInfo: {
marginLeft: 12,
},
loginTitle: {
fontSize: 18,
},
modalMask: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "center",
paddingHorizontal: 24,
},
modalCard: {
borderRadius: 12,
padding: 16,
gap: 12,
},
modalTitle: {
fontSize: 16,
textAlign: "center",
},
modalCancel: {
height: 40,
alignItems: "center",
justifyContent: "center",
},
logoutButton: {
paddingVertical: 12,
alignItems: "center",
justifyContent: "center",
marginTop: 10,
},
});