添加Apple登录

This commit is contained in:
xianyi
2026-01-16 09:58:07 +08:00
parent cc88c283ce
commit 370f04d721
8 changed files with 521 additions and 46 deletions

View File

@@ -1,19 +1,34 @@
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 { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native";
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;
@@ -22,6 +37,107 @@ export default function ProfileScreen() {
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";
@@ -51,29 +167,59 @@ export default function ProfileScreen() {
{ backgroundColor: isDark ? "#000" : "#f2f2f7" },
]}
>
{/* User Info Section */}
<View
style={[
styles.section,
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
<View style={styles.profileHeader}>
<Image
source={{ uri: "https://via.placeholder.com/100" }}
style={styles.avatar}
contentFit="cover"
/>
<View style={styles.profileInfo}>
<ThemedText type="title">{t("profile.name")}</ThemedText>
<ThemedText style={{ color: subTextColor }}>
user@example.com
</ThemedText>
</View>
</View>
{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>
{/* Settings Section */}
<ThemedText style={styles.sectionTitle}>
{t("settings.title")}
</ThemedText>
@@ -84,7 +230,6 @@ export default function ProfileScreen() {
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
]}
>
{/* Theme Setting */}
<View style={styles.settingItem}>
<View style={styles.settingLabel}>
<IconSymbol
@@ -104,7 +249,6 @@ export default function ProfileScreen() {
</View>
</View>
{/* Language Setting */}
<View
style={[
styles.settingItem,
@@ -134,7 +278,92 @@ export default function ProfileScreen() {
</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>
</>
);
}
@@ -190,4 +419,56 @@ const styles = StyleSheet.create({
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,
},
});