feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles
- Config Spec-Kit pour Spec-Driven Development - CONSTITUTION.md + .specify/memory/constitution.md - Commandes Cursor /speckit.*, règles projet - Mission: associations + mutuelles d'épargne et de financement - .gitignore: versionner config spec-kit unionflow Made-with: Cursor
This commit is contained in:
214
unionflow/unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
214
unionflow/unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
/// Client HTTP Dio configuré pour l'API UnionFlow
|
||||
library dio_client;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// Configuration du client HTTP Dio
|
||||
class DioClient {
|
||||
static const int _connectTimeout = 30000; // 30 secondes
|
||||
static const int _receiveTimeout = 30000; // 30 secondes
|
||||
static const int _sendTimeout = 30000; // 30 secondes
|
||||
|
||||
late final Dio _dio;
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
|
||||
DioClient() {
|
||||
_dio = Dio();
|
||||
_configureDio();
|
||||
}
|
||||
|
||||
/// Configuration du client Dio
|
||||
void _configureDio() {
|
||||
// Configuration de base - URL depuis AppConfig
|
||||
_dio.options = BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: const Duration(milliseconds: _connectTimeout),
|
||||
receiveTimeout: const Duration(milliseconds: _receiveTimeout),
|
||||
sendTimeout: const Duration(milliseconds: _sendTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
// Intercepteur d'authentification
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// Ajouter le token d'authentification si disponible
|
||||
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
// Gestion des erreurs d'authentification
|
||||
if (error.response?.statusCode == 401) {
|
||||
// Token expiré, essayer de le rafraîchir
|
||||
final refreshed = await _refreshToken();
|
||||
if (refreshed) {
|
||||
// Réessayer la requête avec le nouveau token
|
||||
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
|
||||
if (token != null) {
|
||||
error.requestOptions.headers['Authorization'] = 'Bearer $token';
|
||||
final response = await _dio.fetch(error.requestOptions);
|
||||
handler.resolve(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
|
||||
// Logger uniquement en mode développement
|
||||
if (AppConfig.enableLogging) {
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
logPrint: (obj) => print('DIO: $obj'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rafraîchit le token d'authentification
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
final refreshToken = await _secureStorage.read(key: 'keycloak_webview_refresh_token');
|
||||
if (refreshToken == null) return false;
|
||||
|
||||
final response = await Dio().post(
|
||||
AppConfig.keycloakTokenUrl,
|
||||
data: {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refreshToken,
|
||||
'client_id': 'unionflow-mobile',
|
||||
},
|
||||
options: Options(
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
await _secureStorage.write(key: 'keycloak_webview_access_token', value: data['access_token']);
|
||||
if (data['refresh_token'] != null) {
|
||||
await _secureStorage.write(key: 'keycloak_webview_refresh_token', value: data['refresh_token']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Erreur lors du rafraîchissement, l'utilisateur devra se reconnecter
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Obtient l'instance Dio configurée
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// Méthodes de convenance pour les requêtes HTTP
|
||||
|
||||
/// GET request
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// POST request
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// PUT request
|
||||
Future<Response<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.put<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// DELETE request
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// PATCH request
|
||||
Future<Response<T>> patch<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return _dio.patch<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
/// Interface pour vérifier la connectivité réseau
|
||||
abstract class NetworkInfo {
|
||||
Future<bool> get isConnected;
|
||||
}
|
||||
|
||||
/// Implémentation de NetworkInfo utilisant connectivity_plus
|
||||
class NetworkInfoImpl implements NetworkInfo {
|
||||
final Connectivity connectivity;
|
||||
|
||||
NetworkInfoImpl(this.connectivity);
|
||||
|
||||
@override
|
||||
Future<bool> get isConnected async {
|
||||
final result = await connectivity.checkConnectivity();
|
||||
return result.any((r) => r != ConnectivityResult.none);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user