Update: 2026-08-06 18:32:03

This commit is contained in:
Hamza-Ayed
2026-08-06 18:32:03 +03:00
parent c759c80c85
commit ca3d456795
8 changed files with 416 additions and 41 deletions
+4
View File
@@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m
android.useAndroidX=true
android.enableJetifier=true
android.useLibSizedPageAlignment=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+60 -23
View File
@@ -1,8 +1,41 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'api_config.dart';
import 'hmac_interceptor.dart';
import '../storage/secure_storage.dart';
/// Request/response fields that must never reach a log sink.
const _sensitiveKeys = {
'password',
'password_confirmation',
'old_password',
'new_password',
'otp',
'access_token',
'refresh_token',
'device_secret',
'push_token',
};
/// Replaces sensitive values with a placeholder, recursing into nested maps.
///
/// Logging raw payloads printed live credentials into Xcode/logcat, where they
/// persist well beyond the debug session.
Object? _redact(Object? data) {
if (data is Map) {
return data.map((key, value) => MapEntry(
key,
_sensitiveKeys.contains(key.toString().toLowerCase())
? '***'
: _redact(value),
));
}
if (data is List) {
return data.map(_redact).toList();
}
return data;
}
class DioClient {
static const String baseUrl = ApiConfig.baseUrl;
late final Dio dio;
@@ -23,29 +56,33 @@ class DioClient {
// Add Interceptors
dio.interceptors.add(HmacInterceptor(SecureStorage()));
// Custom Logging Interceptor as requested
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('--- API REQUEST ---');
print('URL: ${options.method} ${options.uri}');
print('Payload: ${options.data}');
return handler.next(options);
},
onResponse: (response, handler) {
print('--- API RESPONSE ---');
print('URL: ${response.requestOptions.method} ${response.requestOptions.uri}');
print('Status Code: ${response.statusCode}');
print('Response: ${response.data}');
return handler.next(response);
},
onError: (DioException e, handler) {
print('--- API ERROR ---');
print('URL: ${e.requestOptions.method} ${e.requestOptions.uri}');
print('Status Code: ${e.response?.statusCode}');
print('Response: ${e.response?.data ?? e.message}');
return handler.next(e);
},
));
// Logging interceptor. Debug builds only, and credentials are redacted:
// these used to be bare print() calls, which also ran in release builds.
if (kDebugMode) {
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
debugPrint('--- API REQUEST ---');
debugPrint('URL: ${options.method} ${options.uri}');
debugPrint('Payload: ${_redact(options.data)}');
return handler.next(options);
},
onResponse: (response, handler) {
debugPrint('--- API RESPONSE ---');
debugPrint(
'URL: ${response.requestOptions.method} ${response.requestOptions.uri}');
debugPrint('Status Code: ${response.statusCode}');
debugPrint('Response: ${_redact(response.data)}');
return handler.next(response);
},
onError: (DioException e, handler) {
debugPrint('--- API ERROR ---');
debugPrint('URL: ${e.requestOptions.method} ${e.requestOptions.uri}');
debugPrint('Status Code: ${e.response?.statusCode}');
debugPrint('Response: ${_redact(e.response?.data) ?? e.message}');
return handler.next(e);
},
));
}
}
Dio get client => dio;
@@ -20,6 +20,37 @@ class AuthController extends GetxController {
var isLoading = false.obs;
var phone = ''.obs;
/// Turns a DioException into a message that reflects what actually failed.
///
/// Without this, any transport failure (expired TLS cert, no network, DNS)
/// surfaced as "wrong credentials", which sent users hunting for a password
/// problem that did not exist.
String _describeDioError(DioException e,
{String fallback = 'بيانات الدخول غير صحيحة'}) {
final data = e.response?.data;
if (data is Map && data['message'] != null) {
return data['message'].toString();
}
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return 'انتهت مهلة الاتصال بالخادم. تحقق من اتصالك بالإنترنت.';
case DioExceptionType.badCertificate:
return 'تعذّر التحقق من شهادة أمان الخادم. يرجى التواصل مع الدعم.';
case DioExceptionType.connectionError:
case DioExceptionType.unknown:
final msg = e.error?.toString() ?? e.message ?? '';
if (msg.contains('CERTIFICATE') || msg.contains('HandshakeException')) {
return 'تعذّر التحقق من شهادة أمان الخادم. يرجى التواصل مع الدعم.';
}
return 'تعذّر الاتصال بالخادم. تحقق من اتصالك بالإنترنت.';
default:
return fallback;
}
}
Future<void> requestOtp(String phoneNumber) async {
try {
if (phoneNumber.trim().isEmpty) {
@@ -120,9 +151,9 @@ class AuthController extends GetxController {
}
}
} on DioException catch (e, stackTrace) {
AppLogger.error('OTP Verify Failed', e.response?.data, stackTrace);
AppLogger.error('OTP Verify Failed', e.response?.data ?? e.message, stackTrace);
AppSnackbar.showError(
'خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
'خطأ', _describeDioError(e, fallback: 'رمز التحقق غير صحيح'));
} finally {
isLoading.value = false;
}
@@ -199,12 +230,8 @@ class AuthController extends GetxController {
}
}
} on DioException catch (e, stackTrace) {
AppLogger.error('Email Login Failed', e.response?.data, stackTrace);
String errorMessage = 'بيانات الدخول غير صحيحة';
if (e.response?.data != null && e.response?.data is Map) {
errorMessage = e.response?.data['message'] ?? errorMessage;
}
AppSnackbar.showError('خطأ', errorMessage);
AppLogger.error('Email Login Failed', e.response?.data ?? e.message, stackTrace);
AppSnackbar.showError('خطأ', _describeDioError(e));
} finally {
isLoading.value = false;
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: musadaq_app
description: Jordanian E-Invoicing Automation SaaS
publish_to: 'none'
version: 1.0.6+6
version: 1.0.7+7
environment:
sdk: '>=3.2.0 <4.0.0'