添加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

@@ -11,6 +11,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "physical",
"usesAppleSignIn": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
@@ -32,6 +33,7 @@
},
"plugins": [
"expo-router",
"expo-apple-authentication",
[
"expo-splash-screen",
{

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,
},
});

View File

@@ -14,4 +14,8 @@ export const API_ENDPOINTS = {
ODDS: "/v1/api/odds",
SEARCH: "/v1/api/search",
H2H: "/v1/api/h2h",
APPLE_SIGNIN: "/v1/api/auth/apple-signin",
LOGOUT: "/v1/api/auth/logout",
REFRESH_TOKEN: "/v1/api/auth/refresh-token",
USER_PROFILE: "/v1/api/user/profile",
};

View File

@@ -2,6 +2,8 @@ import { API_CONFIG, API_ENDPOINTS } from "@/constants/api";
import {
ApiListResponse,
ApiResponse,
AppleSignInRequest,
AppleSignInResponse,
Country,
H2HData,
League,
@@ -9,11 +11,15 @@ import {
Match,
MatchDetailData,
OddsData,
RefreshTokenRequest,
RefreshTokenResponse,
SearchResult,
Sport,
UpcomingMatch,
UserProfile,
} from "@/types/api";
import axios from "axios";
import { storage } from "./storage";
const apiClient = axios.create({
baseURL: API_CONFIG.BASE_URL,
@@ -23,6 +29,55 @@ const apiClient = axios.create({
},
});
apiClient.interceptors.request.use(async (config) => {
const token = await storage.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
const refreshTokenApi = async (
request: RefreshTokenRequest
): Promise<RefreshTokenResponse> => {
try {
const response = await apiClient.post<
ApiResponse<RefreshTokenResponse>
>(API_ENDPOINTS.REFRESH_TOKEN, request);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Refresh token error:", error);
throw error;
}
};
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshTokenValue = await storage.getRefreshToken();
if (refreshTokenValue) {
const res = await refreshTokenApi({ refreshToken: refreshTokenValue });
await storage.setAccessToken(res.accessToken);
originalRequest.headers.Authorization = `Bearer ${res.accessToken}`;
return apiClient(originalRequest);
}
} catch {
await storage.clear();
}
}
return Promise.reject(error);
}
);
export const fetchSports = async (): Promise<Sport[]> => {
try {
const response = await apiClient.get<ApiResponse<ApiListResponse<Sport>>>(
@@ -311,3 +366,58 @@ export const fetchH2H = async (
throw error;
}
};
export const appleSignIn = async (
request: AppleSignInRequest
): Promise<AppleSignInResponse> => {
try {
const response = await apiClient.post<
ApiResponse<AppleSignInResponse>
>(API_ENDPOINTS.APPLE_SIGNIN, request);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Apple sign in error:", error);
throw error;
}
};
export const logout = async (): Promise<string> => {
try {
const response = await apiClient.post<ApiResponse<string>>(
API_ENDPOINTS.LOGOUT
);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Logout error:", error);
throw error;
}
};
export const refreshToken = refreshTokenApi;
export const fetchUserProfile = async (): Promise<UserProfile> => {
try {
const response = await apiClient.get<ApiResponse<UserProfile>>(
API_ENDPOINTS.USER_PROFILE
);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Fetch user profile error:", error);
throw error;
}
};

48
lib/storage.ts Normal file
View File

@@ -0,0 +1,48 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { UserProfile } from "@/types/api";
const STORAGE_KEYS = {
ACCESS_TOKEN: "access_token",
REFRESH_TOKEN: "refresh_token",
USER: "user",
};
export const storage = {
async setAccessToken(token: string): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
},
async getAccessToken(): Promise<string | null> {
return await AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
},
async setRefreshToken(token: string): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, token);
},
async getRefreshToken(): Promise<string | null> {
return await AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
},
async setUser(user: UserProfile): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
},
async getUser(): Promise<UserProfile | null> {
const userStr = await AsyncStorage.getItem(STORAGE_KEYS.USER);
if (!userStr) return null;
try {
return JSON.parse(userStr) as UserProfile;
} catch {
return null;
}
},
async clear(): Promise<void> {
await AsyncStorage.multiRemove([
STORAGE_KEYS.ACCESS_TOKEN,
STORAGE_KEYS.REFRESH_TOKEN,
STORAGE_KEYS.USER,
]);
},
};

37
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@react-navigation/native": "^7.1.8",
"axios": "^1.13.2",
"expo": "~54.0.31",
"expo-apple-authentication": "~8.0.8",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-dev-client": "~6.0.20",
@@ -1480,7 +1481,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -3087,7 +3087,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.7.10.tgz",
"integrity": "sha512-FGYU5Ebd2whTa4Z+RBCxnWqmyWIQGTJ7PAAhk2RjlVrEXLU0HaFR5JGmEHuNm/Cm9xX3xCwOxYZiA0Xi/DeyAA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/elements": "^2.9.3",
"color": "^4.2.3",
@@ -3132,7 +3131,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.27.tgz",
"integrity": "sha512-kW7LGP/RrisktpGyizTKw6HwSeQJdXnAN9L8GyQJcJAlgL9YtfEg6yEyD5n9RWH90CL8G0cRyUhphKIAFf4lVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.13.7",
"escape-string-regexp": "^4.0.0",
@@ -3331,7 +3329,6 @@
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3402,7 +3399,6 @@
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.52.0",
"@typescript-eslint/types": "8.52.0",
@@ -3964,7 +3960,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4667,7 +4662,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5666,7 +5660,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5863,7 +5856,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -6102,7 +6094,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.31.tgz",
"integrity": "sha512-kQ3RDqA/a59I7y+oqQGyrPbbYlgPMUdKBOgvFLpoHbD2bCM+F75i4N0mUijy7dG5F/CUCu2qHmGGUCXBbMDkCg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.21",
@@ -6150,6 +6141,16 @@
}
}
},
"node_modules/expo-apple-authentication": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/expo-apple-authentication/-/expo-apple-authentication-8.0.8.tgz",
"integrity": "sha512-TwCHWXYR1kS0zaeV7QZKLWYluxsvqL31LFJubzK30njZqeWoWO89HZ8nZVaeXbFV1LrArKsze4BmMb+94wS0AQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-asset": {
"version": "12.0.12",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
@@ -6181,7 +6182,6 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
"integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@expo/config": "~12.0.13",
"@expo/env": "~2.0.8"
@@ -6285,7 +6285,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz",
"integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -6383,7 +6382,6 @@
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz",
"integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==",
"license": "MIT",
"peer": true,
"dependencies": {
"expo-constants": "~18.0.12",
"invariant": "^2.2.4"
@@ -7768,7 +7766,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -10545,7 +10542,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10565,7 +10561,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -10629,7 +10624,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
@@ -10703,7 +10697,6 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -10741,7 +10734,6 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
"integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1",
"semver": "7.7.2"
@@ -10770,7 +10762,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -10781,7 +10772,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
@@ -10807,7 +10797,6 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
@@ -10840,7 +10829,6 @@
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"escape-string-regexp": "^4.0.0",
"invariant": "2.2.4"
@@ -10965,7 +10953,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12286,7 +12273,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12493,7 +12479,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -4,8 +4,8 @@
"version": "1.0.0",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint"
},
@@ -18,6 +18,7 @@
"@react-navigation/native": "^7.1.8",
"axios": "^1.13.2",
"expo": "~54.0.31",
"expo-apple-authentication": "~8.0.8",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-dev-client": "~6.0.20",

View File

@@ -431,3 +431,47 @@ export interface H2HData {
firstTeamResults: H2HMatch[];
secondTeamResults: H2HMatch[];
}
export interface AppleSignInRequest {
authorizationCode: string;
deviceInfo: {
deviceId: string;
platform: string;
platformVersion: string;
};
identityToken: string;
user?: {
name?: {
firstName?: string;
lastName?: string;
};
};
}
export interface AppleSignInResponse {
accessToken: string;
expiresIn: number;
refreshToken: string;
user: {
id: number;
appleId: string;
nickname: string;
avatar: string;
};
}
export interface RefreshTokenRequest {
refreshToken: string;
}
export interface RefreshTokenResponse {
accessToken: string;
expiresIn: number;
}
export interface UserProfile {
id: number;
appleId: string;
nickname: string;
avatar: string;
}