feat: implement API client with JSON handling and error mapping; enhance routing and state management in app shell
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'api_exception.dart';
|
||||
|
||||
/// Platform-neutral HTTP client.
|
||||
/// API base URL is injected at build time via --dart-define=API_BASE_URL.
|
||||
/// Default is same-origin '/api' to avoid mixed-content on HTTPS sites.
|
||||
@@ -19,18 +23,117 @@ class ApiClient {
|
||||
if (token != null) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
|
||||
Future<http.Response> get(String path, {String? token}) =>
|
||||
_client.get(Uri.parse('$baseUrl$path'), headers: _headers(token: token));
|
||||
Future<dynamic> getJson(String path, {String? token}) async {
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token),
|
||||
);
|
||||
return _decodeOrNull(_guardResponse(response));
|
||||
}
|
||||
|
||||
Future<http.Response> post(String path, String body, {String? token}) =>
|
||||
_client.post(Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token), body: body);
|
||||
Future<dynamic> postJson(
|
||||
String path, {
|
||||
Object? body,
|
||||
String? token,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token),
|
||||
body: body == null ? null : jsonEncode(body),
|
||||
);
|
||||
return _decodeOrNull(_guardResponse(response));
|
||||
}
|
||||
|
||||
Future<http.Response> put(String path, String body, {String? token}) =>
|
||||
_client.put(Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token), body: body);
|
||||
Future<dynamic> putJson(
|
||||
String path, {
|
||||
Object? body,
|
||||
String? token,
|
||||
}) async {
|
||||
final response = await _client.put(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token),
|
||||
body: body == null ? null : jsonEncode(body),
|
||||
);
|
||||
return _decodeOrNull(_guardResponse(response));
|
||||
}
|
||||
|
||||
Future<http.Response> delete(String path, {String? token}) =>
|
||||
_client.delete(Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token));
|
||||
Future<dynamic> deleteJson(String path, {String? token}) async {
|
||||
final response = await _client.delete(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token),
|
||||
);
|
||||
return _decodeOrNull(_guardResponse(response));
|
||||
}
|
||||
|
||||
http.Response _guardResponse(http.Response response) {
|
||||
if (response.statusCode < 400) {
|
||||
return response;
|
||||
}
|
||||
|
||||
final parsedBody = _decodeOrNull(response);
|
||||
final serverMessage = _extractMessage(parsedBody);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
throw ApiException(
|
||||
type: ApiErrorType.unauthorized,
|
||||
statusCode: 401,
|
||||
message: serverMessage ?? 'Unauthorized',
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode == 403) {
|
||||
throw ApiException(
|
||||
type: ApiErrorType.forbidden,
|
||||
statusCode: 403,
|
||||
message: serverMessage ?? 'Forbidden',
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode >= 500) {
|
||||
throw ApiException(
|
||||
type: ApiErrorType.server,
|
||||
statusCode: response.statusCode,
|
||||
message: serverMessage ?? 'Server error',
|
||||
);
|
||||
}
|
||||
|
||||
throw ApiException(
|
||||
type: ApiErrorType.unknown,
|
||||
statusCode: response.statusCode,
|
||||
message: serverMessage ?? 'Request failed',
|
||||
);
|
||||
}
|
||||
|
||||
dynamic _decodeOrNull(http.Response response) {
|
||||
final body = response.body.trim();
|
||||
if (body.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return jsonDecode(body);
|
||||
} catch (_) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
String? _extractMessage(dynamic parsedBody) {
|
||||
if (parsedBody is Map<String, dynamic>) {
|
||||
final message = parsedBody['message'];
|
||||
if (message is String && message.trim().isNotEmpty) {
|
||||
return message;
|
||||
}
|
||||
if (message is List && message.isNotEmpty) {
|
||||
return message.first.toString();
|
||||
}
|
||||
final error = parsedBody['error'];
|
||||
if (error is String && error.trim().isNotEmpty) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (parsedBody is String && parsedBody.trim().isNotEmpty) {
|
||||
return parsedBody;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'api_exception.dart';
|
||||
|
||||
String mapErrorToUserMessage(Object error) {
|
||||
if (error is ApiException) {
|
||||
switch (error.type) {
|
||||
case ApiErrorType.unauthorized:
|
||||
return 'Din session har gatt ut. Logga in igen.';
|
||||
case ApiErrorType.forbidden:
|
||||
return 'Du saknar behorighet for denna funktion.';
|
||||
case ApiErrorType.server:
|
||||
return 'Serverfel uppstod. Forsok igen om en stund.';
|
||||
case ApiErrorType.network:
|
||||
return 'Natverksfel. Kontrollera anslutningen och forsok igen.';
|
||||
case ApiErrorType.unknown:
|
||||
return error.message.isNotEmpty
|
||||
? error.message
|
||||
: 'Ett ovantat fel uppstod.';
|
||||
}
|
||||
}
|
||||
return 'Ett ovantat fel uppstod.';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
enum ApiErrorType {
|
||||
unauthorized,
|
||||
forbidden,
|
||||
server,
|
||||
network,
|
||||
unknown,
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
final ApiErrorType type;
|
||||
final int? statusCode;
|
||||
final String message;
|
||||
|
||||
const ApiException({
|
||||
required this.type,
|
||||
required this.message,
|
||||
this.statusCode,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final status = statusCode == null ? '' : ' (HTTP $statusCode)';
|
||||
return 'ApiException$type$status: $message';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user