Commit b75a9279 authored by DatHV's avatar DatHV
Browse files

update config, build.

parent 36ac8d24
class AppConfig {
final String name;
final String baseUrl;
final String t3Token;
final bool enableLogging;
const AppConfig({
required this.name,
required this.baseUrl,
required this.t3Token,
required this.enableLogging,
});
static late AppConfig current;
}
......@@ -15,7 +15,7 @@ class BaseResponseModel<T> {
bool get isSuccess {
final _code = code ?? 0;
if (_code >= 200 && _code < 299) return true;
return status == "success";
return status?.toUpperCase() == "SUCCESS";
}
const BaseResponseModel({this.code, this.status, this.errorMessage, this.errorCode, this.message, this.data});
......
......@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/app_loading.dart';
import 'package:mypoint_flutter_app/networking/app_navigator.dart';
import '../networking/dio_http_service.dart';
import '../resources/base_color.dart';
import '../widgets/alert/custom_alert_dialog.dart';
......@@ -52,6 +53,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen> {
bool showCloseButton = true,
VoidCallback? onConfirmed,
}) {
if (AppNavigator.isShowingDialog) return;
Get.dialog(
CustomAlertDialog(
showCloseButton: showCloseButton,
......
class APIPaths {//sandbox
static const String baseUrl = "https://api.sandbox.mypoint.com.vn/8854/gup2start/rest";
// static const String baseUrl = "https://api.sandbox.mypoint.com.vn/8854/gup2start/rest";
static const String checkUpdate = "/version-management-service/api/v1.0/check-customer-software-update";
static const String getOnboardingInfo = "/resource/api/v2.0/intro-screen";
static const String checkPhoneNumber = "/user/api/v2.0/account/users/checkPhoneNumber";
......
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle, MethodChannel;
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'app_config.dart';
import 'package:flutter/services.dart' show rootBundle, MethodChannel;
Future<void> loadEnv() async {
String flavor = 'dev'; // default flavor
String baseUrl = 'https://api.sandbox.mypoint.com.vn/8854/gup2start/rest';
String t3Token = 'dev-xxx';
bool enableLogging = true;
// Try to get config from BuildConfig if available
try {
const MethodChannel channel = MethodChannel('com.icom.mypoint/config');
final Map<dynamic, dynamic> config = await channel.invokeMethod('getConfig');
flavor = config['flavor'] ?? 'dev';
baseUrl = config['baseUrl'] ?? baseUrl;
t3Token = config['libToken'] ?? t3Token;
enableLogging = config['enableLogging'] ?? enableLogging;
} catch (e) {
class AppConfig {
final String flavor;
final String baseUrl;
final String t3Token;
final bool enableLogging;
AppConfig({
required this.flavor,
required this.baseUrl,
required this.t3Token,
required this.enableLogging,
});
factory AppConfig.fromMap(Map<String, dynamic> map) {
final flavor = (map['flavor'] as String?)?.trim();
final baseUrl = (map['baseUrl'] as String?)?.trim();
final token = (map['t3Token'] as String?)?.trim();
final enable = map['enableLogging'];
if ((flavor ?? '').isEmpty) throw const FormatException("env thiếu 'flavor'");
if ((baseUrl ?? '').isEmpty) throw const FormatException("env thiếu 'baseUrl'");
// if ((token ?? '').isEmpty) throw const FormatException("env thiếu 'libToken'");
if (kDebugMode) {
print('Could not get config from BuildConfig, using default: $e');
print('AppConfig: flavor=$flavor, baseUrl=$baseUrl, t3Token=${token ?? ''}, enableLogging=$enable');
}
return AppConfig(
flavor: flavor!,
baseUrl: baseUrl!,
t3Token: token ?? '',
enableLogging: enable is bool ? enable : true,
);
}
AppConfig.current = AppConfig(
name: flavor,
baseUrl: baseUrl,
t3Token: t3Token,
enableLogging: enableLogging,
);
static late AppConfig current;
}
Future<void> loadEnv() async {
Map<String, dynamic>? cfg;
if (Platform.isIOS) {
cfg = await _tryLoadAsset('assets/config/env.json');
cfg ??= await _tryLoadFromMethodChannel();
} else if (Platform.isAndroid) {
cfg = await _tryLoadFromMethodChannel();
cfg ??= await _tryLoadAsset('assets/config/env.json');
} else {
cfg = await _tryLoadAsset('assets/config/env.json');
}
if (cfg == null) {
throw Exception('Không tải được cấu hình môi trường (env).');
}
AppConfig.current = AppConfig.fromMap(cfg);
if (kDebugMode) {
print('✅ AppConfig loaded: flavor=${AppConfig.current.flavor}');
}
}
Future<Map<String, dynamic>?> _tryLoadAsset(String path) async {
try {
final s = await rootBundle.loadString(path);
return jsonDecode(s) as Map<String, dynamic>;
} catch (_) {
return null;
}
}
Future<Map<String, dynamic>?> _tryLoadFromMethodChannel() async {
try {
const channel = MethodChannel('com.icom.mypoint/config');
final dynamic result = await channel.invokeMethod('getConfig');
if (result is String) {
final json = jsonDecode(result) as Map<String, dynamic>;
return json;
} else if (result is Map) {
final map = Map<String, dynamic>.from(result as Map);
return map;
}
} catch (_) {}
return null;
}
\ No newline at end of file
......@@ -14,6 +14,7 @@ class AppNavigator {
static bool _networkDialogShown = false;
static bool _errorDialogShown = false;
static bool get isShowingDialog => _authDialogShown || _networkDialogShown || _errorDialogShown;
static BuildContext? get _ctx => key.currentContext;
static Future<void> showAuthAlertAndGoLogin(String message) async {
......
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
import '../app_config.dart';
import '../env_loader.dart';
import 'interceptor/auth_interceptor.dart';
import 'interceptor/exception_interceptor.dart';
import 'interceptor/logger_interceptor.dart';
......
......@@ -5,12 +5,14 @@ import 'package:mypoint_flutter_app/preference/data_preference.dart';
class AuthInterceptor extends Interceptor {
bool _isHandlingAuth = false;
static const _kAuthHandledKey = '__auth_handled__';
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
Future<void> onResponse(Response response, ResponseInterceptorHandler handler) async {
final data = response.data;
if (_isTokenInvalid(data)) {
_handleAuthError(data);
response.requestOptions.extra[_kAuthHandledKey] = true;
await _handleAuthError(data);
handler.reject(
DioException(
requestOptions: response.requestOptions
......@@ -28,8 +30,10 @@ class AuthInterceptor extends Interceptor {
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
final alreadyHandled = err.requestOptions.extra[_kAuthHandledKey] == true;
final data = err.response?.data;
final statusCode = err.response?.statusCode;
if (alreadyHandled) return;
if (statusCode == 401 || _isTokenInvalid(data)) {
await _handleAuthError(data);
return handler.reject(err);
......
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import '../resources/base_color.dart';
......@@ -12,8 +15,24 @@ enum BiometricTypeEnum {
}
class BiometricManager {
BiometricManager._();
static final BiometricManager _instance = BiometricManager._();
factory BiometricManager() => _instance;
final LocalAuthentication _localAuth = LocalAuthentication();
Future<bool> isDeviceSupported() async {
try {
final supported = await _localAuth.isDeviceSupported();
return supported;
} on PlatformException catch (e) {
debugPrint('[Bio] isDeviceSupported error: ${e.code} ${e.message}');
return false;
} catch (e) {
debugPrint('[Bio] isDeviceSupported unexpected: $e');
return false;
}
}
/// Kiểm tra xem thiết bị hỗ trợ loại sinh trắc học nào: faceID, fingerprint, none
Future<BiometricTypeEnum> checkDeviceBiometric() async {
try {
......
......@@ -12,7 +12,7 @@ class UserPointManager extends RestfulApiViewModel {
final RxInt _userPoint = 0.obs;
HeaderHomeModel? _headerInfo;
get point => _userPoint.value;
int get point => _userPoint.value;
Future<int?> fetchUserPoint() async {
if (!DataPreference.instance.logged) return null;
......
......@@ -2,7 +2,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/resources/base_color.dart';
import '../../preference/point/point_manager.dart';
import '../../resources/button_style.dart';
import '../../resources/text_style.dart';
......@@ -37,13 +39,13 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
@override
Widget build(BuildContext context) {
final int userPoints = UserPointManager().point;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 16),
Image.asset('assets/images/ic_pipi_03.png', height: 220),
const SizedBox(height: 16),
Text("Bạn có chắc chắn muốn xoá tài khoản?", style: AppTextStyle.title, textAlign: TextAlign.center),
......@@ -54,8 +56,8 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
style: AppTextStyle.content,
children: [
const TextSpan(text: "Toàn bộ "),
TextSpan(text: "$userPoints điểm ", style: AppTextStyle.boldContent),
const TextSpan(text: "và " ),
TextSpan(text: userPoints.money(CurrencyUnit.point), style: AppTextStyle.boldContent),
const TextSpan(text: " và " ),
WidgetSpan(
child: GestureDetector(
onTap: _onUnUsedVoucherPressed,
......@@ -84,9 +86,9 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith<Color>((states) {
if (states.contains(MaterialState.selected)) {
return Colors.red; // ✅ Checked: màu đỏ
return BaseColor.primary500; // ✅ Checked: màu đỏ
}
return Colors.white; // ❌ Unchecked: ô trắng
return Colors.white;
}),
checkColor: MaterialStateProperty.all(Colors.white), // ✅ Tick màu trắng
side: const BorderSide(color: Colors.grey),
......@@ -119,7 +121,7 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 40,
height: 48,
child: ElevatedButton(
onPressed: _viewModel.agreed.value ? _onConfirmDelete : null,
style: AppButtonStyle.secondary,
......@@ -129,7 +131,7 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 40,
height: 48,
child: ElevatedButton(
onPressed: () => Get.back(),
style: AppButtonStyle.primary,
......@@ -151,11 +153,12 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
}
void _onUnUsedVoucherPressed() {
print("Đi đến màn hình ưu đãi chưa sử dụng");
// TODO: Get.toNamed('/unused-voucher'); hoặc mở webview
Get.back();
Get.toNamed(myVoucherListScreen);
}
void _onTermConditionPressed() {
Get.back();
Get.toNamed(campaignDetailScreen, arguments: {"type": DetailPageRuleType.policyDeleteAccount});
}
}
......@@ -243,20 +243,19 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
Widget _buildBiometricSection(LoginViewModel vm) {
return Obx(() {
// Nếu thiết bị không hỗ trợ => ẩn
// if (vm.biometricType.value == BiometricTypeEnum.none) {
// return const SizedBox.shrink();
// }
// Hiển thị 1 icon tuỳ loại
IconData icon = Icons.fingerprint;
String label = "Vân tay";
if (!vm.isSupportedBiometric.value) {
return const SizedBox.shrink();
}
String icon = 'assets/images/ic_fingerprint.png';
String label = "vân tay";
if (vm.biometricType.value == BiometricTypeEnum.faceId) {
icon = Icons.face;
icon = 'assets/images/ic_face.png';
label = "Face ID";
}
return Column(
children: [
IconButton(
icon: Icon(icon, size: 36),
icon: Image.asset(icon, width: 40, height: 40),
onPressed: () => vm.onBiometricLoginPressed(phoneNumber),
),
Text("Đăng nhập bằng $label"),
......
......@@ -16,11 +16,10 @@ import '../main_tab_screen/main_tab_screen.dart';
enum LoginState { idle, typing, error }
class LoginViewModel extends RestfulApiViewModel {
final BiometricManager _biometricManager = BiometricManager();
var loginState = LoginState.idle.obs;
var isPasswordVisible = false.obs;
var password = "".obs;
var isSupportedBiometric = false.obs;
var biometricType = BiometricTypeEnum.none.obs;
void Function(String message)? onShowAlertError;
......@@ -31,16 +30,12 @@ class LoginViewModel extends RestfulApiViewModel {
@override
void onInit() {
super.onInit();
_initBiometric();
}
Future<void> _initBiometric() async {
final type = await _biometricManager.checkDeviceBiometric();
biometricType.value = type;
freshBiometric();
}
Future<bool> canUseBiometrics() async {
return _biometricManager.canCheckBiometrics();
Future<void> freshBiometric() async {
isSupportedBiometric.value = await BiometricManager().isDeviceSupported();
biometricType.value = await BiometricManager().checkDeviceBiometric();
}
void onPasswordChanged(String value) {
......@@ -128,8 +123,8 @@ class LoginViewModel extends RestfulApiViewModel {
/// Xác thực đăng nhập bằng sinh trắc
Future<void> onBiometricLoginPressed(String phone) async {
final canUse = await canUseBiometrics();
if (!canUse || biometricType.value == BiometricTypeEnum.none) {
final isSupported = await BiometricManager().isDeviceSupported();
if (!isSupported) {
onShowAlertError?.call("Thiết bị không hỗ trợ sinh trắc học");
return;
}
......
......@@ -10,7 +10,7 @@ class OnboardingViewModel extends RestfulApiViewModel {
var info = BaseResponseModel<OnboardingInfoModel>().obs;
var checkPhoneRes = BaseResponseModel<CheckPhoneResponseModel>().obs;
var phoneNumber = "".obs;
var isChecked = false.obs;
var isChecked = true.obs;
bool get isButtonEnabled => isChecked.value && phoneNumber.value.isPhoneValid();
String get content => info?.value?.data?.content ?? "";
......
......@@ -27,9 +27,10 @@ class DeleteAccountOtpRepository extends RestfulApiViewModel implements IOtpRepo
return client.verifyDeleteAccount(otpCode).then((value) {
hideLoading();
if (value.isSuccess) {
showToastMessage("Xóa tài khoản thành công");
DataPreference.instance.clearBioToken(phoneNumber);
DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
showToastMessage("Xóa tài khoản thành công");
}
return value;
});
......
......@@ -323,16 +323,10 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
}
void _safeBackToLogin() {
bool found = false;
Navigator.popUntil(Get.context!, (route) {
final matched = route.settings.name == loginScreen;
if (matched) found = true;
return matched;
});
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
final displayName = DataPreference.instance.displayName;
print("Safe back to login screen with phone: $phone, displayName: $displayName, found: $found");
if (phone != null && found) {
print("Safe back to login screen with phone: $phone, displayName: $displayName");
if (phone != null) {
Get.offAllNamed(loginScreen, arguments: {"phone": phone, 'fullName': displayName});
} else {
DataPreference.instance.clearData();
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
......@@ -21,28 +22,28 @@ class SettingViewModel extends RestfulApiViewModel {
Future<bool> toggleBiometric(bool enable) async {
final phone = DataPreference.instance.phone;
if (phone == null) return biometricEnabled;
final canCheckBiometrics = BiometricManager().canCheckBiometrics();
if (!(await canCheckBiometrics)) {
return biometricEnabled;
}
final didAuthenticate = BiometricManager().authenticateBiometric();
if (!(await didAuthenticate)) return biometricEnabled;
showLoading();
if (enable) {
client.registerBiometric().then((value) {
try {
final supported = await BiometricManager().isDeviceSupported();
if (!supported) return biometricEnabled;
final canCheck = await BiometricManager().canCheckBiometrics();
if (!canCheck) return biometricEnabled;
final didAuth = await BiometricManager().authenticateBiometric();
if (!didAuth) return biometricEnabled;
showLoading();
if (enable) {
final value = await client.registerBiometric();
final token = value.data?.bioToken ?? "";
DataPreference.instance.saveBioToken(token);
hideLoading();
await DataPreference.instance.saveBioToken(token);
return true;
});
} else {
client.unRegisterBiometric().then((value) {
DataPreference.instance.clearBioToken(phone!);
hideLoading();
} else {
await client.unRegisterBiometric();
await DataPreference.instance.clearBioToken(phone);
return false;
});
}
} catch (_) {
return biometricEnabled;
} finally {
hideLoading();
}
return enable;
}
}
......@@ -75,7 +75,7 @@ class _SupportScreenState extends State<SupportScreen> {
),
child: Row(
children: [
Icon(_getIcon(item.type), color: BaseColor.primary500, size: 24),
Icon(_getIcon(item.type), color: Colors.black54, size: 24),
const SizedBox(width: 10),
Expanded(
child: Text(item.title, style: const TextStyle(fontSize: 16)),
......
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