703 lines
20 KiB
TypeScript
703 lines
20 KiB
TypeScript
import * as AppleAuthentication from "expo-apple-authentication";
|
|
import Constants from "expo-constants";
|
|
import { Image } from "expo-image";
|
|
import { Stack, useRouter } 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 { useAppState } from "@/context/AppStateContext";
|
|
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";
|
|
|
|
const BOOKMAKERS = [
|
|
"10Bet",
|
|
"WilliamHill",
|
|
"bet365",
|
|
"Marathon",
|
|
"Unibet",
|
|
"Betfair",
|
|
"188bet",
|
|
"Pncl",
|
|
"Sbo",
|
|
];
|
|
|
|
export default function ProfileScreen() {
|
|
const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } =
|
|
useTheme();
|
|
const { state, updateOddsSettings } = useAppState();
|
|
const { t, i18n } = useTranslation();
|
|
const router = useRouter();
|
|
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 [oddsModalVisible, setOddsModalVisible] = React.useState(false);
|
|
|
|
const currentLanguage = i18n.language;
|
|
|
|
const toggleOdds = () => {
|
|
updateOddsSettings({
|
|
...state.oddsSettings,
|
|
enabled: !state.oddsSettings.enabled,
|
|
});
|
|
};
|
|
|
|
const selectBookmaker = (name: string) => {
|
|
const current = state.oddsSettings.selectedBookmakers;
|
|
let next: string[];
|
|
if (current.includes(name)) {
|
|
next = current.filter((b) => b !== name);
|
|
} else {
|
|
next = [...current, name].slice(-2); // Keep last 2
|
|
}
|
|
updateOddsSettings({
|
|
...state.oddsSettings,
|
|
selectedBookmakers: next,
|
|
});
|
|
};
|
|
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" }}>
|
|
{t("settings.logout")}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</>
|
|
) : (
|
|
<TouchableOpacity
|
|
style={styles.loginCard}
|
|
onPress={handleLoginPress}
|
|
activeOpacity={0.8}
|
|
>
|
|
<View style={styles.loginIcon} />
|
|
<View style={styles.loginInfo}>
|
|
<ThemedText style={styles.loginTitle}>
|
|
{t("settings.login")}
|
|
</ThemedText>
|
|
<ThemedText style={{ color: subTextColor }}>
|
|
{t("settings.click_to_login")}
|
|
</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
|
|
style={[
|
|
styles.settingItem,
|
|
{
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
|
|
},
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
style={styles.settingItemContent}
|
|
onPress={() => router.push("/privacy" as any)}
|
|
>
|
|
<View style={styles.settingLabel}>
|
|
<IconSymbol
|
|
name="document"
|
|
size={20}
|
|
color={iconColor}
|
|
style={{ marginRight: 10 }}
|
|
/>
|
|
<ThemedText>{t("profile.privacy")}</ThemedText>
|
|
</View>
|
|
<IconSymbol
|
|
name="chevron-forward"
|
|
size={16}
|
|
color={subTextColor}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<ThemedText style={styles.sectionTitle}>
|
|
{t("settings.odds_title")}
|
|
</ThemedText>
|
|
|
|
<View
|
|
style={[
|
|
styles.section,
|
|
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
|
|
]}
|
|
>
|
|
<View style={styles.settingItem}>
|
|
<View style={styles.settingLabel}>
|
|
<IconSymbol
|
|
name="stats-chart"
|
|
size={20}
|
|
color={iconColor}
|
|
style={{ marginRight: 10 }}
|
|
/>
|
|
<ThemedText>{t("settings.odds_show")}</ThemedText>
|
|
</View>
|
|
<View style={styles.settingControl}>
|
|
<TouchableOpacity onPress={toggleOdds} style={styles.button}>
|
|
<ThemedText>
|
|
{state.oddsSettings.enabled
|
|
? t("settings.odds_enabled")
|
|
: t("settings.odds_disabled")}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<View
|
|
style={[
|
|
styles.settingItem,
|
|
{
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
|
|
},
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
style={styles.settingItemContent}
|
|
onPress={() => setOddsModalVisible(true)}
|
|
disabled={!state.oddsSettings.enabled}
|
|
>
|
|
<View style={styles.settingLabel}>
|
|
<IconSymbol
|
|
name="list"
|
|
size={20}
|
|
color={state.oddsSettings.enabled ? iconColor : subTextColor}
|
|
style={{ marginRight: 10 }}
|
|
/>
|
|
<ThemedText
|
|
style={{
|
|
color: state.oddsSettings.enabled
|
|
? textColor
|
|
: subTextColor,
|
|
}}
|
|
>
|
|
{t("settings.odds_select_company")}
|
|
</ThemedText>
|
|
</View>
|
|
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
<ThemedText style={{ color: subTextColor, marginRight: 4 }}>
|
|
{state.oddsSettings.selectedBookmakers.join(", ") ||
|
|
t("settings.odds_unselected")}
|
|
</ThemedText>
|
|
<IconSymbol
|
|
name="chevron-forward"
|
|
size={16}
|
|
color={subTextColor}
|
|
/>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</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}>
|
|
{t("settings.select_login_method")}
|
|
</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>{t("settings.google_login")}</ThemedText>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.modalCancel}
|
|
onPress={() => setLoginModalVisible(false)}
|
|
>
|
|
<ThemedText>{t("settings.cancel")}</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</Modal>
|
|
|
|
<Modal
|
|
visible={oddsModalVisible}
|
|
transparent
|
|
animationType="slide"
|
|
onRequestClose={() => setOddsModalVisible(false)}
|
|
>
|
|
<TouchableOpacity
|
|
style={styles.modalMask}
|
|
activeOpacity={1}
|
|
onPress={() => setOddsModalVisible(false)}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.modalCard,
|
|
{
|
|
backgroundColor: isDark ? "#1c1c1e" : "#fff",
|
|
maxHeight: "70%",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.modalTitle}>
|
|
{t("settings.odds_modal_title")}
|
|
</ThemedText>
|
|
<ScrollView style={{ marginVertical: 10 }}>
|
|
{BOOKMAKERS.map((name) => {
|
|
const isSelected =
|
|
state.oddsSettings.selectedBookmakers.includes(name);
|
|
return (
|
|
<TouchableOpacity
|
|
key={name}
|
|
style={[
|
|
styles.bookmakerItem,
|
|
{ borderColor: isDark ? "#38383a" : "#eee" },
|
|
]}
|
|
onPress={() => selectBookmaker(name)}
|
|
>
|
|
<ThemedText
|
|
style={{
|
|
color: isSelected ? "#FF9500" : textColor,
|
|
fontWeight: isSelected ? "bold" : "normal",
|
|
}}
|
|
>
|
|
{name}
|
|
</ThemedText>
|
|
{isSelected && (
|
|
<IconSymbol name="checkmark" size={18} color="#FF9500" />
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
<TouchableOpacity
|
|
style={styles.modalCancel}
|
|
onPress={() => setOddsModalVisible(false)}
|
|
>
|
|
<ThemedText type="defaultSemiBold">
|
|
{t("settings.odds_confirm")}
|
|
</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,
|
|
},
|
|
settingItemContent: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
flex: 1,
|
|
},
|
|
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",
|
|
},
|
|
bookmakerItem: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingVertical: 14,
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
},
|
|
logoutButton: {
|
|
paddingVertical: 12,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
marginTop: 10,
|
|
},
|
|
});
|