From a7348405474fb23295d2635642be518cc247b285 Mon Sep 17 00:00:00 2001 From: StarAppeal Date: Sat, 6 Sep 2025 03:48:24 +0200 Subject: [PATCH] refactor: unify API response handling and improve error state management --- app/(tabs)/settings.tsx | 2 +- app/login.tsx | 8 ++--- src/components/ChangePasswordModal.tsx | 20 ++++++----- src/context/AuthProvider.tsx | 39 ++++++++++----------- src/hooks/useSpotifyAuth.ts | 7 +++- src/services/RestService.ts | 47 +++++++++++++++----------- 6 files changed, 66 insertions(+), 57 deletions(-) diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 3959f28..738b231 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -44,7 +44,7 @@ export default function SettingsScreen() { /> {!!authenticatedUser?.spotifyConfig && ( { const rest = new RestService(jwtToken); - rest.updateSelfSpotifyConfig().then((result) => { + rest.removeSpotifyConfig().then((result) => { console.log("Spotify Login entfernt"); console.log(result); refreshUser() diff --git a/app/login.tsx b/app/login.tsx index f68c876..c76b04a 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -55,8 +55,8 @@ export default function LoginScreen() { returnKeyType="next" value={username} onChangeText={setUsername} - error={!!error && error?.id === "username"} - errorText={error?.message} + error={!!error && error?.toLowerCase().includes("user") } + errorText={error!} autoCapitalize="none" /> @@ -65,8 +65,8 @@ export default function LoginScreen() { returnKeyType="done" value={password} onChangeText={setPassword} - error={!!error && error?.id === "password"} - errorText={error?.message} + error={!!error && error?.toLowerCase().includes("pass") } + errorText={error!} autoComplete="password" /> diff --git a/src/components/ChangePasswordModal.tsx b/src/components/ChangePasswordModal.tsx index c94260b..4164618 100644 --- a/src/components/ChangePasswordModal.tsx +++ b/src/components/ChangePasswordModal.tsx @@ -2,7 +2,7 @@ import React, {useState} from "react"; import { Paragraph} from "react-native-paper"; import {useTheme} from "@/src/context/ThemeProvider"; import {StyleSheet, View} from "react-native"; -import {RestService} from "@/src/services/RestService"; +import {ApiResponse, RestService} from "@/src/services/RestService"; import {useAuth} from "@/src/context/AuthProvider"; import CustomModal from "@/src/components/themed/CustomModal"; import PasswordInput from "@/src/components/PasswordInput"; @@ -14,22 +14,24 @@ export default function ChangePasswordModal() { const {token: jwtToken} = useAuth(); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - const [apiResponse, setApiResponse] = useState<{ success: boolean, message: string } | null>(null); + const [apiResponse, setApiResponse] = useState | null>(null); const handleConfirm = () => { if (!password || !confirmPassword) { - setApiResponse({success: false, message: "Bitte füllen Sie alle Felder aus!"}); + + + setApiResponse({ok: false, data: {message: "Bitte füllen Sie alle Felder aus!"}}); return; } if (password !== confirmPassword) { - setApiResponse({success: false, message: "Passwörter stimmen nicht überein!"}); + setApiResponse({ok: false, data: { message: "Passwörter stimmen nicht überein!"}}); return; } new RestService(jwtToken).changeSelfPassword(password, confirmPassword).then( (response) => { console.log(response); - setApiResponse(response.result); - if (response.result.success) { + setApiResponse(response); + if (response.ok) { // add something here setPassword(""); setConfirmPassword(""); @@ -50,11 +52,11 @@ export default function ChangePasswordModal() { Passwort ändern - {apiResponse && apiResponse.message && ( + {apiResponse && apiResponse.data?.message && ( + style={[styles.apiResponseBox, {backgroundColor: apiResponse.ok ? theme.colors.success : theme.colors.error}]}> {apiResponse.message} + style={{color: apiResponse.ok ? theme.colors.onSuccess : theme.colors.onError}}>{apiResponse.data.message} )} diff --git a/src/context/AuthProvider.tsx b/src/context/AuthProvider.tsx index 5558a02..6bbe862 100644 --- a/src/context/AuthProvider.tsx +++ b/src/context/AuthProvider.tsx @@ -3,18 +3,14 @@ import {getFromStorage, JWT_TOKEN_KEY, removeFromStorage, saveInStorage} from "@ import {RestService} from "@/src/services/RestService"; import {User} from "@/src/model/User"; -interface AuthError { - message: string; - id: "username" | "password" | "general"; -} type AuthContextType = { isAuthenticated: boolean | null; token: string | null; login: (username: string, password: string) => Promise; logout: () => Promise; - error: AuthError | null; authenticatedUser: User | null; + error: string | null; loading: boolean; refreshUser: () => Promise; }; @@ -25,7 +21,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children} const [isAuthenticated, setIsAuthenticated] = useState(null); const [authenticatedUser, setAuthenticatedUser] = useState(null); const [token, setToken] = useState(null); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -45,19 +41,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children} }, []); const saveUser = async (token: string): Promise => { - const user = await new RestService(token).getSelf(); - if (!user) { + const response = await new RestService(token).getSelf(); + if (!response.ok || !response.data) { // token ist ungültig await removeFromStorage(JWT_TOKEN_KEY); setToken(null); setIsAuthenticated(false); setAuthenticatedUser(null); - setError({ - message: "Token invalid", - id: "general", - }); + setError("Token invalid"); return null; } + const user = response.data; setAuthenticatedUser(user); return user; } @@ -68,21 +62,19 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children} return; } const response = await new RestService(null).login(username, password); - if (!response.success) { - console.error("Login failed:", response.message); - setError({ - message: response.message, - id: response.id, - }); + if (!response.ok) { + console.error("Login failed:", response.data.message); + setError(response.data.message!); setIsAuthenticated(false); return; } - await saveInStorage(JWT_TOKEN_KEY, response.token); - setToken(response.token); + const token = response.data.token!; + await saveInStorage(JWT_TOKEN_KEY, token); + setToken(token); // Fehler zurücksetzen setError(null); // User laden und ERST DANN isAuthenticated setzen - const user = await saveUser(response.token); + const user = await saveUser(token); setIsAuthenticated(!!user); setLoading(false); }; @@ -95,12 +87,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children} }; const refreshUser = async () => { + console.log("refreshUser") + console.log(token) if (!token) return; await saveUser(token); }; return ( - + {children} ); diff --git a/src/hooks/useSpotifyAuth.ts b/src/hooks/useSpotifyAuth.ts index f826ab5..1c40dd6 100644 --- a/src/hooks/useSpotifyAuth.ts +++ b/src/hooks/useSpotifyAuth.ts @@ -35,7 +35,12 @@ export const useSpotifyAuth = ( if (response?.type === 'success') { try { const {code} = response.params; - const token = (await new RestService(jwtToken).exchangeSpotifyCodeForToken(code)).token; + const res = (await new RestService(jwtToken).exchangeSpotifyCodeForToken(code)); + if (!res.ok) { + console.log("Fehler beim token abrufen"); + return; + } + const token = res.data.token; console.log('Token:', token); onAuthSuccess(token); } catch (error) { diff --git a/src/services/RestService.ts b/src/services/RestService.ts index 1c797ac..19a2dac 100644 --- a/src/services/RestService.ts +++ b/src/services/RestService.ts @@ -4,10 +4,6 @@ import {SpotifyConfig, User} from "@/src/model/User"; const API_URL = process.env.EXPO_PUBLIC_API_URL; -interface TokenWrapper { - token: Token -} - export interface Token { access_token: string; refresh_token: string; @@ -16,6 +12,11 @@ export interface Token { scope: string; } +export interface ApiResponse { + ok: boolean; + data: T; +} + class RestService { private readonly jwtToken: string | null; private api: AxiosInstance; @@ -54,31 +55,33 @@ class RestService { ); } - async exchangeSpotifyCodeForToken(code: string): Promise { + async exchangeSpotifyCodeForToken(code: string): Promise> { const redirectUri = makeRedirectUri({ scheme: 'led.matrix', path: 'callback', }); - return this.request( - 'GET', - `/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}` + return this.request>( + 'POST', + `/spotify/token/generate`, + {"authCode": code, "redirectUri": redirectUri}, + {'Content-Type': 'application/json'} ); } - async fetchAllUser(): Promise<{ users: User[] }> { - return this.request<{ users: User[] }>('GET', '/user'); + async fetchAllUser(): Promise> { + return this.request>('GET', '/user'); } - async fetchUserById(id: string): Promise { - return this.request('GET', `/user/${id}`); + async fetchUserById(id: string): Promise> { + return this.request>('GET', `/user/${id}`); } - async getSelf(): Promise { - return this.request('GET', '/user/me'); + async getSelf(): Promise> { + return this.request>('GET', '/user/me'); } - async changeSelfPassword(password: string, passwordConfirmation: string): Promise<{result: { success: boolean; message: string }}> { - return this.request<{result: { success: boolean; message: string } }>( + async changeSelfPassword(password: string, passwordConfirmation: string): Promise> { + return this.request>( 'PUT', '/user/me/password', {password, passwordConfirmation}, @@ -104,9 +107,9 @@ class RestService { ); } - async updateSelfSpotifyConfig(spotifyConfig?: SpotifyConfig): Promise<{ success: boolean; message: string }> { + async updateSelfSpotifyConfig(spotifyConfig?: SpotifyConfig): Promise> { const payload = spotifyConfig ?? {}; - return this.request<{ success: boolean; message: string }>( + return this.request>( 'PUT', '/user/me/spotify', payload, @@ -114,8 +117,12 @@ class RestService { ); } - async login(username: string, password: string): Promise<{ success: boolean; token: string; message: string; id: "username" | "password" }> { - return this.request<{ success: boolean; token: string; message: string; id: "username" | "password" }>( + async removeSpotifyConfig(): Promise> { + return this.request>('DELETE', '/user/me/spotify'); + } + + async login(username: string, password: string): Promise> { + return this.request>( "POST", '/auth/login', {username, password},