Commit 55151ba2 authored by DatHV's avatar DatHV
Browse files

update history point, manager

parent f714cdcc
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'interceptor/auth_interceptor.dart';
import 'interceptor/exception_interceptor.dart';
import 'interceptor/logger_interceptor.dart';
import 'interceptor/request_interceptor.dart';
const int connectTimeout = 30;
const int receiveTimeout = 30;
class DioHttpService {
DioHttpService._internal();
static final DioHttpService _instance = DioHttpService._internal();
factory DioHttpService() => _instance;
String _baseUrl = '';
Dio get dio => _dio;
late final Dio _dio = Dio(
BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: connectTimeout),
receiveTimeout: const Duration(seconds: receiveTimeout),
contentType: 'application/json',
responseType: ResponseType.json,
validateStatus: (_) => true,
receiveDataWhenStatusError: true,
),
)
..interceptors.add(AuthInterceptor())
..interceptors.add(RequestInterceptor())
..interceptors.add(ExceptionInterceptor())
..interceptors.add(InterceptorsWrapper(
onError: (e, h) {
if (e.response != null) return h.resolve(e.response!);
h.next(e);
},
))
..interceptors.addAll(kReleaseMode ? const [] : [LoggerInterceptor()]);
void setBaseUrl(String newUrl) {
_baseUrl = newUrl;
_dio.options.baseUrl = newUrl;
}
void setDefaultHeaders(Map<String, dynamic> headers) {
dio.options.headers.addAll(headers);
}
}
\ No newline at end of file
import 'package:dio/dio.dart';
import '../configs/constants.dart';
import 'app_navigator.dart';
import '../../configs/constants.dart';
import '../app_navigator.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
class AuthInterceptor extends Interceptor {
bool _handling = false; // chặn bắn 2 lần nếu nhiều request fail cùng lúc
bool _isHandlingAuth = false;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final data = response.data;
if (_isInvalidTokenPayload(data)) {
_handleInvalidToken(data);
// chặn parse tiếp: reject error để upstream biết đã fail
return handler.reject(
if (_isTokenInvalid(data)) {
_handleAuthError(data);
handler.reject(
DioException(
requestOptions: response.requestOptions,
response: response,
......@@ -20,24 +19,22 @@ class AuthInterceptor extends Interceptor {
error: 'ERR_AUTH_TOKEN_INVALID',
),
);
return;
}
super.onResponse(response, handler);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final data = err.response?.data;
final status = err.response?.statusCode;
if (status == 401 || _isInvalidTokenPayload(data)) {
_handleInvalidToken(data);
// vẫn reject để caller biết yêu cầu đã fail
return handler.reject(err);
final statusCode = err.response?.statusCode;
if (statusCode == 401 || _isTokenInvalid(data)) {
_handleAuthError(data);
}
super.onError(err, handler);
handler.next(err);
}
bool _isInvalidTokenPayload(dynamic data) {
bool _isTokenInvalid(dynamic data) {
if (data is Map<String, dynamic>) {
final code = data['error_code'] ?? data['errorCode'];
return ErrorCodes.tokenInvalidCodes.contains(code);
......@@ -45,20 +42,20 @@ class AuthInterceptor extends Interceptor {
return false;
}
void _handleInvalidToken(dynamic data) async {
if (_handling) return;
_handling = true;
// Xoá token / session
Future<void> _handleAuthError(dynamic data) async {
if (_isHandlingAuth) return;
_isHandlingAuth = true;
try {
await DataPreference.instance.clearData();
final message = (data is Map && data['error_message'] is String)
? data['error_message'] as String
: '';
// Hiện alert + điều hướng login
await AppNavigator.showAuthAlertAndGoLogin(message);
_handling = false;
String? message;
if (data is Map<String, dynamic>) {
message = data['error_message']?.toString() ??
data['errorMessage']?.toString() ??
data['message']?.toString();
}
await AppNavigator.showAuthAlertAndGoLogin(message ?? ErrorCodes.tokenInvalidMessage);
} finally {
_isHandlingAuth = false;
}
}
}
import 'package:dio/dio.dart';
import '../app_navigator.dart';
class ExceptionInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_handleError(err);
handler.next(err);
}
void _handleError(DioException error) {
String message;
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.sendTimeout:
message = 'Kết nối mạng không ổn định.\nVui lòng thử lại!';
break;
case DioExceptionType.connectionError:
message = 'Không thể kết nối đến máy chủ.\nVui lòng kiểm tra kết nối mạng!';
break;
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
if (statusCode == 401) {
// Auth error - handled by AuthInterceptor
return;
}
message = _extractErrorMessage(error.response?.data) ??
'Đã xảy ra lỗi. Vui lòng thử lại!';
break;
default:
message = 'Đã xảy ra lỗi không xác định. Vui lòng thử lại!';
}
// Show alert dialog with the error message
AppNavigator.showAlert(message);
}
String? _extractErrorMessage(dynamic data) {
if (data is Map<String, dynamic>) {
return data['message']?.toString() ??
data['error_message']?.toString() ??
data['errorMessage']?.toString();
}
return null;
}
}
//
// typedef RetryPrompt = Future<bool> Function(DioException e);
//
// class ExceptionInterceptor extends Interceptor {
// ExceptionInterceptor({
// required Dio dio,
// RetryPrompt? prompt,
// this.shouldRetryWhen = _defaultShouldRetry,
// }) : _dio = dio,
// _prompt = prompt ?? _defaultPrompt;
//
// final Dio _dio;
// final RetryPrompt _prompt;
//
// /// Quyết định lỗi nào là lỗi mạng đáng retry.
// final bool Function(DioException e) shouldRetryWhen;
//
// static bool _defaultShouldRetry(DioException e) {
// return e.type == DioExceptionType.connectionTimeout ||
// e.type == DioExceptionType.sendTimeout ||
// e.type == DioExceptionType.receiveTimeout ||
// e.type == DioExceptionType.unknown ||
// e.type == DioExceptionType.connectionError;
// }
//
// static Future<bool> _defaultPrompt(DioException e) async {
// // Dialog đơn giản với GetX. Bạn có thể thay Alert tuỳ UI của app.
// final result = await Get.dialog<bool>(
// AlertDialog(
// title: const Text('Mất kết nối'),
// content: const Text('Kết nối mạng không ổn định. Bạn có muốn thử lại không?'),
// actions: [
// TextButton(onPressed: () => Get.back(result: false), child: const Text('Đóng')),
// ElevatedButton(onPressed: () => Get.back(result: true), child: const Text('Thử lại')),
// ],
// ),
// barrierDismissible: false,
// );
// return result == true;
// }
//
// @override
// void onError(DioException err, ErrorInterceptorHandler handler) async {
// if (!shouldRetryWhen(err)) {
// return handler.next(err);
// }
// if (err.requestOptions.extra['__retried__'] == true) {
// return handler.next(err);
// }
// final wantRetry = await _prompt(err);
// if (!wantRetry) {
// return handler.next(err);
// }
// try {
// final resp = await _retryRequest(err.requestOptions);
// return handler.resolve(resp);
// } catch (_) {
// return handler.next(err);
// }
// }
//
// Future<Response<dynamic>> _retryRequest(RequestOptions ro) {
// // Đánh dấu đã retry để tránh vòng lặp
// ro.extra['__retried__'] = true;
// final options = Options(
// method: ro.method,
// headers: ro.headers,
// responseType: ro.responseType,
// contentType: ro.contentType,
// followRedirects: ro.followRedirects,
// receiveDataWhenStatusError: ro.receiveDataWhenStatusError,
// validateStatus: ro.validateStatus,
// sendTimeout: ro.sendTimeout,
// receiveTimeout: ro.receiveTimeout,
// extra: ro.extra,
// );
//
// return _dio.request<dynamic>(
// ro.path,
// data: ro.data, // Lưu ý: nếu dùng FormData với stream tự chế, có thể không retry được
// queryParameters: ro.queryParameters,
// options: options,
// cancelToken: ro.cancelToken,
// onSendProgress: ro.onSendProgress,
// onReceiveProgress: ro.onReceiveProgress,
// );
// }
// }
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
class LoggerInterceptor extends Interceptor {
final Logger _logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
lineLength: 120,
printTime: true,
colors: true,
),
);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final uri = options.uri;
_logger.i(
'🚀 ${options.method} $uri\n'
'Headers: ${_formatHeaders(options.headers)}\n'
'Query: ${options.queryParameters}\n'
'Body: ${_formatData(options.data)}',
);
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final uri = response.requestOptions.uri;
final statusCode = response.statusCode;
_logger.d(
'✅ $statusCode ${response.requestOptions.method} $uri\n'
'Response: ${_formatData(response.data)}',
);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final uri = err.requestOptions.uri;
final statusCode = err.response?.statusCode ?? 'Unknown';
_logger.e(
'❌ $statusCode ${err.requestOptions.method} $uri\n'
'Error: ${err.message}\n'
'Response: ${_formatData(err.response?.data)}',
);
handler.next(err);
}
String _formatHeaders(Map<String, dynamic> headers) {
final filtered = Map<String, dynamic>.from(headers);
// Hide sensitive headers
if (filtered.containsKey('Authorization')) {
filtered['Authorization'] = '***';
}
return filtered.toString();
}
String _formatData(dynamic data) {
if (data == null) return 'null';
if (data is String && data.length > 1000) {
return '${data.substring(0, 1000)}... (truncated)';
}
return data.toString();
}
}
\ No newline at end of file
import 'package:dio/dio.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
class ModifyRequestInterceptor extends Interceptor {
class RequestInterceptor extends Interceptor {
@override
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
String authKey = 'Authorization';
String? token = await DataPreference.instance.token;
String? token = DataPreference.instance.token;
if (token!= null) {
options.headers[authKey] = "Bearer $token";
}
super.onRequest(options, handler);
options.headers.addAll({
'Accept': 'application/json',
'Content-Type': 'application/json',
'Accept-Language': 'vi',
});
handler.next(options);
}
}
\ No newline at end of file
typedef Json = Map<String, dynamic>;
abstract class Encodable {
Json toJson();
}
abstract class Fillable extends Encodable {
void fill(Json data);
}
extension JsonEncode on Json {
Json toJson() {
return this;
}
}
class ModelMaker {
static T? makeFrom<T extends Encodable>(dynamic json) {
try {
switch (T) {
// case WorkerSiteResponse: return WorkerSiteResponse.fromJson(json as Json) as T;
// case LoginResponse: return LoginResponse.fromJson(json as Json) as T;
default: return null;
}
} catch (e) {
print("API ERROR parser: ${e.toString()}");
return null;
}
}
}
extension L on List {
}
\ No newline at end of file
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../configs/api_paths.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class RequestManager {
static final RequestManager _instance = RequestManager._internal();
factory RequestManager() => _instance;
late Dio _dio;
bool _isErrorDialogShown = false;
final List<Future Function()> _pendingRetries = [];
RequestManager._internal() {
BaseOptions options = BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
responseType: ResponseType.json,
headers: {
'Content-Type': 'application/json',
},
);
_dio = Dio(options);
// interceptor handle errors
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// Log request
print("REQUEST[${options.method}] => ${options.baseUrl}${options.path}");
return handler.next(options);
},
onResponse: (response, handler) {
// Log response
print("RESPONSE[${response.statusCode}] => ${response.data}");
return handler.next(response);
},
onError: (DioException error, handler) async {
if (error.response?.statusCode == 401 ||
error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.error is SocketException) {
_pendingRetries.add(() async {
return await _retryRequest(error.requestOptions);
});
if (!_isErrorDialogShown) {
_isErrorDialogShown = true;
bool retry = await _showNetworkErrorAlert();
if (retry) {
await _retryPendingRequests();
} else {
_pendingRetries.clear();
}
_isErrorDialogShown = false;
}
}
return handler.next(error);
},
));
}
// retry request từ RequestOptions
Future<Response<T>> _retryRequest<T>(RequestOptions options) async {
try {
Response<T> response = await _dio.request<T>(
options.path,
data: options.data,
queryParameters: options.queryParameters,
options: Options(
method: options.method,
headers: options.headers,
extra: options.extra,
),
);
return response;
} catch (e) {
rethrow;
}
}
// retry all request errors
Future<void> _retryPendingRequests() async {
List<Future Function()> retries = List.from(_pendingRetries);
_pendingRetries.clear();
await Future.wait(retries.map((f) => f()));
}
Future<bool> _showNetworkErrorAlert() async {
BuildContext? context = navigatorKey.currentState?.overlay?.context;
if (context == null) return false;
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text("Lỗi mạng"),
content: Text("Đã xảy ra lỗi mạng hoặc xác thực. Bạn có muốn thử lại các request lỗi không?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text("Hủy"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text("Thử lại"),
),
],
),
).then((value) => value ?? false);
}
/// [cancelToken]: Token hủy request.
/// [converter]: Hàm chuyển đổi từ Map<String, dynamic> sang model T (nếu T kế thừa BaseModel).
Future<T> request<T>({
String? host,
required String path,
required String method,
Map<String, dynamic>? queryParameters,
dynamic data,
Map<String, dynamic>? headers,
CancelToken? cancelToken,
T Function(Map<String, dynamic> json)? converter,
}) async {
_dio.options.baseUrl = host ?? APIPaths.baseUrl;
if (headers != null) {
_dio.options.headers.addAll(headers);
}
try {
Response response = await _dio.request(
path,
data: data,
queryParameters: queryParameters,
options: Options(method: method),
cancelToken: cancelToken,
);
if (converter != null && response.data is Map<String, dynamic>) {
return converter(response.data as Map<String, dynamic>);
}
return response.data as T;
} on DioException catch (e) {
throw Exception("Request error: ${e.message}");
}
}
}
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/networking/auth_interceptor.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../configs/callbacks.dart';
import '../configs/constants.dart';
import 'model_maker.dart';
enum Method {
GET, POST, PUT, DELETE
}
class RestfulAPIClient {
final Dio _dio;
RestfulAPIClient(this._dio) {
_dio.interceptors.add(AuthInterceptor());
}
Json header = {};
Future<BaseResponseModel<T>> fetchObject<T>(
String path,
T Function(dynamic json) fromJsonT,
) async {
final response = await _dio.get(path);
return BaseResponseModel<T>.fromJson(response.data, fromJsonT);
}
Future<BaseResponseModel<List<T>>> fetchList<T>(
String path,
T Function(dynamic json) fromJson,
) async {
final res = await _dio.get(path);
return BaseResponseModel<List<T>>.fromJson(res.data, (json) {
return (json as List).map((e) => fromJson(e)).toList();
});
}
Future<BaseResponseModel<T>> requestNormal<T>(String path, Method method, Json params, CallbackReturn<T, dynamic> parser) async {
final result = await request<BaseResponseModel<T>>(path, method, params, (data) {
return BaseResponseModel<T>.fromJson(data, (json) => parser(json));
});
return result ?? BaseResponseModel<T>(errorMessage: Constants.commonError);
}
Future<T?> request<T>(String path, Method method, Json params, CallbackReturn<T, Json> parser) async {
final isGet = method == Method.GET;
Json query = isGet ? params : {};
Json body = !isGet ? params : {};
// body["lang"] = 'vi';
final option = Options(method: method.name)
.compose(
_dio.options,
path,
queryParameters: query,
data: body,
);
String? token = await DataPreference.instance.token;
if (token != null) {
option.headers["Authorization"] = "Bearer $token";
}
try {
final result = await _dio.fetch<Map<String, dynamic>>(option);
final json = result.data;
if (json == null) return null;
final code = json['error_code'] ?? json['errorCode'];
if (ErrorCodes.tokenInvalidCodes.contains(code)) {
return null;
}
return parser(json);
} on DioException catch(e) {
_print(e.toString());
final data = e.response?.data;
if (data is Json) {
try {
return parser(data);
} catch (e) {
_print(e.toString());
return null;
}
} else {
return null;
}
} catch(e) {
_print(e.toString());
return null;
}
}
void _print(String error) {
if (kDebugMode) {
print("===================\nAPI Error Parse false ${error}\n================");
}
}
}
\ No newline at end of file
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/networking/interceptor/auth_interceptor.dart';
import '../configs/callbacks.dart';
import '../configs/constants.dart';
enum Method { GET, POST, PUT, DELETE }
class RestfulAPIClient {
final Dio _dio;
Json header = {};
RestfulAPIClient(this._dio);
Future<BaseResponseModel<T>> fetchObject<T>(String path, T Function(dynamic json) fromJsonT) =>
requestNormal<T>(path, Method.GET, const {}, fromJsonT);
Future<BaseResponseModel<List<T>>> fetchList<T>(String path, T Function(dynamic json) fromJsonT) =>
requestNormal<List<T>>(path, Method.GET, const {}, (jsonRoot) {
if (jsonRoot is List) {
return jsonRoot.map(fromJsonT).toList();
}
if (jsonRoot is Map && jsonRoot['data'] is List) {
return (jsonRoot['data'] as List).map(fromJsonT).toList();
}
return <T>[];
});
Future<BaseResponseModel<T>> requestNormal<T>(
String path,
Method method,
Json params,
T Function(dynamic json) parser,
) async {
final isGet = method == Method.GET;
final query = isGet ? params : const <String, dynamic>{};
final body = isGet ? const <String, dynamic>{} : params;
final opt = _opts(method).compose(_dio.options, path, queryParameters: query, data: body);
try {
final res = await _dio.fetch<dynamic>(opt);
final status = res.statusCode ?? 0;
final map = _asJson(res.data);
try {
final model = BaseResponseModel<T>.fromJson(map, parser);
return model;
} catch (_) {
final msg = _extractMessage(map, status) ?? 'HTTP $status';
if (_isOk(status)) {
T? data;
try { data = parser(map); } catch (_) {}
return BaseResponseModel<T>(status: "success", message: map['message']?.toString(), data: data, code: status);
} else {
return BaseResponseModel<T>(status: "fail", message: msg, data: null, code: status);
}
}
} on DioException catch (e) {
_debug('DioException: $e');
final status = e.response?.statusCode;
final map = _asJson(e.response?.data);
final msg = _extractMessage(map, status) ?? e.message ?? Constants.commonError;
return BaseResponseModel<T>(status: "fail", message: msg, data: null, code: status);
} catch (e) {
_debug('Unknown exception: $e');
return BaseResponseModel<T>(status: "fail", message: Constants.commonError, data: null);
}
}
Options _opts(Method m) => Options(method: m.name, validateStatus: (_) => true, receiveDataWhenStatusError: true);
bool _isOk(int? code) => code != null && code >= 200 && code < 300;
/// Ép mọi kiểu body về Map:
/// - Map: trả nguyên
/// - String: cố gắng jsonDecode → Map; nếu không, bọc thành {'message': '...'}
/// - List: bọc thành {'data': List}
/// - null/khác: {'message': '...'} hoặc message chung
Json _asJson(dynamic data) {
if (data is Json) return data;
if (data is String) {
try {
final decoded = jsonDecode(data);
if (decoded is Json) return decoded;
if (decoded is List) return <String, dynamic>{'data': decoded};
return <String, dynamic>{'message': data};
} catch (_) {
return <String, dynamic>{'message': data};
}
}
if (data is List) {
return <String, dynamic>{'data': data};
}
if (data == null) {
return <String, dynamic>{'message': Constants.commonError};
}
return <String, dynamic>{'message': data.toString()};
}
/// Rút message từ nhiều key phổ biến; fallback HTTP code
String? _extractMessage(Json j, [int? status]) {
const keys = ['message', 'errorMessage', 'error_message', 'error', 'msg', 'detail', 'description'];
for (final k in keys) {
final v = j[k];
if (v != null) {
final s = v.toString().trim();
if (s.isNotEmpty) return s;
}
}
return status == null ? null : 'HTTP $status';
}
void _debug(Object e) {
if (kDebugMode) {
print('=== API DEBUG === $e');
}
}
}
\ No newline at end of file
......@@ -3,15 +3,18 @@ import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/game/models/game_bundle_response.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import '../configs/callbacks.dart';
import '../configs/device_info.dart';
import '../model/auth/biometric_register_response_model.dart';
import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart';
import '../model/update_response_model.dart';
import '../screen/history_point/models/history_point_models.dart';
import '../screen/history_point/models/transaction_summary_by_date_model.dart';
import '../screen/splash/models/update_response_model.dart';
import '../preference/point/header_home_model.dart';
import '../screen/affiliate/model/affiliate_brand_model.dart';
import '../screen/affiliate/model/affiliate_category_model.dart';
......@@ -73,9 +76,8 @@ import '../screen/voucher/models/product_brand_model.dart';
import '../screen/voucher/models/product_store_model.dart';
import '../screen/voucher/models/product_type.dart';
import '../screen/voucher/models/search_product_response_model.dart';
import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
extension RestfulAPIClientAllRequest on RestfulAPIClient {
Future<BaseResponseModel<UpdateResponseModel>> checkUpdateApp() async {
String version = Platform.version;
final body = {"operating_system": "iOS", "software_model": "MyPoint", "version": version, "build_number": "1"};
......@@ -624,7 +626,7 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
Future<BaseResponseModel<List<ProductBrandModel>>> productTopUpBrands() async {
final body = {"topup_type": "PRODUCT_MODEL_MOBILE_SERVICE", "page_size": "999", "page_index": 0};
return requestNormal(APIPaths.getTopUpBrands, Method.GET, body, (data) {
return requestNormal(APIPaths.productTopUpsBrands, Method.GET, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => ProductBrandModel.fromJson(e)).toList();
});
......@@ -675,9 +677,9 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
});
}
Future<BaseResponseModel<EmptyCodable>> redeemProductTopUps(String productId, String phoneNumber) async {
Future<BaseResponseModel<EmptyCodable>> redeemProductTopUps(int productId, String phoneNumber) async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token, "product_id": productId, "quantity": 1, "phone_number": phoneNumber};
final body = {"access_token": token, "product_id": productId, "quantity": 1, "phone_number": phoneNumber, "lang": "vi"};
return requestNormal(APIPaths.redeemProductTopUps, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
......@@ -926,11 +928,36 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future<BaseResponseModel<List<BankAccountInfoModel>>> getOrderPaymentMyAccounts() async {
// String? token = DataPreference.instance.token ?? "";
// final body = {"access_token": token, "lang": "vi"};
return requestNormal(APIPaths.orderPaymentMyAccounts, Method.GET, {}, (data) {
final list = data as List<dynamic>;
return list.map((e) => BankAccountInfoModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<String>> setDefaultBankAccount(String id, bool isDefault) async {
final path = APIPaths.bankAccountSetDefault.replaceFirst('%@', id);
final body = {"is_default": isDefault ? 1 : 0};
return requestNormal(path, Method.POST, body, (data) => data as String);
}
Future<BaseResponseModel<String>> deleteBankAccount(String id) async {
final path = APIPaths.bankAccountDelete.replaceFirst('%@', id);
return requestNormal(path, Method.DELETE, {}, (data) => data as String);
}
Future<BaseResponseModel<TransactionSummaryByDateModel>> transactionGetSummaryByDate(Json body) async {
String? token = DataPreference.instance.token ?? "";
body["access_token"] = token;
return requestNormal(APIPaths.transactionGetSummaryByDate, Method.POST, body, (data) {
return TransactionSummaryByDateModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<ListHistoryResponseModel>> transactionHistoryGetList(Json body) async {
String? token = DataPreference.instance.token ?? "";
body["access_token"] = token;
return requestNormal(APIPaths.transactionHistoryGetList, Method.POST, body, (data) {
return ListHistoryResponseModel.fromJson(data as Json);
});
}
}
\ No newline at end of file
import 'package:mypoint_flutter_app/networking/restful_api.dart';
import '../dio_http_service/dio_http_service.dart';
import 'base_view_model.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client.dart';
import '../base/base_view_model.dart';
import 'dio_http_service.dart';
class RestfulApiViewModel extends BaseViewModel {
final RestfulAPIClient _apiService = RestfulAPIClient(DioHttpService().getDio());
final RestfulAPIClient _apiService = RestfulAPIClient(DioHttpService().dio);
RestfulAPIClient get client => _apiService;
@override
void onInit() {
super.onInit();
}
}
\ No newline at end of file
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart';
......@@ -14,16 +15,37 @@ class DataPreference {
String phoneNumberUsedForLoginScreen = "";
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
final tokenJson = prefs.getString('login_token');
if (tokenJson != null) {
if (tokenJson != null && tokenJson.isNotEmpty) {
try {
_loginToken = LoginTokenResponseModel.fromJson(jsonDecode(tokenJson));
} catch (e) {
if (kDebugMode) {
print('Failed to parse login token: $e');
}
// Clear invalid token
await prefs.remove('login_token');
}
}
final profileJson = prefs.getString('user_profile');
if (profileJson != null) {
if (profileJson != null && profileJson.isNotEmpty) {
try {
_profile = ProfileResponseModel.fromJson(jsonDecode(profileJson));
phoneNumberUsedForLoginScreen = _profile?.workerSite?.phoneNumber ?? "";
} catch (e) {
if (kDebugMode) {
print('Failed to parse user profile: $e');
}
// Clear invalid profile
await prefs.remove('user_profile');
}
}
} catch (e) {
if (kDebugMode) {
print('DataPreference init failed: $e');
}
}
}
String get displayName {
......@@ -77,9 +99,28 @@ class DataPreference {
}
Future<String?> getBioToken(String phone) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString('biometric_login_token_$phone');
return jsonString != null ? jsonDecode(jsonString) : null;
if (jsonString != null && jsonString.isNotEmpty) {
try {
return jsonDecode(jsonString) as String?;
} catch (e) {
if (kDebugMode) {
print('Failed to parse bio token for $phone: $e');
}
// Clear invalid bio token
await prefs.remove('biometric_login_token_$phone');
return null;
}
}
return null;
} catch (e) {
if (kDebugMode) {
print('getBioToken failed for $phone: $e');
}
return null;
}
}
Future<void> clearBioToken(String phone) async {
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import '../data_preference.dart';
import 'header_home_model.dart';
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../home/models/achievement_model.dart';
......@@ -46,6 +47,9 @@ class _AchievementListScreenState extends State<AchievementListScreen> {
Widget _buildAchievementContent() {
final items = _viewModel.achievements;
if (items.isEmpty) {
return EmptyWidget();
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.builder(
......@@ -77,7 +81,9 @@ class _AchievementListScreenState extends State<AchievementListScreen> {
Widget _buildPointHuntingContent() {
final items = _viewModel.achievements;
return RefreshIndicator(
return items.isEmpty
? EmptyWidget()
: RefreshIndicator(
onRefresh: () => _viewModel.fetchAchievements(),
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart';
import '../home/models/achievement_model.dart';
......
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import 'affiliate_popup_brands.dart';
import 'model/affiliate_brand_model.dart';
import 'model/affiliate_category_model.dart';
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import 'models/affiliate_brand_detail_model.dart';
class AffiliateBrandDetailViewModel extends RestfulApiViewModel {
......
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import '../affiliate/model/affiliate_brand_model.dart';
import '../affiliate/model/affiliate_category_model.dart';
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resources/base_color.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../../widgets/custom_toast_message.dart';
import 'bank_account_detail_viewmodel.dart';
import 'bank_account_info_model.dart';
class BankAccountDetailScreen extends StatefulWidget {
class BankAccountDetailScreen extends BaseScreen {
final BankAccountInfoModel model;
const BankAccountDetailScreen({super.key, required this.model});
......@@ -10,30 +17,33 @@ class BankAccountDetailScreen extends StatefulWidget {
State<BankAccountDetailScreen> createState() => _BankAccountDetailScreenState();
}
class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
late bool _isDefault;
class _BankAccountDetailScreenState extends BaseState<BankAccountDetailScreen> with BasicState {
late final BankAccountDetailViewModel _viewModel;
@override
void initState() {
super.initState();
_isDefault = widget.model.isDefault ?? false;
_viewModel = Get.put(BankAccountDetailViewModel(model: widget.model));
_viewModel.isDefault.value = widget.model.isDefault ?? false;
_viewModel.deleteBackAccountSuccess = (message) {
showToastMessage(message);
Get.back(result: true);
};
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget build(BuildContext context) {
final title = widget.model.cardName?.isNotEmpty == true
? widget.model.cardName!.toUpperCase()
: (widget.model.paymentMethod ?? 'THẺ/TÀI KHOẢN');
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: 'Thông tin thẻ'),
appBar: CustomNavigationBar(title: widget.model.formBankTitle ?? 'Chi tiết thẻ/tài khoản'),
body: Column(
children: [
const SizedBox(height: 8),
_CardPreview(
title: title,
maskedNumber: widget.model.cardNumber ?? '',
),
_CardPreview(model: widget.model),
const SizedBox(height: 12),
_defaultRow(context),
const Spacer(),
......@@ -47,13 +57,13 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
}
Widget _defaultRow(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
return Obx(() =>
Container(
margin: const EdgeInsets.symmetric(horizontal: 0),
padding: const EdgeInsets.symmetric(horizontal: 16),
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
......@@ -64,18 +74,18 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
),
),
Switch(
value: _isDefault,
activeColor: Colors.white,
activeTrackColor: const Color(0xFF34C759),
onChanged: (val) async {
setState(() => _isDefault = val);
// if (widget.onMakeDefault != null) {
// await widget.onMakeDefault!(widget.model);
// }
value: _viewModel.isDefault.value,
onChanged: (value) async {
_viewModel.changeDefaultBankAccount();
},
),
activeColor: Colors.white,
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey.shade400,
)
],
),
),
);
}
......@@ -83,28 +93,9 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
onPressed: () async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Xoá thẻ?'),
content: Text(
widget.model.formMessageDelete ??
'Bạn có chắc muốn xoá thẻ/tài khoản này?',
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Huỷ')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Xoá')),
],
),
);
// if (ok == true && widget.onDelete != null) {
// await widget.onDelete!(widget.model);
// if (context.mounted) Navigator.pop(context);
// }
},
onPressed: _showAlertConfirmLogout,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF2F2F2),
backgroundColor: Colors.grey.shade400,
elevation: 0,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
......@@ -116,21 +107,39 @@ class _BankAccountDetailScreenState extends State<BankAccountDetailScreen> {
),
);
}
_showAlertConfirmLogout() {
final dataAlert = DataAlertModel(
title: "Xoá thẻ?",
description: "Bạn có chắc muốn xoá thẻ/tài khoản này?",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
Get.back();
_viewModel.deleteBankAccount();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
);
showAlert(data: dataAlert);
}
}
class _CardPreview extends StatelessWidget {
final String title;
final String maskedNumber;
const _CardPreview({
required this.title,
required this.maskedNumber,
});
final BankAccountInfoModel model;
const _CardPreview({required this.model});
@override
Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width-32;
return Container(
height: 180,
height: widthItem*9/16,
width: widthItem,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: const LinearGradient(
......@@ -145,25 +154,44 @@ class _CardPreview extends StatelessWidget {
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 12, offset: Offset(0, 6)),
],
image: DecorationImage(
image: bgImageProvider(url: model.formBankBackground),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.15), BlendMode.darken),
),
margin: const EdgeInsets.all(16),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Spacer(),
Text(
title, // VISA / MASTER / BANK NAME
style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: 1.2),
model.formBankName ?? 'THẺ/TÀI KHOẢN', // VISA / MASTER / BANK NAME
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.w700, letterSpacing: 1.2),
),
const Spacer(),
const Text('Số thẻ', style: TextStyle(color: Colors.white70, fontSize: 13)),
Text(
model.formBankNumberTitle ?? 'Số thẻ/Tài khoản',
style: TextStyle(color: Colors.white70, fontSize: 16),
),
const SizedBox(height: 6),
Text(
maskedNumber,
model.formBankNumber ?? '',
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: 2),
),
],
),
);
}
ImageProvider<Object> bgImageProvider({
required String? url,
String placeholderAsset = 'assets/images/bg_card_bank_account.png',
}) {
if (url == null || url.isEmpty) {
return AssetImage(placeholderAsset);
}
return NetworkImage(url);
}
}
\ No newline at end of file
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import '../../networking/restful_api_viewmodel.dart';
import 'bank_account_info_model.dart';
class BankAccountDetailViewModel extends RestfulApiViewModel {
final BankAccountInfoModel model;
void Function(String message)? onShowAlertError;
void Function(String message)? deleteBackAccountSuccess;
var isDefault = false.obs;
BankAccountDetailViewModel({required this.model});
changeDefaultBankAccount() async {
final revertDefault = !isDefault.value;
final accountId = model.id.toString();
showLoading();
try {
final response = await client.setDefaultBankAccount(accountId, revertDefault);
hideLoading();
if (response.isSuccess) {
isDefault.value = revertDefault;
showToastMessage(response.data ?? response.message ?? "Cập nhật thành công");
} else {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
deleteBankAccount() async {
final accountId = model.id.toString();
showLoading();
try {
final response = await client.deleteBankAccount(accountId);
hideLoading();
if (response.isSuccess) {
deleteBackAccountSuccess?.call(response.data ?? response.message ?? "Xoá tài khoản thành công");
} else {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
} finally {
hideLoading();
}
}
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment