refactor: unify API response handling and improve error state management

This commit is contained in:
StarAppeal
2025-09-06 03:48:24 +02:00
parent e63032f79d
commit a734840547
6 changed files with 66 additions and 57 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ export default function SettingsScreen() {
/>
{!!authenticatedUser?.spotifyConfig && ( <ThemedButton mode={"outlined"} title={"Remove Spotify"} onPress={() => {
const rest = new RestService(jwtToken);
rest.updateSelfSpotifyConfig().then((result) => {
rest.removeSpotifyConfig().then((result) => {
console.log("Spotify Login entfernt");
console.log(result);
refreshUser()
+4 -4
View File
@@ -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"
/>
<ThemedButton mode="outlined" onPress={onLoginPressed} title={"Login"} />
+11 -9
View File
@@ -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<ApiResponse<{ message: string }> | 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() {
<CustomModal resetCallback={resetModal} buttonTitle="Passwort ändern">
<View style={styles.modalContent}>
<Paragraph>Passwort ändern</Paragraph>
{apiResponse && apiResponse.message && (
{apiResponse && apiResponse.data?.message && (
<View
style={[styles.apiResponseBox, {backgroundColor: apiResponse.success ? theme.colors.success : theme.colors.error}]}>
style={[styles.apiResponseBox, {backgroundColor: apiResponse.ok ? theme.colors.success : theme.colors.error}]}>
<Paragraph
style={{color: apiResponse.success ? theme.colors.onSuccess : theme.colors.onError}}>{apiResponse.message}</Paragraph>
style={{color: apiResponse.ok ? theme.colors.onSuccess : theme.colors.onError}}>{apiResponse.data.message}</Paragraph>
</View>
)}
+17 -22
View File
@@ -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<void>;
logout: () => Promise<void>;
error: AuthError | null;
authenticatedUser: User | null;
error: string | null;
loading: boolean;
refreshUser: () => Promise<void>;
};
@@ -25,7 +21,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [authenticatedUser, setAuthenticatedUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<AuthError | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
@@ -45,19 +41,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}
}, []);
const saveUser = async (token: string): Promise<User | null> => {
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 (
<AuthContext.Provider value={{isAuthenticated, token, login, logout, error, authenticatedUser, loading, refreshUser}}>
<AuthContext.Provider
value={{isAuthenticated, token, login, logout, error, authenticatedUser, loading, refreshUser}}>
{children}
</AuthContext.Provider>
);
+6 -1
View File
@@ -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) {
+27 -20
View File
@@ -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<T> {
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<TokenWrapper> {
async exchangeSpotifyCodeForToken(code: string): Promise<ApiResponse<{ token: Token }>> {
const redirectUri = makeRedirectUri({
scheme: 'led.matrix',
path: 'callback',
});
return this.request<TokenWrapper>(
'GET',
`/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}`
return this.request<ApiResponse<{ token: Token }>>(
'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<ApiResponse<{ users: User[] }>> {
return this.request<ApiResponse<{ users: User[] }>>('GET', '/user');
}
async fetchUserById(id: string): Promise<User> {
return this.request<User>('GET', `/user/${id}`);
async fetchUserById(id: string): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>('GET', `/user/${id}`);
}
async getSelf(): Promise<User> {
return this.request<User>('GET', '/user/me');
async getSelf(): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>('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<ApiResponse<{ message: string }>> {
return this.request<ApiResponse<{message: string}>>(
'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<ApiResponse<{message: string}>> {
const payload = spotifyConfig ?? {};
return this.request<{ success: boolean; message: string }>(
return this.request<ApiResponse<{message: string}>>(
'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<ApiResponse<User>> {
return this.request<ApiResponse<User>>('DELETE', '/user/me/spotify');
}
async login(username: string, password: string): Promise<ApiResponse<{ message?: string, token?: string }>> {
return this.request<ApiResponse<{message?: string, token?: string}>>(
"POST",
'/auth/login',
{username, password},