Created
October 21, 2024 10:41
-
-
Save Software78/b4ecbfde5677c932613d2fedd0b80046 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ignore_for_file: avoid_dynamic_calls | |
| import 'dart:convert'; | |
| import 'dart:developer'; | |
| import 'dart:io'; | |
| // ignore: depend_on_referenced_packages | |
| import 'package:crypto/crypto.dart'; | |
| import 'package:dio/dio.dart' as dio; | |
| import 'package:duduzili/app/service_locator.dart'; | |
| import 'package:duduzili/core/api/encrypter.dart'; | |
| import 'package:duduzili/core/storage/cache_storage.dart'; | |
| import 'package:duduzili/env/env.dart'; | |
| import 'package:firebase_performance_dio/firebase_performance_dio.dart'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:freezed_annotation/freezed_annotation.dart'; | |
| import 'package:pretty_dio_logger/pretty_dio_logger.dart'; | |
| part 'api_helper.freezed.dart'; | |
| // part 'api_helper.g.dart'; | |
| enum MethodType { get, post, put, delete, patch } | |
| @freezed | |
| class ApiResponse<T> with _$ApiResponse<T> { | |
| const factory ApiResponse.success({ApiSuccess<T>? data}) = _Success<T>; | |
| const factory ApiResponse.error(ApiError error) = _Error<T>; | |
| } | |
| @freezed | |
| class ListApiResponse<T> with _$ListApiResponse<T> { | |
| const factory ListApiResponse.success({ListApiSuccess<T>? data}) = | |
| _ListSuccess<T>; | |
| const factory ListApiResponse.error(ApiError error) = _ListError<T>; | |
| } | |
| class ApiSuccess<T> { | |
| ApiSuccess({ | |
| this.message, | |
| this.status, | |
| this.data, | |
| this.accessToken, | |
| }); | |
| factory ApiSuccess.fromJson( | |
| Map<String, dynamic> json, | |
| T? Function(Map<String, dynamic> json)? fromJson, | |
| ) { | |
| return ApiSuccess( | |
| message: (json['message']) as String?, | |
| status: json['status'] as bool?, | |
| accessToken: json['access_token'] as String?, | |
| data: fromJson?.call(json['data'] as Map<String, dynamic>), | |
| ); | |
| } | |
| String? message; | |
| String? accessToken; | |
| bool? status; | |
| T? data; | |
| @override | |
| String toString() => | |
| 'ApiSuccess<$T>(message: $message, status: $status, data: $data)'; | |
| } | |
| class ListApiSuccess<T> { | |
| ListApiSuccess({ | |
| this.message, | |
| this.status, | |
| this.data, | |
| }); | |
| factory ListApiSuccess.fromJson( | |
| Map<String, dynamic> json, | |
| T Function(Map<String, dynamic> json) fromJson, | |
| ) { | |
| return ListApiSuccess( | |
| message: json['message'] as String?, | |
| status: json['status'] as bool?, | |
| data: (json['data'] as List) | |
| .map((e) => fromJson(e as Map<String, dynamic>)) | |
| .toList(), | |
| ); | |
| } | |
| String? message; | |
| bool? status; | |
| List<T>? data; | |
| @override | |
| String toString() => | |
| 'ListApiSuccess<$T>(message: $message, status: $status, data: $data)'; | |
| } | |
| class ApiError { | |
| ApiError({ | |
| this.message, | |
| this.code, | |
| this.success, | |
| this.detail, | |
| this.data, | |
| }); | |
| factory ApiError.unknown() => ApiError(message: 'Unknown error occurred'); | |
| factory ApiError.fromJson(Map<String, dynamic> json) { | |
| return ApiError( | |
| message: json['message'] as String?, | |
| code: json['code'] as int?, | |
| success: json['success'] as bool?, | |
| detail: json['data']['detail'] as String?, | |
| data: json['data'] as String?, | |
| ); | |
| } | |
| String? message; | |
| int? code; | |
| bool? success; | |
| String? detail; | |
| dynamic data; | |
| @override | |
| String toString() { | |
| return '''ApiError(message: $message, code: $code, success: $success, detail: $detail, data: $data)'''; | |
| } | |
| } | |
| class ApiHandler { | |
| ApiHandler({required String baseUrl}) { | |
| _dio = dio.Dio(dio.BaseOptions(baseUrl: baseUrl)) | |
| ..options.connectTimeout = const Duration(minutes: 1) | |
| ..options.receiveTimeout = const Duration(minutes: 1) | |
| ..options.sendTimeout = const Duration(minutes: 1) | |
| ..interceptors.add(DioFirebasePerformanceInterceptor()) | |
| ..interceptors.add(DuduziliSecurityInterceptor()) | |
| ..interceptors.add(DuduziliEncryptionInterceptor()) | |
| ..interceptors.add( | |
| PrettyDioLogger( | |
| requestBody: true, | |
| requestHeader: true, | |
| responseHeader: true, | |
| logPrint: (value) { | |
| if (kDebugMode) { | |
| log(value.toString(), name: 'Dio'); | |
| } | |
| }, | |
| ), | |
| ); | |
| } | |
| late final dio.Dio _dio; | |
| void addToken(String token) { | |
| _dio.options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| void clearToken() { | |
| _dio.options.headers.remove('Authorization'); | |
| } | |
| Future<ApiResponse<T>> request<T>({ | |
| required String path, | |
| required MethodType method, | |
| Map<String, dynamic>? payload, | |
| Map<String, String>? headers, | |
| Map<String, dynamic>? queryParameters, | |
| T? Function(Map<String, dynamic> json)? responseMapper, | |
| bool authenticate = true, | |
| Map<String, dynamic>? files, | |
| }) async { | |
| final formData = dio.FormData.fromMap(payload ?? {}); | |
| if (files != null) { | |
| for (final item in files.entries) { | |
| if (item.value is List<File>) { | |
| final fileList = item.value as List<File>; | |
| formData.files.addAll( | |
| List.generate(fileList.length, (i) { | |
| final file = fileList[i]; | |
| return MapEntry( | |
| item.key, | |
| dio.MultipartFile.fromFileSync( | |
| file.path, | |
| filename: file.path.split('/').last, | |
| ), | |
| ); | |
| }), | |
| ); | |
| } else if (item.value is File) { | |
| final file = item.value as File; | |
| formData.files.add( | |
| MapEntry( | |
| item.key, | |
| dio.MultipartFile.fromFileSync( | |
| file.path, | |
| filename: file.path.split('/').last, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| try { | |
| if (!authenticate) { | |
| _dio.options.headers.remove('Authorization'); | |
| } else { | |
| final token = locator.get<SharedPrefs>().accessToken; | |
| if (token != null) { | |
| _dio.options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| } | |
| dio.Response<Map<String, dynamic>> response; | |
| switch (method) { | |
| case MethodType.get: | |
| response = await _dio.get( | |
| path, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.post: | |
| response = await _dio.post( | |
| path, | |
| data: files != null ? formData : payload, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.put: | |
| response = await _dio.put( | |
| path, | |
| data: files != null ? formData : payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.delete: | |
| response = await _dio.delete( | |
| path, | |
| data: files != null ? formData : payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.patch: | |
| response = await _dio.patch( | |
| path, | |
| data: files != null ? formData : payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| } | |
| final successResponse = | |
| ApiSuccess.fromJson(response.data ?? {}, responseMapper); | |
| final finalResponse = ApiResponse<T>.success( | |
| data: successResponse, | |
| ); | |
| log(finalResponse.toString(), name: 'DioResponse'); | |
| return finalResponse; | |
| } on dio.DioException catch (e) { | |
| log('Error: $e', name: 'DioError'); | |
| // log('StackTrace: $s', name: 'DioError'); | |
| final errorData = ((e.response?.data != null && e.response?.data is Map) | |
| ? ((e.response?.data as Map)['data']) as dynamic | |
| : '') | |
| .toString() | |
| .replaceAll('{', '') | |
| .replaceAll('}', '') | |
| .split(':') | |
| .last | |
| .replaceAll('[', '') | |
| .replaceAll(']', ''); | |
| final error = ApiResponse<T>.error( | |
| ApiError( | |
| message: (e.response?.data != null && e.response?.data is Map) | |
| ? ((e.response?.data as Map)['message']) as String? | |
| : 'Internal server error', | |
| data: errorData, | |
| // ignore: avoid_bool_literals_in_conditional_expressions | |
| success: (e.response?.data != null && e.response?.data is Map) | |
| ? ((e.response?.data as Map)['success']) as bool | |
| : false, | |
| detail: (e.response?.data != null && e.response?.data is Map) | |
| ? ((e.response?.data as Map)['detail']) as String? | |
| : null, | |
| )..code = e.response?.statusCode ?? 500, | |
| ); | |
| return error; | |
| } catch (e) { | |
| log('Error: $e', name: 'DioError'); | |
| // log('StackTrace: $s', name: 'DioError'); | |
| final error = ApiResponse<T>.error( | |
| ApiError(message: 'Unknown error occurred: $e')..code = 500, | |
| ); | |
| return error; | |
| } | |
| } | |
| Future<ListApiResponse<T>> requestList<T>({ | |
| required String path, | |
| required MethodType method, | |
| Map<String, dynamic>? payload, | |
| Map<String, String>? headers, | |
| Map<String, dynamic>? queryParameters, | |
| T Function(Map<String, dynamic> json)? responseMapper, | |
| bool authenticate = true, | |
| }) async { | |
| try { | |
| if (!authenticate) { | |
| _dio.options.headers.remove('Authorization'); | |
| } else { | |
| final token = locator.get<SharedPrefs>().accessToken; | |
| if (token != null) { | |
| _dio.options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| } | |
| dio.Response<Map<String, dynamic>> response; | |
| switch (method) { | |
| case MethodType.get: | |
| response = await _dio.get( | |
| path, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.post: | |
| response = await _dio.post( | |
| path, | |
| data: payload, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.put: | |
| response = await _dio.put( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.delete: | |
| response = await _dio.delete( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.patch: | |
| response = await _dio.patch( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| } | |
| final successResponse = ListApiSuccess<T>.fromJson( | |
| response.data ?? {}, | |
| responseMapper ?? (json) => json as T, | |
| ); | |
| final finalResponse = ListApiResponse<T>.success( | |
| data: successResponse, | |
| ); | |
| log(finalResponse.toString(), name: 'DioResponse'); | |
| return finalResponse; | |
| } on dio.DioException catch (e) { | |
| final error = ListApiResponse<T>.error( | |
| ApiError( | |
| message: (e.response?.data != null && e.response?.data is Map) | |
| ? ((e.response?.data as Map)['message']) as String? | |
| : 'Internal server error', | |
| )..code = e.response?.statusCode ?? 500, | |
| ); | |
| return error; | |
| } catch (e) { | |
| final error = ListApiResponse<T>.error( | |
| ApiError(message: 'Unknown error occurred: $e')..code = 500, | |
| ); | |
| return error; | |
| } | |
| } | |
| Future<T?> custom<T>({ | |
| required String path, | |
| required MethodType method, | |
| Map<String, dynamic>? payload, | |
| Map<String, String>? headers, | |
| Map<String, dynamic>? queryParameters, | |
| T? Function(Map<String, dynamic> json)? responseMapper, | |
| bool authenticate = true, | |
| }) async { | |
| try { | |
| if (!authenticate) { | |
| _dio.options.headers.remove('Authorization'); | |
| } else { | |
| final token = locator.get<SharedPrefs>().accessToken; | |
| if (token != null) { | |
| _dio.options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| } | |
| dio.Response<Map<String, dynamic>> response; | |
| switch (method) { | |
| case MethodType.get: | |
| response = await _dio.get( | |
| path, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.post: | |
| response = await _dio.post( | |
| path, | |
| data: payload, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.put: | |
| response = await _dio.put( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.delete: | |
| response = await _dio.delete( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| case MethodType.patch: | |
| response = await _dio.patch( | |
| path, | |
| data: payload, | |
| queryParameters: queryParameters, | |
| options: dio.Options( | |
| headers: headers, | |
| ), | |
| ); | |
| } | |
| return responseMapper?.call(response.data ?? {}); | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| Future<List<ApiResponse<dynamic>>> chainRequests({ | |
| required List<RequestConfig<dynamic>> requests, | |
| }) async { | |
| assert( | |
| requests.isNotEmpty, | |
| 'requests cannot be empty', | |
| ); | |
| final responses = <ApiResponse<dynamic>>[]; | |
| for (final item in requests) { | |
| responses.add( | |
| await request( | |
| path: item.path, | |
| method: item.method, | |
| files: item.files, | |
| authenticate: item.authenticate, | |
| headers: item.headers, | |
| payload: item.payload, | |
| queryParameters: item.queryParameters, | |
| responseMapper: item.responseMapper, | |
| ), | |
| ); | |
| } | |
| return responses; | |
| } | |
| } | |
| class RequestConfig<T> { | |
| RequestConfig({ | |
| required this.path, | |
| required this.method, | |
| this.payload, | |
| this.headers, | |
| this.queryParameters, | |
| this.authenticate = true, | |
| this.responseMapper, | |
| this.files, | |
| }); | |
| final String path; | |
| final MethodType method; | |
| final Map<String, dynamic>? payload; | |
| final Map<String, String>? headers; | |
| final Map<String, dynamic>? queryParameters; | |
| final T? Function(Map<String, dynamic> json)? responseMapper; | |
| final bool authenticate; | |
| final Map<String, dynamic>? files; | |
| } | |
| class DuduziliSecurityInterceptor extends dio.InterceptorsWrapper { | |
| @override | |
| void onRequest( | |
| dio.RequestOptions options, | |
| dio.RequestInterceptorHandler handler, | |
| ) { | |
| final dateTime = DateTime.now().millisecondsSinceEpoch; | |
| final hashKey = '${Env().apiKey}${Env().secretKey}$dateTime'; | |
| final encodedHash = utf8.encode(hashKey); | |
| final hash = sha256.convert(encodedHash); | |
| options.headers.addAll({ | |
| 'API-KEY': Env().apiKey, | |
| 'HASH-KEY': hash, | |
| 'IDEMPOTENCY-KEY': dateTime, | |
| }); | |
| handler.next(options); | |
| } | |
| } | |
| class DuduziliEncryptionInterceptor extends dio.InterceptorsWrapper { | |
| @override | |
| void onRequest( | |
| dio.RequestOptions options, | |
| dio.RequestInterceptorHandler handler, | |
| ) { | |
| if (options.data is Map<String, dynamic>) { | |
| options.data = encryptMap(options.data as Map<String, dynamic>); | |
| } else if (options.data is dio.FormData) { | |
| final fields = <String, dynamic>{}; | |
| final files = <String, dynamic>{}; | |
| log('files: $files', name: 'Dio Encrypt'); | |
| final formData = options.data as dio.FormData; | |
| for (final item in formData.fields) { | |
| if (item.value.isNotEmpty) { | |
| fields[item.key] = item.value; | |
| } else { | |
| fields.remove(item.key); | |
| } | |
| } | |
| for (final item in formData.files) { | |
| files[item.key] = item.value; | |
| } | |
| log('files: ${formData.files}', name: 'Dio Encrypt'); | |
| final encodedFormData = dio.FormData.fromMap({ | |
| ...encryptMap(fields), | |
| 'media': formData.files.map((e) => e.value).toList(), | |
| }); | |
| options.data = encodedFormData; | |
| } | |
| handler.next(options); | |
| } | |
| @override | |
| void onResponse( | |
| dio.Response<dynamic> response, | |
| dio.ResponseInterceptorHandler handler, | |
| ) { | |
| try { | |
| if (response.data != null) { | |
| if (response.data['data'] is List) { | |
| response.data['data'] = | |
| decryptList(response.data['data'] as List<dynamic>); | |
| } else { | |
| if (response.data is Map<String, dynamic>) { | |
| if ((response.data as Map<String, dynamic>).containsKey('data')) { | |
| response.data['data'] = decryptMap( | |
| (response.data['data'] as Map<String, dynamic>?) ?? {}, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| handler.next(response); | |
| } catch (e) { | |
| log('Error decrypting response: $e', name: 'Dio Decrypt'); | |
| handler.next(response); | |
| } | |
| } | |
| } | |
| Map<String, dynamic> encryptMap(Map<String, dynamic> payload) { | |
| final decryptedPayload = <String, dynamic>{}; | |
| final encrypter = locator.get<AESCryptoSystem>(); | |
| payload.forEach((key, value) { | |
| if (value is String) { | |
| // Encrypt string values | |
| decryptedPayload[key] = encrypter.encrypt(value); | |
| } else if (value is Map<String, dynamic>) { | |
| // Recursively encrypt nested maps | |
| decryptedPayload[key] = encryptMap(value); | |
| } else if (value is List<dynamic>) { | |
| // Recursively encrypt list elements | |
| decryptedPayload[key] = encryptList(value); | |
| } else { | |
| // Handle other data types (if needed) | |
| decryptedPayload[key] = value; | |
| } | |
| }); | |
| return decryptedPayload; | |
| } | |
| List<dynamic> encryptList(List<dynamic> list) { | |
| final encryptedList = <dynamic>[]; | |
| final encrypter = locator.get<AESCryptoSystem>(); | |
| for (final element in list) { | |
| if (element is String) { | |
| // Encrypt string elements | |
| encryptedList.add(encrypter.encrypt(element)); | |
| } else if (element is Map<String, dynamic>) { | |
| // Recursively encrypt nested maps | |
| encryptedList.add(encryptMap(element)); | |
| } else if (element is List<dynamic>) { | |
| // Recursively encrypt nested lists | |
| encryptedList.add( | |
| encryptList( | |
| element, | |
| ), | |
| ); | |
| } else { | |
| // Handle other data types (if needed) | |
| encryptedList.add(element); | |
| } | |
| } | |
| return encryptedList; | |
| } | |
| Map<String, dynamic> decryptMap(Map<String, dynamic> payload) { | |
| final encryptedPayload = <String, dynamic>{}; | |
| final encrypter = locator.get<AESCryptoSystem>(); | |
| payload.forEach((key, value) { | |
| if (value is String) { | |
| final decryptedValue = encrypter.decrypt(value); | |
| final number = num.tryParse(decryptedValue); | |
| if (number != null) { | |
| encryptedPayload[key] = number; | |
| return; | |
| } | |
| if (decryptedValue.toLowerCase() == 'true' || | |
| decryptedValue.toLowerCase() == 'false') { | |
| encryptedPayload[key] = decryptedValue.toLowerCase() == 'true'; | |
| return; | |
| } | |
| encryptedPayload[key] = encrypter.decrypt(value); | |
| } else if (value is Map<String, dynamic>) { | |
| // Recursively encrypt nested maps | |
| encryptedPayload[key] = decryptMap(value); | |
| } else if (value is List<dynamic>) { | |
| // Recursively encrypt list elements | |
| encryptedPayload[key] = decryptList(value); | |
| } else if (value is List<Map<String, dynamic>>) { | |
| // Recursively encrypt list elements | |
| encryptedPayload[key] = value.map(decryptMap).toList(); | |
| } else { | |
| // Handle other data types (if needed) | |
| encryptedPayload[key] = value; | |
| } | |
| }); | |
| return encryptedPayload; | |
| } | |
| List<dynamic> decryptList(List<dynamic> list) { | |
| final decryptedList = <dynamic>[]; | |
| final encrypter = locator.get<AESCryptoSystem>(); | |
| for (final element in list) { | |
| if (element is String) { | |
| // Encrypt string elements | |
| decryptedList.add(encrypter.decrypt(element)); | |
| } else if (element is Map<String, dynamic>) { | |
| // Recursively encrypt nested maps | |
| decryptedList.add(decryptMap(element)); | |
| } else if (element is List<dynamic>) { | |
| // Recursively encrypt nested lists | |
| decryptedList.add( | |
| decryptList( | |
| element, | |
| ), | |
| ); | |
| } else { | |
| // Handle other data types (if needed) | |
| decryptedList.add(element); | |
| } | |
| } | |
| return decryptedList; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class PaginationResponse<T> { | |
| PaginationResponse(); | |
| factory PaginationResponse.fromJson( | |
| Map<String, dynamic> json, | |
| T Function(Object? json) fromJsonT, | |
| ) { | |
| return PaginationResponse<T>() | |
| ..count = json['count'] as num | |
| ..next = json['next'] as String | |
| ..previous = json['previous'] as String | |
| ..results = | |
| (json['results'] as List<Object?>).map((e) => fromJsonT(e)).toList(); | |
| } | |
| late num count; | |
| late String next; | |
| late String previous; | |
| late List<T> results; | |
| @override | |
| String toString() { | |
| return '''PaginationModel<$T>(count: $count, next: $next, previous: $previous, results: $results)'''; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment