添加Apple登录
This commit is contained in:
2
app.json
2
app.json
@@ -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",
|
||||
{
|
||||
|
||||
297
app/profile.tsx
297
app/profile.tsx
@@ -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" },
|
||||
]}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<View style={styles.profileHeader}>
|
||||
<Image
|
||||
source={{ uri: "https://via.placeholder.com/100" }}
|
||||
source={{ uri: user?.avatar || "https://via.placeholder.com/100" }}
|
||||
style={styles.avatar}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<View style={styles.profileInfo}>
|
||||
<ThemedText type="title">{t("profile.name")}</ThemedText>
|
||||
<ThemedText type="title">
|
||||
{user?.nickname || t("profile.name")}
|
||||
</ThemedText>
|
||||
<ThemedText style={{ color: subTextColor }}>
|
||||
user@example.com
|
||||
{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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
110
lib/api.ts
110
lib/api.ts
@@ -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
48
lib/storage.ts
Normal 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
37
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
44
types/api.ts
44
types/api.ts
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user