/opt/canhelp/apps/mobile/lib/services
Edit: /opt/canhelp/apps/mobile/lib/services/api_client.dart (59021B)
import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kReleaseMode;
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart' as http_parser;
import '../models/app_notification.dart';
import '../models/availability.dart';
import '../models/category.dart';
import '../models/chat_room.dart';
import '../models/dashboard_data.dart';
import '../models/location.dart';
import '../models/offer.dart';
import '../models/portfolio_item.dart';
import '../models/price_list.dart';
import '../models/review.dart';
import '../models/specialist_card.dart';
import '../models/task.dart';
import '../models/user.dart';
import 'storage_service.dart';
class ApiException implements Exception {
final int statusCode;
final String message;
final Map
? body;
ApiException(this.statusCode, this.message, [this.body]);
@override
String toString() => 'ApiException($statusCode): $message';
}
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
ApiClient._internal();
final StorageService _storage = StorageService();
final http.Client _http = http.Client();
/// Base URL without any path suffix.
/// Prefer the build-time API_URL define, then fall back to production,
/// then platform-specific localhost for local dev.
String get origin {
const env = String.fromEnvironment('API_URL');
if (env.isNotEmpty) return env;
if (kReleaseMode) return 'https://api.canhelp.gr';
try {
if (Platform.isAndroid) return 'http://10.0.2.2:4000';
} catch (_) {}
return 'http://localhost:4000';
}
String get _origin => origin;
/// REST API base: /api
String get baseUrl => '$_origin/api';
/// Better Auth base: /api/auth
String get _authUrl => '$_origin/api/auth';
String? _webVerificationCallback() {
const webUrl = String.fromEnvironment('WEB_URL');
if (webUrl.isEmpty) return null;
return '${webUrl.replaceAll(RegExp(r'/$'), '')}/dashboard';
}
// ── helpers ──────────────────────────────────────────────
Map _jsonHeaders([String? token]) => {
'Content-Type': 'application/json',
'Accept': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
Future _handleResponse(http.Response res) async {
if (res.statusCode >= 200 && res.statusCode < 300) {
if (res.body.isEmpty) return {};
return jsonDecode(res.body);
}
final Map body = res.body.isNotEmpty
? jsonDecode(res.body) as Map
: {};
final message =
(body['message'] ?? body['error'] ?? 'Request failed') as String;
throw ApiException(res.statusCode, message, body);
}
Future _request(
String method,
String path, {
Map? body,
Map? queryParams,
bool auth = false,
}) async {
final uri = Uri.parse(
'$baseUrl$path',
).replace(queryParameters: queryParams);
final token = auth ? await _storage.getAccessToken() : null;
final headers = _jsonHeaders(token);
http.Response res;
switch (method) {
case 'GET':
res = await _http.get(uri, headers: headers);
case 'POST':
res = await _http.post(uri, headers: headers, body: jsonEncode(body));
case 'PATCH':
res = await _http.patch(uri, headers: headers, body: jsonEncode(body));
case 'DELETE':
res = await _http.delete(uri, headers: headers);
default:
throw UnsupportedError('HTTP method $method');
}
return _handleResponse(res);
}
// ── Auth (Better Auth endpoints) ─────────────────────────
/// Extracts the Bearer token from a Better Auth response.
String? _extractToken(http.Response res, Map data) {
final bodyToken = data['token'] as String?;
if (bodyToken != null && bodyToken.isNotEmpty) return bodyToken;
return res.headers['set-auth-token'];
}
/// Sign in with email + password.
Future