networking and error handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:provider/single_child_widget.dart';
|
import 'package:provider/single_child_widget.dart';
|
||||||
|
import 'package:tm_app/core/config/network_manager.dart';
|
||||||
import 'package:tm_app/core/theme/theme_provider.dart';
|
import 'package:tm_app/core/theme/theme_provider.dart';
|
||||||
import 'package:tm_app/data/repositories/home_repository.dart';
|
import 'package:tm_app/data/repositories/home_repository.dart';
|
||||||
import 'package:tm_app/data/repositories/tender_detail_repository.dart';
|
import 'package:tm_app/data/repositories/tender_detail_repository.dart';
|
||||||
@@ -14,7 +15,6 @@ import 'package:tm_app/view_models/your_tenders_view_model.dart';
|
|||||||
|
|
||||||
import '../../data/repositories/auth_repository.dart';
|
import '../../data/repositories/auth_repository.dart';
|
||||||
import '../../data/services/auth_service.dart';
|
import '../../data/services/auth_service.dart';
|
||||||
import '../../data/services/network_manager.dart';
|
|
||||||
import '../../data/services/tenders_service.dart';
|
import '../../data/services/tenders_service.dart';
|
||||||
import '../../view_models/auth_view_model.dart';
|
import '../../view_models/auth_view_model.dart';
|
||||||
import '../../view_models/home_view_model.dart';
|
import '../../view_models/home_view_model.dart';
|
||||||
@@ -87,9 +87,6 @@ List<SingleChildWidget> get viewModels {
|
|||||||
(context) =>
|
(context) =>
|
||||||
YourTendersViewModel(yourTendersRepository: context.read()),
|
YourTendersViewModel(yourTendersRepository: context.read()),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (context) => TendersViewModel(tendersRepository: context.read()),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../utils/app_exceptions.dart';
|
||||||
|
import '../utils/logger.dart';
|
||||||
|
import '../utils/network_connectivity.dart';
|
||||||
|
import '../utils/result.dart';
|
||||||
|
|
||||||
|
class NetworkManager {
|
||||||
|
final Dio mainDio;
|
||||||
|
final NetworkConnectivity _connectivity = NetworkConnectivity();
|
||||||
|
|
||||||
|
NetworkManager()
|
||||||
|
: mainDio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
// baseUrl: 'https://stage.cura-bau.de',
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
sendTimeout: const Duration(seconds: 10),
|
||||||
|
contentType: 'application/json',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
await addInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addInterceptors() async {
|
||||||
|
final requestInterceptor = InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
appLogger.info('🌐 Request: ${options.method} ${options.path}');
|
||||||
|
appLogger.info('🌐 Headers: ${options.headers}');
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
onResponse: (response, handler) {
|
||||||
|
appLogger.info(
|
||||||
|
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
|
||||||
|
);
|
||||||
|
appLogger.info('✅ Response data: ${response.data}');
|
||||||
|
return handler.next(response);
|
||||||
|
},
|
||||||
|
onError: (error, handler) {
|
||||||
|
appLogger.error(
|
||||||
|
'❌ Error: ${error.message} - ${error.requestOptions.path}',
|
||||||
|
);
|
||||||
|
appLogger.error('❌ Error type: ${error.type}');
|
||||||
|
return handler.next(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
mainDio.interceptors.add(requestInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced makeRequest method with comprehensive error handling
|
||||||
|
Future<Result<T>> makeRequest<T>(
|
||||||
|
String endpoint,
|
||||||
|
T Function(Map<String, dynamic>) fromJson, {
|
||||||
|
String method = 'GET',
|
||||||
|
FormData? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) async {
|
||||||
|
appLogger.info('🚀 network request: $method $endpoint');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check network connectivity first
|
||||||
|
if (!await _connectivity.hasInternetConnection()) {
|
||||||
|
appLogger.error('❌ No internet connection available');
|
||||||
|
return Result.error(
|
||||||
|
NetworkConnectionException('No internet connection available'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
Dio dioInstance = mainDio;
|
||||||
|
|
||||||
|
await addInterceptors();
|
||||||
|
|
||||||
|
Future<Response> makeApiCall(String token, String url) async {
|
||||||
|
final headers =
|
||||||
|
token.isEmpty
|
||||||
|
? <String, String>{}
|
||||||
|
: {'Authorization': 'Bearer $token'};
|
||||||
|
|
||||||
|
final options = Options(
|
||||||
|
headers: headers,
|
||||||
|
validateStatus: (status) => status != null && status < 500,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (method.toUpperCase()) {
|
||||||
|
case 'POST':
|
||||||
|
return await dioInstance.post(
|
||||||
|
url + endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
case 'PUT':
|
||||||
|
return await dioInstance.put(
|
||||||
|
url + endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
case 'PATCH':
|
||||||
|
return await dioInstance.patch(
|
||||||
|
url + endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
case 'DELETE':
|
||||||
|
return await dioInstance.delete(
|
||||||
|
url + endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
case 'GET':
|
||||||
|
return await dioInstance.get(
|
||||||
|
url + endpoint,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return await dioInstance.get(
|
||||||
|
url + endpoint,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? bearerToken = prefs.getString('bearer');
|
||||||
|
String? url = prefs.getString('url');
|
||||||
|
|
||||||
|
appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
|
||||||
|
appLogger.info(
|
||||||
|
'🔑 Bearer token: ${bearerToken != null ? 'PRESENT' : 'NOT SET'}',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url == null || url.isEmpty) {
|
||||||
|
appLogger.error('❌ Base URL not configured');
|
||||||
|
return Result.error(BadRequestException('Base URL not configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Response res = await makeApiCall(bearerToken ?? '', url);
|
||||||
|
|
||||||
|
// Handle successful responses
|
||||||
|
if (res.statusCode == 200 || res.statusCode == 201) {
|
||||||
|
appLogger.info('✅ Request successful: ${res.statusCode}');
|
||||||
|
try {
|
||||||
|
final result = fromJson(res.data);
|
||||||
|
appLogger.info('✅ Response parsed successfully');
|
||||||
|
return Result.ok(result);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
appLogger.error('❌ Failed to parse response: $e');
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException(
|
||||||
|
'Failed to parse response data',
|
||||||
|
res.statusCode,
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle specific HTTP error codes
|
||||||
|
return _handleHttpError(res);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return _handleDioException(e);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
appLogger.error('Unexpected error in makeRequest: $e');
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException('Unexpected error occurred', null, e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle HTTP error responses
|
||||||
|
Result<T> _handleHttpError<T>(Response response) {
|
||||||
|
final statusCode = response.statusCode;
|
||||||
|
final message = _extractErrorMessage(response.data);
|
||||||
|
|
||||||
|
switch (statusCode) {
|
||||||
|
case 400:
|
||||||
|
return Result.error(
|
||||||
|
BadRequestException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
case 401:
|
||||||
|
return Result.error(
|
||||||
|
AuthenticationException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
case 403:
|
||||||
|
return Result.error(
|
||||||
|
ForbiddenException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
case 404:
|
||||||
|
return Result.error(
|
||||||
|
NotFoundException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
case 423:
|
||||||
|
return Result.error(
|
||||||
|
ForbiddenException(
|
||||||
|
'Resource is locked: $message',
|
||||||
|
statusCode,
|
||||||
|
response.data,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 429:
|
||||||
|
return Result.error(
|
||||||
|
RateLimitException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
case 500:
|
||||||
|
case 502:
|
||||||
|
case 503:
|
||||||
|
case 504:
|
||||||
|
return Result.error(
|
||||||
|
ServerException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException(message, statusCode, response.data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle Dio exceptions
|
||||||
|
Result<T> _handleDioException<T>(DioException e) {
|
||||||
|
switch (e.type) {
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
case DioExceptionType.sendTimeout:
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
return Result.error(TimeoutException('Request timed out', e));
|
||||||
|
|
||||||
|
case DioExceptionType.connectionError:
|
||||||
|
return Result.error(
|
||||||
|
NetworkConnectionException('Connection error occurred', e),
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
if (e.response?.statusCode == 401) {
|
||||||
|
// Handle token refresh logic here if needed
|
||||||
|
// For now, return authentication error
|
||||||
|
return Result.error(
|
||||||
|
AuthenticationException(
|
||||||
|
'Authentication failed',
|
||||||
|
e.response?.statusCode,
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException(
|
||||||
|
'Bad response from server',
|
||||||
|
e.response?.statusCode,
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.cancel:
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException('Request was cancelled', null, e),
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.unknown:
|
||||||
|
default:
|
||||||
|
if (e.error is SocketException) {
|
||||||
|
return Result.error(
|
||||||
|
NetworkConnectionException('No internet connection', e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Result.error(
|
||||||
|
FetchDataException('Network error occurred', null, e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract error message from response data
|
||||||
|
String _extractErrorMessage(data) {
|
||||||
|
if (data == null) {
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
return data['message'] ??
|
||||||
|
data['error'] ??
|
||||||
|
data['detail'] ??
|
||||||
|
'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data is String) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple request method for backward compatibility
|
||||||
|
// Future<T> makeSimpleRequest<T>(
|
||||||
|
// String endpoint,
|
||||||
|
// T Function(Map<String, dynamic>) fromJson, {
|
||||||
|
// String method = 'GET',
|
||||||
|
// FormData? data,
|
||||||
|
// }) async {
|
||||||
|
// final result = await makeRequest(
|
||||||
|
// endpoint,
|
||||||
|
// fromJson,
|
||||||
|
// method: method,
|
||||||
|
// data: data,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return switch (result) {
|
||||||
|
// Ok(value: final value) => value,
|
||||||
|
// Error(error: final error) => throw error,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import 'package:tm_app/views/login/pages/login_screen.dart';
|
|||||||
|
|
||||||
import '../../views/profile/pages/profile_screen.dart';
|
import '../../views/profile/pages/profile_screen.dart';
|
||||||
import '../../views/tenders/pages/tenders_screen.dart';
|
import '../../views/tenders/pages/tenders_screen.dart';
|
||||||
import '../../views/your_tenders/your_tenders_screen.dart';
|
import '../../views/your_tenders/pages/your_tenders_screen.dart';
|
||||||
import 'app_shell/app_shell_screen.dart';
|
import 'app_shell/app_shell_screen.dart';
|
||||||
|
|
||||||
part 'app_routes.g.dart';
|
part 'app_routes.g.dart';
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
class AppException implements Exception {
|
class AppException implements Exception {
|
||||||
final String message;
|
final String message;
|
||||||
final String prefix;
|
final String prefix;
|
||||||
|
final int? statusCode;
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
AppException([this.message = 'An unknown error occurred.', this.prefix = '']);
|
AppException([
|
||||||
|
this.message = 'An unknown error occurred.',
|
||||||
|
this.prefix = '',
|
||||||
|
this.statusCode,
|
||||||
|
this.originalError,
|
||||||
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -11,11 +18,91 @@ class AppException implements Exception {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FetchDataException extends AppException {
|
class FetchDataException extends AppException {
|
||||||
FetchDataException([String? message])
|
FetchDataException([String? message, int? statusCode, originalError])
|
||||||
: super(message ?? 'Error During Communication: ', 'Network Error');
|
: super(
|
||||||
|
message ?? 'Error During Communication: ',
|
||||||
|
'Network Error',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationException extends AppException {
|
class AuthenticationException extends AppException {
|
||||||
AuthenticationException([String? message])
|
AuthenticationException([String? message, int? statusCode, originalError])
|
||||||
: super(message ?? 'Authentication Failed.', 'Auth Error');
|
: super(
|
||||||
|
message ?? 'Authentication Failed.',
|
||||||
|
'Auth Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkConnectionException extends AppException {
|
||||||
|
NetworkConnectionException([String? message, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'No internet connection available.',
|
||||||
|
'Connection Error: ',
|
||||||
|
null,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeoutException extends AppException {
|
||||||
|
TimeoutException([String? message, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Request timed out.',
|
||||||
|
'Timeout Error: ',
|
||||||
|
null,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerException extends AppException {
|
||||||
|
ServerException([String? message, int? statusCode, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Server error occurred.',
|
||||||
|
'Server Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BadRequestException extends AppException {
|
||||||
|
BadRequestException([String? message, int? statusCode, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Bad request.',
|
||||||
|
'Bad Request Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotFoundException extends AppException {
|
||||||
|
NotFoundException([String? message, int? statusCode, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Resource not found.',
|
||||||
|
'Not Found Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ForbiddenException extends AppException {
|
||||||
|
ForbiddenException([String? message, int? statusCode, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Access forbidden.',
|
||||||
|
'Forbidden Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RateLimitException extends AppException {
|
||||||
|
RateLimitException([String? message, int? statusCode, originalError])
|
||||||
|
: super(
|
||||||
|
message ?? 'Rate limit exceeded.',
|
||||||
|
'Rate Limit Error: ',
|
||||||
|
statusCode,
|
||||||
|
originalError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class NetworkConnectivity {
|
||||||
|
static final NetworkConnectivity _instance = NetworkConnectivity._internal();
|
||||||
|
factory NetworkConnectivity() => _instance;
|
||||||
|
NetworkConnectivity._internal();
|
||||||
|
|
||||||
|
/// Check if device has internet connectivity
|
||||||
|
Future<bool> hasInternetConnection() async {
|
||||||
|
try {
|
||||||
|
// Try to reach a reliable host to check internet connectivity
|
||||||
|
final result = await InternetAddress.lookup('google.com');
|
||||||
|
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
|
||||||
|
} on SocketException catch (_) {
|
||||||
|
return false;
|
||||||
|
} on Exception catch (_) {
|
||||||
|
// If connectivity check fails, assume no connection
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'network_manager.dart';
|
import '../../core/config/network_manager.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
AuthService({required NetworkManager networkManager})
|
AuthService({required NetworkManager networkManager})
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import 'package:tm_app/core/utils/result.dart';
|
|||||||
import 'package:tm_app/data/services/mock_data/home_mock_data.dart';
|
import 'package:tm_app/data/services/mock_data/home_mock_data.dart';
|
||||||
import 'package:tm_app/data/services/model/home/home_request/home_request_model.dart';
|
import 'package:tm_app/data/services/model/home/home_request/home_request_model.dart';
|
||||||
import 'package:tm_app/data/services/model/home/home_response/home_response_model.dart';
|
import 'package:tm_app/data/services/model/home/home_response/home_response_model.dart';
|
||||||
import 'package:tm_app/data/services/network_manager.dart';
|
|
||||||
|
import '../../core/config/network_manager.dart';
|
||||||
|
|
||||||
class HomeService {
|
class HomeService {
|
||||||
HomeService({required NetworkManager networkManager})
|
HomeService({required NetworkManager networkManager})
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
class NetworkManager {
|
|
||||||
final Dio mainDio;
|
|
||||||
|
|
||||||
NetworkManager()
|
|
||||||
: mainDio = Dio(
|
|
||||||
BaseOptions(
|
|
||||||
// baseUrl: 'https://stage.cura-bau.de',
|
|
||||||
connectTimeout: const Duration(seconds: 10),
|
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
|
||||||
contentType: 'application/json',
|
|
||||||
headers: {'Accept': 'application/json'},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
await addInterceptors();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addInterceptors() async {
|
|
||||||
final requestInterceptor = InterceptorsWrapper(
|
|
||||||
onRequest: (options, handler) {
|
|
||||||
// TODO: add more
|
|
||||||
return handler.next(options);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mainDio.interceptors.add(requestInterceptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T> makeRequest<T>(
|
|
||||||
String endpoint,
|
|
||||||
T Function(Map<String, dynamic>) fromJson, {
|
|
||||||
String method = 'GET',
|
|
||||||
FormData? data,
|
|
||||||
}) async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
Dio dioInstance = mainDio;
|
|
||||||
|
|
||||||
await addInterceptors();
|
|
||||||
|
|
||||||
Future<Response> makeApiCall(String token, String url) async {
|
|
||||||
final headers =
|
|
||||||
token.isEmpty
|
|
||||||
? <String, String>{}
|
|
||||||
: {'Authorization': 'Bearer $token'};
|
|
||||||
switch (method.toUpperCase()) {
|
|
||||||
case 'POST':
|
|
||||||
return await dioInstance.post(
|
|
||||||
url + endpoint,
|
|
||||||
data: data,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
case 'PUT':
|
|
||||||
return await dioInstance.put(
|
|
||||||
url + endpoint,
|
|
||||||
data: data,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
case 'PATCH':
|
|
||||||
return await dioInstance.patch(
|
|
||||||
url + endpoint,
|
|
||||||
data: data,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
case 'DELETE':
|
|
||||||
return await dioInstance.delete(
|
|
||||||
url + endpoint,
|
|
||||||
data: data,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
case 'GET':
|
|
||||||
return await dioInstance.get(
|
|
||||||
url + endpoint,
|
|
||||||
data: data,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return await dioInstance.get(
|
|
||||||
url + endpoint,
|
|
||||||
options: Options(headers: headers),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String? bearerToken = prefs.getString('bearer');
|
|
||||||
String? url = prefs.getString('url');
|
|
||||||
Response res = await makeApiCall(bearerToken ?? '', url ?? '');
|
|
||||||
switch (res.statusCode) {
|
|
||||||
case 200:
|
|
||||||
case 201:
|
|
||||||
return fromJson(res.data);
|
|
||||||
case 401:
|
|
||||||
break;
|
|
||||||
case 403:
|
|
||||||
throw Exception('Forbidden: ${res.data['message']}');
|
|
||||||
case 404:
|
|
||||||
throw Exception('Not found: ${res.data['message']}');
|
|
||||||
case 423:
|
|
||||||
throw Exception('Locked: ${res.data['message']}');
|
|
||||||
case 500:
|
|
||||||
throw Exception('Internal server error: ${res.data['message']}');
|
|
||||||
default:
|
|
||||||
throw Exception('Error: ${res.data['message']}');
|
|
||||||
}
|
|
||||||
} on DioException catch (e) {
|
|
||||||
if (e.response?.statusCode == 401) {
|
|
||||||
// await api.refreshToken();
|
|
||||||
String? bearerToken = prefs.getString('bearer');
|
|
||||||
String? url = prefs.getString('url');
|
|
||||||
Response res = await makeApiCall(bearerToken ?? '', url ?? '');
|
|
||||||
|
|
||||||
if (res.statusCode == 200 || res.statusCode == 201) {
|
|
||||||
return fromJson(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception(e.message ?? 'Error occurred');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception('Unhandled error occurred');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,8 @@ import 'package:tm_app/core/utils/result.dart';
|
|||||||
import 'package:tm_app/data/services/mock_data/tender_detail_mock.dart';
|
import 'package:tm_app/data/services/mock_data/tender_detail_mock.dart';
|
||||||
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
|
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
|
||||||
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
|
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
|
||||||
import 'package:tm_app/data/services/network_manager.dart';
|
|
||||||
|
import '../../core/config/network_manager.dart';
|
||||||
|
|
||||||
class TenderDetailService {
|
class TenderDetailService {
|
||||||
TenderDetailService({required NetworkManager networkManager})
|
TenderDetailService({required NetworkManager networkManager})
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender
|
|||||||
import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart';
|
import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart';
|
||||||
import 'package:tm_app/data/services/model/tenders/tenders_request/tenders_request.dart';
|
import 'package:tm_app/data/services/model/tenders/tenders_request/tenders_request.dart';
|
||||||
import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
|
import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
|
||||||
import 'package:tm_app/data/services/network_manager.dart';
|
|
||||||
|
import '../../core/config/network_manager.dart';
|
||||||
|
|
||||||
class TendersService {
|
class TendersService {
|
||||||
TendersService({required NetworkManager networkManager})
|
TendersService({required NetworkManager networkManager})
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tm_app/core/constants/assets.dart';
|
|
||||||
import 'package:tm_app/core/constants/strings.dart';
|
import 'package:tm_app/core/constants/strings.dart';
|
||||||
import 'package:tm_app/core/theme/colors.dart';
|
import 'package:tm_app/core/theme/colors.dart';
|
||||||
import 'package:tm_app/core/utils/size_config.dart';
|
import 'package:tm_app/core/utils/size_config.dart';
|
||||||
@@ -9,6 +7,7 @@ import 'package:tm_app/view_models/tender_detail_view_model.dart';
|
|||||||
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
|
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
|
||||||
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
|
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
|
||||||
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
|
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
|
||||||
|
import 'package:tm_app/views/shared/tender_app_bar.dart';
|
||||||
|
|
||||||
class TenderDetailMobilePage extends StatelessWidget {
|
class TenderDetailMobilePage extends StatelessWidget {
|
||||||
final String tenderId;
|
final String tenderId;
|
||||||
@@ -18,7 +17,7 @@ class TenderDetailMobilePage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.backgroundColor,
|
backgroundColor: AppColors.backgroundColor,
|
||||||
appBar: _buildAppBar(context),
|
appBar: appBar(context: context, title: AppStrings.tenderDetailTitle),
|
||||||
body: Consumer<TenderDetailViewModel>(
|
body: Consumer<TenderDetailViewModel>(
|
||||||
builder: (context, tenderViewModel, child) {
|
builder: (context, tenderViewModel, child) {
|
||||||
if (tenderViewModel.isLoading) {
|
if (tenderViewModel.isLoading) {
|
||||||
@@ -55,31 +54,4 @@ class TenderDetailMobilePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
|
||||||
return PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(60),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: AppColors.backgroundColor,
|
|
||||||
elevation: 0,
|
|
||||||
titleSpacing: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: SvgPicture.asset(
|
|
||||||
AssetsManager.arrowLeft,
|
|
||||||
height: 24.0.w(),
|
|
||||||
width: 24.0.w(),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
AppStrings.tenderDetailTitle,
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.grey70,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20.0.sp(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:tm_app/core/constants/assets.dart';
|
import 'package:tm_app/core/constants/assets.dart';
|
||||||
import 'package:tm_app/core/theme/colors.dart';
|
import 'package:tm_app/core/theme/colors.dart';
|
||||||
import 'package:tm_app/core/utils/size_config.dart';
|
import 'package:tm_app/core/utils/size_config.dart';
|
||||||
@@ -34,3 +35,33 @@ PreferredSizeWidget tenderMobileAppBar({required String title}) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PreferredSizeWidget appBar({
|
||||||
|
required BuildContext context,
|
||||||
|
required String title,
|
||||||
|
}) {
|
||||||
|
return PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(60),
|
||||||
|
child: AppBar(
|
||||||
|
backgroundColor: AppColors.backgroundColor,
|
||||||
|
elevation: 0,
|
||||||
|
titleSpacing: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
AssetsManager.arrowLeft,
|
||||||
|
height: 24.0.w(),
|
||||||
|
width: 24.0.w(),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.grey70,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 20.0.sp(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tm_app/core/constants/strings.dart';
|
import 'package:tm_app/core/constants/strings.dart';
|
||||||
import 'package:tm_app/core/theme/colors.dart';
|
import 'package:tm_app/core/theme/colors.dart';
|
||||||
import 'package:tm_app/core/utils/size_config.dart';
|
|
||||||
import 'package:tm_app/view_models/your_tenders_view_model.dart';
|
import 'package:tm_app/view_models/your_tenders_view_model.dart';
|
||||||
import 'package:tm_app/views/shared/main_tab_bar.dart';
|
import 'package:tm_app/views/shared/main_tab_bar.dart';
|
||||||
|
import 'package:tm_app/views/shared/tender_app_bar.dart';
|
||||||
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
|
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
|
||||||
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
|
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
|
||||||
|
|
||||||
import '../../../core/constants/assets.dart';
|
|
||||||
|
|
||||||
class YourTendersMobilePage extends StatefulWidget {
|
class YourTendersMobilePage extends StatefulWidget {
|
||||||
const YourTendersMobilePage({super.key});
|
const YourTendersMobilePage({super.key});
|
||||||
|
|
||||||
@@ -42,7 +39,7 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.backgroundColor,
|
backgroundColor: AppColors.backgroundColor,
|
||||||
appBar: _buildAppBar(context),
|
appBar: appBar(context: context, title: AppStrings.yourTenders),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
MainTabBar(
|
MainTabBar(
|
||||||
@@ -60,30 +57,3 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
|
||||||
return PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(60),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: AppColors.backgroundColor,
|
|
||||||
elevation: 0,
|
|
||||||
titleSpacing: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: SvgPicture.asset(
|
|
||||||
AssetsManager.arrowLeft,
|
|
||||||
height: 24.0.w(),
|
|
||||||
width: 24.0.w(),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
AppStrings.yourTenders,
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.grey70,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20.0.sp(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user