import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import 'package:uuid/uuid.dart'; class AuthService { AuthService({http.Client? client}) : _client = client ?? http.Client(); static const _apiOrigin = String.fromEnvironment('API_BASE_URL'); static const _accessKey = 'sportpath_access_token'; static const _refreshKey = 'sportpath_refresh_token'; static const _deviceKey = 'sportpath_device_uuid'; final FlutterSecureStorage _storage = const FlutterSecureStorage(); final http.Client _client; String? _accessToken; String? _refreshToken; String? _deviceUuid; String? _pendingChallengeId; String? _pendingPhone; bool? _pendingRegistration; Future? _refreshing; bool get isSignedIn => _refreshToken != null && _refreshToken!.isNotEmpty; bool get isApiConfigured => _apiOrigin.startsWith('https://'); String get apiBase => '${_apiOrigin.replaceFirst(RegExp(r'/+$'), '')}/api/v1'; Future init() async { _accessToken = await _storage.read(key: _accessKey); _refreshToken = await _storage.read(key: _refreshKey); _deviceUuid = await _storage.read(key: _deviceKey); _deviceUuid ??= const Uuid().v4(); await _storage.write(key: _deviceKey, value: _deviceUuid); return this; } Future requestOtp( {required String phoneE164, required bool isRegistration}) async { _requireApi(); final response = await _client .post( Uri.parse('$apiBase/auth/request-otp.php'), headers: const {'Content-Type': 'application/json'}, body: jsonEncode({ 'phone_e164': phoneE164, 'purpose': isRegistration ? 'register' : 'login', 'device_uuid': _deviceUuid, }), ) .timeout(const Duration(seconds: 20)); _throwIfNotSuccessful(response); final body = jsonDecode(response.body) as Map; _pendingChallengeId = body['challenge_id'] as String?; _pendingPhone = phoneE164; _pendingRegistration = isRegistration; if (_pendingChallengeId == null) { throw const AuthException('لم يصل معرّف التحقق من الخادم.'); } } Future verifyOtp({ required String phoneE164, required String code, required bool isRegistration, String? displayName, }) async { _requireApi(); final challengeId = _pendingChallengeId; if (challengeId == null || _pendingPhone != phoneE164 || _pendingRegistration != isRegistration) { throw const AuthException('اطلب رمز تحقق جديدًا أولًا.'); } final response = await _client .post( Uri.parse('$apiBase/auth/verify-otp.php'), headers: const {'Content-Type': 'application/json'}, body: jsonEncode({ 'challenge_id': challengeId, 'code': code, 'display_name': displayName, 'device_uuid': _deviceUuid, 'platform': defaultTargetPlatform == TargetPlatform.iOS ? 'ios' : 'android', }), ) .timeout(const Duration(seconds: 20)); _throwIfNotSuccessful(response); final body = jsonDecode(response.body) as Map; _accessToken = body['access_token'] as String?; _refreshToken = body['refresh_token'] as String?; if (_accessToken == null || _refreshToken == null) { throw const AuthException('ردّ الخادم لا يحتوي بيانات جلسة صالحة.'); } await _storage.write(key: _accessKey, value: _accessToken); await _storage.write(key: _refreshKey, value: _refreshToken); _pendingChallengeId = null; _pendingPhone = null; _pendingRegistration = null; } Future authenticatedPost(Uri uri, String body) async { _requireApi(); _accessToken ??= await _storage.read(key: _accessKey); var token = _accessToken; if (token == null && !await _refreshSession()) { throw const AuthException('سجّل الدخول لمزامنة بياناتك.'); } token = _accessToken; var response = await _client .post(uri, headers: _bearerHeaders(token!), body: body) .timeout(const Duration(seconds: 30)); if (response.statusCode == 401 && await _refreshSession()) { response = await _client .post(uri, headers: _bearerHeaders(_accessToken!), body: body) .timeout(const Duration(seconds: 30)); } return response; } Future _refreshSession() async { final activeRefresh = _refreshing; if (activeRefresh != null) return activeRefresh; final refreshFuture = _performRefresh(); _refreshing = refreshFuture; try { return await refreshFuture; } finally { if (identical(_refreshing, refreshFuture)) _refreshing = null; } } Future _performRefresh() async { final refreshToken = _refreshToken ?? await _storage.read(key: _refreshKey); if (refreshToken == null || refreshToken.isEmpty || !isApiConfigured) { return false; } try { final response = await _client .post( Uri.parse('$apiBase/auth/refresh.php'), headers: const {'Content-Type': 'application/json'}, body: jsonEncode({'refresh_token': refreshToken}), ) .timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { if (response.statusCode == 401 || response.statusCode == 403) { await clearSession(); } return false; } final body = jsonDecode(response.body) as Map; _accessToken = body['access_token'] as String?; _refreshToken = body['refresh_token'] as String?; if (_accessToken == null || _refreshToken == null) { await clearSession(); return false; } await _storage.write(key: _accessKey, value: _accessToken); await _storage.write(key: _refreshKey, value: _refreshToken); return true; } catch (error) { debugPrint('[AUTH] Refresh failed: $error'); return false; } } Future logout() async { final token = _accessToken ?? await _storage.read(key: _accessKey); if (token != null && isApiConfigured) { try { var response = await _client .post( Uri.parse('$apiBase/auth/logout.php'), headers: _bearerHeaders(token), ) .timeout(const Duration(seconds: 10)); if (response.statusCode == 401 && await _refreshSession()) { response = await _client .post( Uri.parse('$apiBase/auth/logout.php'), headers: _bearerHeaders(_accessToken!), ) .timeout(const Duration(seconds: 10)); } } catch (error) { debugPrint('[AUTH] Remote sign out failed: $error'); } } await clearSession(); } Future clearSession() async { _accessToken = null; _refreshToken = null; await _storage.delete(key: _accessKey); await _storage.delete(key: _refreshKey); } Map _bearerHeaders(String token) => { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }; void _requireApi() { if (!isApiConfigured) { throw const AuthException( 'عنوان API غير مضبوط. أضف API_BASE_URL عند بناء التطبيق.'); } } void _throwIfNotSuccessful(http.Response response) { if (response.statusCode >= 200 && response.statusCode < 300) return; String message = 'تعذر إكمال الطلب (${response.statusCode}).'; try { final body = jsonDecode(response.body) as Map; switch (body['error']) { case 'rate_limited': message = 'طلبات كثيرة. انتظر قليلًا ثم حاول مجددًا.'; break; case 'otp_provider_not_configured': message = 'خدمة الرسائل لم تُفعّل على الخادم بعد.'; break; case 'invalid_code': message = 'رمز التحقق غير صحيح.'; break; case 'invalid_or_expired_challenge': message = 'انتهت صلاحية رمز التحقق. اطلب رمزًا جديدًا.'; break; case 'verification_failed': message = 'تعذر التحقق من الحساب أو الرمز.'; break; case 'account_inactive': message = 'الحساب غير نشط. تواصل مع الدعم.'; break; case 'otp_delivery_failed': message = 'تعذر إرسال رمز التحقق. حاول لاحقًا.'; break; } } catch (_) {} throw AuthException(message); } } class AuthException implements Exception { const AuthException(this.message); final String message; @override String toString() => message; }