diff --git a/app.json b/app.json index 6b07b45..170eb37 100644 --- a/app.json +++ b/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", { diff --git a/app/profile.tsx b/app/profile.tsx index 327e33b..d38c306 100644 --- a/app/profile.tsx +++ b/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(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 */} - - - - {t("profile.name")} - - user@example.com - - - + {isLoggedIn ? ( + <> + + + + + {user?.nickname || t("profile.name")} + + + {user?.appleId || ""} + + + + + 登出 + + + ) : ( + + + + 登录 + + 点击登录 + + + + )} - {/* Settings Section */} {t("settings.title")} @@ -84,7 +230,6 @@ export default function ProfileScreen() { { backgroundColor: isDark ? "#1c1c1e" : "#fff" }, ]} > - {/* Theme Setting */} - {/* Language Setting */} + + {/* 登录 + + + {appleAvailable && ( + + + + )} + + {}}> + Google 登录 + + + */} + setLoginModalVisible(false)} + > + setLoginModalVisible(false)} + > + + 选择登录方式 + {appleAvailable && ( + + )} + {}}> + Google 登录 + + setLoginModalVisible(false)} + > + 取消 + + + + ); } @@ -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, + }, }); diff --git a/constants/api.ts b/constants/api.ts index a235020..b62f928 100644 --- a/constants/api.ts +++ b/constants/api.ts @@ -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", }; diff --git a/lib/api.ts b/lib/api.ts index 6a0dcb2..c94b799 100644 --- a/lib/api.ts +++ b/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 => { + try { + const response = await apiClient.post< + ApiResponse + >(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 => { try { const response = await apiClient.get>>( @@ -311,3 +366,58 @@ export const fetchH2H = async ( throw error; } }; + +export const appleSignIn = async ( + request: AppleSignInRequest +): Promise => { + try { + const response = await apiClient.post< + ApiResponse + >(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 => { + try { + const response = await apiClient.post>( + 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 => { + try { + const response = await apiClient.get>( + 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; + } +}; diff --git a/lib/storage.ts b/lib/storage.ts new file mode 100644 index 0000000..e3358d4 --- /dev/null +++ b/lib/storage.ts @@ -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 { + await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token); + }, + + async getAccessToken(): Promise { + return await AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); + }, + + async setRefreshToken(token: string): Promise { + await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, token); + }, + + async getRefreshToken(): Promise { + return await AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); + }, + + async setUser(user: UserProfile): Promise { + await AsyncStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user)); + }, + + async getUser(): Promise { + 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 { + await AsyncStorage.multiRemove([ + STORAGE_KEYS.ACCESS_TOKEN, + STORAGE_KEYS.REFRESH_TOKEN, + STORAGE_KEYS.USER, + ]); + }, +}; diff --git a/package-lock.json b/package-lock.json index 9b4c325..ed55b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 8ba8529..368ee99 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/types/api.ts b/types/api.ts index 3bb3016..7d5f900 100644 --- a/types/api.ts +++ b/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; +}