Commit f1723336 authored by DatHV's avatar DatHV
Browse files

cập nhật ui, lịch sử điểm.. base networking

parent 38520c1e
...@@ -9,6 +9,7 @@ import 'package:mypoint_flutter_app/screen/game/models/game_bundle_response.dart ...@@ -9,6 +9,7 @@ import 'package:mypoint_flutter_app/screen/game/models/game_bundle_response.dart
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'; import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import '../configs/callbacks.dart'; import '../configs/callbacks.dart';
import '../configs/device_info.dart'; import '../configs/device_info.dart';
import '../directional/directional_screen.dart';
import '../model/auth/biometric_register_response_model.dart'; import '../model/auth/biometric_register_response_model.dart';
import '../model/auth/login_token_response_model.dart'; import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart'; import '../model/auth/profile_response_model.dart';
...@@ -960,4 +961,11 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -960,4 +961,11 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
return ListHistoryResponseModel.fromJson(data as Json); return ListHistoryResponseModel.fromJson(data as Json);
}); });
} }
Future<BaseResponseModel<DirectionalScreen>> getDirectionOfflineBrand(String id) async {
final body = {"bank_account": id};
return requestNormal(APIPaths.getOfflineBrand, Method.GET, body, (data) {
return DirectionalScreen.fromJson(data as Json);
});
}
} }
\ No newline at end of file
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client.dart';
import '../base/base_response_model.dart';
import '../base/base_view_model.dart'; import '../base/base_view_model.dart';
import '../configs/constants.dart';
import 'app_navigator.dart';
import 'dio_http_service.dart'; import 'dio_http_service.dart';
import 'error_mapper.dart';
import 'interceptor/network_error_gate.dart';
typedef ApiCall<T> = Future<BaseResponseModel<T>> Function();
typedef OnSuccess<T> = FutureOr<void> Function(T data, BaseResponseModel<T> res);
typedef OnFailure<T> = FutureOr<void> Function(String message, BaseResponseModel<T>? res, Object? error);
typedef OnComplete = FutureOr<void> Function();
typedef ApiTask = Future<void> Function();
class RestfulApiViewModel extends BaseViewModel { class RestfulApiViewModel extends BaseViewModel {
final RestfulAPIClient _apiService = RestfulAPIClient(DioHttpService().dio); final RestfulAPIClient _apiService = RestfulAPIClient(DioHttpService().dio);
RestfulAPIClient get client => _apiService; RestfulAPIClient get client => _apiService;
Future<void> callApi<T>({
required ApiCall<T> request,
required OnSuccess<T> onSuccess,
OnFailure<T>? onFailure,
OnComplete? onComplete,
bool showAppNavigatorDialog = false,
bool withLoading = true,
String defaultError = ErrorCodes.commonError,
}) async {
if (withLoading) showLoading();
BaseResponseModel<T>? res;
try {
res = await request();
if (res.isSuccess) {
final T data = res.data as T;
await onSuccess(data, res);
} else {
final msg = res.errorMessage ?? defaultError;
final hasInternet = await NetworkConnectivity().hasInternet();
if (hasInternet) {
if (showAppNavigatorDialog) {
AppNavigator.showAlertError(content: msg);
} else {
await onFailure?.call(msg, res, null);
}
}
}
} catch (e) {
String msg = defaultError;
if (e is DioException) {
final mapped = e.requestOptions.extra['mapped_error'];
msg = (mapped is String && mapped.isNotEmpty) ? mapped : ErrorMapper.map(e);
} else {
msg = ErrorMapper.map(e);
}
final hasInternet = await NetworkConnectivity().hasInternet();
if (hasInternet) {
if (showAppNavigatorDialog) {
AppNavigator.showAlertError(content: msg);
} else {
await onFailure?.call(msg, res, null);
}
}
} finally {
if (withLoading) hideLoading();
onComplete?.call();
}
}
Future<void> runAllApis({
required List<ApiTask> tasks,
OnComplete? onComplete,
bool withLoading = true,
}) async {
if (withLoading) showLoading();
try {
final futures = tasks.map((t) => t()).toList();
await Future.wait(futures);
} finally {
if (withLoading) hideLoading();
onComplete?.call();
}
}
} }
\ No newline at end of file
...@@ -14,18 +14,21 @@ class UserPointManager extends RestfulApiViewModel { ...@@ -14,18 +14,21 @@ class UserPointManager extends RestfulApiViewModel {
get point => _userPoint.value; get point => _userPoint.value;
Future fetchUserPoint() async { Future<int?> fetchUserPoint() async {
if (!DataPreference.instance.logged) return; if (!DataPreference.instance.logged) return null;
try { try {
final response = await client.getHomeHeaderData(); final response = await client.getHomeHeaderData();
if (response.isSuccess && response.data != null) { if (response.isSuccess && response.data != null) {
_headerInfo = response.data; _headerInfo = response.data;
_userPoint.value = _headerInfo?.totalPointActive ?? 0; _userPoint.value = _headerInfo?.totalPointActive ?? 0;
return _userPoint.value;
} else { } else {
_userPoint.value = 0; _userPoint.value = 0;
return null;
} }
} catch (e) { } catch (e) {
_userPoint.value = 0; _userPoint.value = 0;
return null;
} }
} }
} }
...@@ -23,16 +23,12 @@ class AchievementViewModel extends RestfulApiViewModel { ...@@ -23,16 +23,12 @@ class AchievementViewModel extends RestfulApiViewModel {
"start": 0, "start": 0,
"limit": 1000, "limit": 1000,
}; };
showLoading(); await callApi<AchievementListResponse>(
try { request: () => client.getAchievementList(body),
final response = await client.getAchievementList(body); onSuccess: (data, _) {
if (response.data != null) { achievements.value = data.achievements ?? [];
achievements.value = response.data?.achievements ?? []; },
} showAppNavigatorDialog: true,
} catch (error) { );
print("Error fetching achievements: $error");
} finally {
hideLoading();
}
} }
} }
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import 'affiliate_popup_brands.dart';
import 'model/affiliate_brand_model.dart'; import 'model/affiliate_brand_model.dart';
import 'model/affiliate_category_model.dart'; import 'model/affiliate_category_model.dart';
import 'model/affiliate_category_type.dart'; import 'model/affiliate_category_type.dart';
......
...@@ -53,10 +53,7 @@ class AffiliateProductTopSale extends StatelessWidget { ...@@ -53,10 +53,7 @@ class AffiliateProductTopSale extends StatelessWidget {
final title = product.productName ?? ''; final title = product.productName ?? '';
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => product.direcionalScreen?.begin(),
print("name ${product.productLink}");
product.direcionalScreen?.begin();
},
child: Container( child: Container(
width: 160, width: 160,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
...@@ -70,12 +67,13 @@ class AffiliateProductTopSale extends StatelessWidget { ...@@ -70,12 +67,13 @@ class AffiliateProductTopSale extends StatelessWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.network( child: AspectRatio(
imageUrl, aspectRatio: 1,
scale: 1, child: Image.network(
width: double.infinity, imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Image.asset('assets/images/ic_logo.png'), errorBuilder: (_, __, ___) => Image.asset('assets/images/ic_logo.png'),
),
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
......
...@@ -71,7 +71,7 @@ class _DailyCheckInScreenState extends BaseState<DailyCheckInScreen> with BasicS ...@@ -71,7 +71,7 @@ class _DailyCheckInScreenState extends BaseState<DailyCheckInScreen> with BasicS
_buildCheckInList(), _buildCheckInList(),
const SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: 48, height: 48,
......
...@@ -64,7 +64,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu ...@@ -64,7 +64,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu
link: _layerLink, link: _layerLink,
child: IconButton( child: IconButton(
key: _infoKey, key: _infoKey,
icon: const Icon(Icons.info_outline, color: Colors.white), icon: const Icon(Icons.info, color: Colors.white),
onPressed: _togglePopup, onPressed: _togglePopup,
), ),
), ),
......
...@@ -208,15 +208,13 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit { ...@@ -208,15 +208,13 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
), ),
Obx(() { Obx(() {
if (!_showHover.value) return SizedBox.shrink(); if (!_showHover.value) return SizedBox.shrink();
return Positioned.fill( return HoverView(
child: HoverView( imagePath: _viewModel.hoverData.value?.icon ?? '',
imagePath: _viewModel.hoverData.value?.icon ?? '', onTap: _handleHoverViewTap,
onTap: _handleHoverViewTap, onClose: _handleCloseHoverView,
onClose: _handleCloseHoverView, backgroundColor: Colors.transparent,
backgroundColor: Colors.transparent, size: 80,
size: 80, countDownTime: _viewModel.hoverData.value?.countDownTime ?? 0.0,
countDownTime: _viewModel.hoverData.value?.countDownTime ?? 0.0,
),
); );
}), }),
], ],
......
...@@ -296,6 +296,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -296,6 +296,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
onPressed: () { onPressed: () {
hideKeyboard();
enabled ? vm.onLoginPressed(phoneNumber) : null; enabled ? vm.onLoginPressed(phoneNumber) : null;
}, },
child: const Text( child: const Text(
......
...@@ -189,7 +189,7 @@ class _MembershipScreenState extends BaseState<MembershipScreen> with BasicState ...@@ -189,7 +189,7 @@ class _MembershipScreenState extends BaseState<MembershipScreen> with BasicState
Get.toNamed(campaignDetailScreen, arguments: {"id": pageId}); Get.toNamed(campaignDetailScreen, arguments: {"id": pageId});
} }
}, },
child: SizedBox(width: 40, height: 40, child: Icon(Icons.info_outline, color: Colors.white, size: 24)), child: SizedBox(width: 40, height: 40, child: Icon(Icons.info, color: Colors.white, size: 24)),
), ),
), ),
); );
......
...@@ -15,7 +15,7 @@ class NotificationItemModel { ...@@ -15,7 +15,7 @@ class NotificationItemModel {
@JsonKey(name: 'click_action_param') @JsonKey(name: 'click_action_param')
final String? clickActionParam; final String? clickActionParam;
@JsonKey(name: 'seen_at') @JsonKey(name: 'seen_at')
final String? seenAt; String? seenAt;
final String? status; final String? status;
@JsonKey(name: 'create_time') @JsonKey(name: 'create_time')
final String? createTime; final String? createTime;
......
...@@ -167,21 +167,18 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS ...@@ -167,21 +167,18 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS
title: "Xoá tất cả", title: "Xoá tất cả",
description: "Bạn có muốn xoá hết tất cả thông báo không? \nLưu ý: bạn sẽ không thể xem lại thông báo đã xoá", description: "Bạn có muốn xoá hết tất cả thông báo không? \nLưu ý: bạn sẽ không thể xem lại thông báo đã xoá",
localHeaderImage: "assets/images/ic_pipi_03.png", localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [AlertButton( buttons: [
text: "Xoá",
onPressed: () {
Get.back();
_viewModel.deleteAllNotifications();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton( AlertButton(
text: "Huỷ", text: "Xoá",
onPressed: () => Get.back(), onPressed: () {
bgColor: Colors.white, Get.back();
textColor: BaseColor.second500, _viewModel.deleteAllNotifications();
),], },
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
); );
showAlert(data: dataAlert); showAlert(data: dataAlert);
} }
...@@ -271,9 +268,11 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS ...@@ -271,9 +268,11 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS
}, },
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
print("Click item ${item.title}");
_viewModel.handleClickNotification(item); _viewModel.handleClickNotification(item);
// item.directionalScreen?.begin(); // item.directionalScreen?.begin();
setState(() {
item.seenAt = DateTime.now().toString();
});
}, },
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../extensions/string_extension.dart'; import '../../extensions/string_extension.dart';
...@@ -52,7 +53,7 @@ class _CampaignDetailScreenState extends BaseState<CampaignDetailScreen> with Ba ...@@ -52,7 +53,7 @@ class _CampaignDetailScreenState extends BaseState<CampaignDetailScreen> with Ba
body: Obx(() { body: Obx(() {
CampaignDetailModel? pageDetail = _viewModel.campaignDetail.value.data?.pageDetail; CampaignDetailModel? pageDetail = _viewModel.campaignDetail.value.data?.pageDetail;
if (pageDetail == null) { if (pageDetail == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: EmptyWidget());
} }
final thumbnail = pageDetail.thumbnail ?? ""; final thumbnail = pageDetail.thumbnail ?? "";
final publishDate = pageDetail.publishDate ?? ""; final publishDate = pageDetail.publishDate ?? "";
......
...@@ -183,7 +183,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po ...@@ -183,7 +183,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
const Expanded( const Expanded(
child: Text( child: Text(
'Mời bạn nhận quà liền tay 🎁', 'Mời bạn nhận quà liền tay 🎁',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.black87), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87),
), ),
), ),
const Icon(Icons.chevron_right, color: Colors.black87, size: 24), const Icon(Icons.chevron_right, color: Colors.black87, size: 24),
...@@ -202,6 +202,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po ...@@ -202,6 +202,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
'title': 'Săn điểm', 'title': 'Săn điểm',
'type': 'APP_SCREEN_POINT_HUNTING', 'type': 'APP_SCREEN_POINT_HUNTING',
}, },
{'icon': Icons.qr_code_2, 'title': 'QR Code', 'type': 'APP_SCREEN_QR_CODE'},
{'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': 'DAILY_CHECKIN'}, {'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': 'DAILY_CHECKIN'},
{'icon': Icons.border_right, 'title': 'Hoá đơn điện', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'}, {'icon': Icons.border_right, 'title': 'Hoá đơn điện', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'},
// {'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'}, // {'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'},
......
import 'package:barcode_widget/barcode_widget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/preference/point/point_manager.dart';
import 'package:mypoint_flutter_app/screen/qr_code/qr_code_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/qr_code/scan_code_screen.dart';
import 'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart';
import 'package:mypoint_flutter_app/widgets/dashed_line.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../preference/data_preference.dart';
class QRCodeScreen extends BaseScreen {
const QRCodeScreen({super.key});
@override
State<QRCodeScreen> createState() => _QRCodeScreenState();
}
class _QRCodeScreenState extends BaseState<QRCodeScreen> with BasicState {
final _viewModel = Get.put(QRCodeViewModel());
final _scanCtl = ScanTabController();
int _tab = 0;
final RxInt _points = 0.obs;
final String _code = DataPreference.instance.profile?.workerSite?.id ?? '';
final RxBool _isShowPoint = true.obs;
@override
void initState() {
super.initState();
_freshPoint();
_viewModel.onShowAlertError = (message) {
if (message.isEmpty) {
_scanCtl.resume();
return;
}
_showAlertAndResumeScanCode(message);
};
}
_freshPoint() async {
_points.value = (await UserPointManager().fetchUserPoint()) ?? 0;
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: _tab == 0 ? 'Mã tích điểm' : 'Quét mã'),
backgroundColor: Color(0xFFF3F4F6),
body: SafeArea(
child: Stack(
children: [
_tab == 0 ? _buildYourCode() : _buildScanQRCode(),
Positioned(
top: 12,
left: 16,
right: 16,
child: SegmentedTabs(
tabs: const ['Mã của bạn', 'Quét mã'],
index: _tab,
onChanged: (i) => setState(() => _tab = i),
),
),
],
),
),
);
}
Widget _buildYourCode() {
final width = MediaQuery.of(context).size.width;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
const SizedBox(height: 64),
Image.asset('assets/images/ic_pipi_04.png', width: 120, height: 120),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: const [BoxShadow(color: Color(0x14000000), blurRadius: 16, offset: Offset(0, 6))],
),
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'Vui lòng đưa nhân viên thu ngân quét mã để tích điểm',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF0C2B4C)),
),
const SizedBox(height: 12),
BarcodeWidget(
barcode: Barcode.code128(),
data: _code,
width: double.infinity,
height: 80,
drawText: false,
color: Colors.black,
backgroundColor: Colors.white,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
QrImageView(
data: _code,
version: QrVersions.auto,
size: width / 1.7,
embeddedImage: const AssetImage('assets/images/ic_logo.png'),
embeddedImageStyle: const QrEmbeddedImageStyle(size: Size(64, 64)),
),
const SizedBox(height: 12),
DashedLine(),
const SizedBox(height: 12),
Row(
children: [
const Text(
'MyPoint',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF0C2B4C)),
),
const Spacer(),
Obx(
() => Row(
children: [
Image.asset('assets/images/ic_point.png', width: 24, height: 24),
const SizedBox(width: 6),
Text(
_isShowPoint.value ? '******' : _points.value.money(CurrencyUnit.point),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
const SizedBox(width: 6),
GestureDetector(
onTap: () {
setState(() {
_isShowPoint.toggle();
});
},
child: Icon(
_isShowPoint.value ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.black38,
size: 24,
),
),
],
),
),
],
),
],
),
),
],
),
);
}
_showAlertAndResumeScanCode(String message) {
showAlertError(
content: message,
showCloseButton: false,
onConfirmed: () {
_scanCtl.resume();
},
);
}
Widget _buildScanQRCode() {
return ScanTabView(
controller: _scanCtl,
onCodeDetected: (code, format) async {
final id = _viewModel.getResultCodeID(code) ?? '';
// if (id.isEmpty) {
// final url = code.toUri();
// if (url == null) {
// _showAlertAndResumeScanCode('Mã QR không hợp lệ');
// } else {
// print('_buildScanQRCode $url');
// _scanCtl.resume();
// }
// return;
// }
_viewModel.getDirectionFromId(code);
},
);
}
}
/// Segmented control (pill) giống iOS
class SegmentedTabs extends StatelessWidget {
const SegmentedTabs({super.key, required this.tabs, required this.index, required this.onChanged});
final List<String> tabs;
final int index;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(color: Color(0x22000000), borderRadius: BorderRadius.circular(24)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(tabs.length, (i) {
final selected = i == index;
return SizedBox(
width: 160,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => onChanged(i),
child: AnimatedContainer(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: selected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(20),
boxShadow:
selected
? const [BoxShadow(color: Color(0x22000000), blurRadius: 8, offset: Offset(0, 3))]
: const [],
),
child: Text(
tabs[i],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: selected ? const Color(0xFF0C2B4C) : Colors.white,
),
),
),
),
);
}),
),
),
);
}
}
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
class QRPackageCodeValue {
final String type;
final String length;
final String value;
const QRPackageCodeValue({required this.type, required this.length, required this.value});
}
class QRCodeViewModel extends RestfulApiViewModel {
void Function(String message)? onShowAlertError;
getDirectionFromId(String id) async {
showLoading();
final response = await client.getDirectionOfflineBrand(id);
final direction = response.data;
if (response.isSuccess && direction != null) {
hideLoading();
final directionSuccess = await direction.begin();
if (directionSuccess != true) {
onShowAlertError?.call(ErrorCodes.serverErrorMessage);
} else {
onShowAlertError?.call('');
}
} else {
hideLoading();
onShowAlertError?.call(response.errorMessage ?? ErrorCodes.serverErrorMessage);
}
}
int _crc16CcittEmv(List<int> bytes) {
const poly = 0x1021;
var crc = 0xFFFF;
for (final b in bytes) {
crc ^= (b & 0xFF) << 8;
for (var i = 0; i < 8; i++) {
final carry = (crc & 0x8000) != 0;
crc = (crc << 1) & 0xFFFF;
if (carry) crc ^= poly;
}
}
return crc & 0xFFFF;
}
bool emvCrcValid(String full, String crcHexFromPayload) {
final idx = full.lastIndexOf('63');
if (idx < 0 || idx + 4 > full.length) return false; // không có "63" + "LEN"
final lenStr = full.substring(idx + 2, idx + 4);
final len = int.tryParse(lenStr) ?? 0;
if (len != 4) return false;
// dữ liệu tham gia CRC: từ đầu → sau "63" + "04" (không gồm value CRC)
final data = full.substring(0, idx + 4);
final crc = _crc16CcittEmv(data.codeUnits)
.toRadixString(16)
.toUpperCase()
.padLeft(4, '0');
return crc == crcHexFromPayload.toUpperCase();
}
QRPackageCodeValue? _findByType(List<QRPackageCodeValue> list, String type) {
for (final e in list) {
if (e.type == type) return e;
}
return null;
}
String? getResultCodeID(String code) {
final codes = getPackageCode(code);
final crcField = _findByType(codes, '63');
final merchantAccountInfo = _findByType(codes, '38');
if (crcField == null || merchantAccountInfo == null) return null;
if (!emvCrcValid(code, crcField.value)) return null;
final merchantInfoValue = getPackageCode(merchantAccountInfo.value);
final merchantIdInfo = _findByType(merchantInfoValue, '01');
if (merchantIdInfo == null) return null;
final merchantData = getPackageCode(merchantIdInfo.value);
final bankIdField = _findByType(merchantData, '01');
return bankIdField?.value;
}
List<QRPackageCodeValue> getPackageCode(String input) {
final codes = <QRPackageCodeValue>[];
var s = input;
while (s.isNotEmpty) {
if (s.length < 4) break;
final id = s.substring(0, 2);
s = s.substring(2);
final lenStr = s.substring(0, 2);
s = s.substring(2);
final len = int.tryParse(lenStr) ?? 0;
if (len < 0 || s.length < len) break;
final value = s.substring(0, len);
s = s.substring(len);
codes.add(QRPackageCodeValue(type: id, length: lenStr, value: value));
}
return codes;
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScanTabController {
Future<void> Function()? _resume;
Future<void> Function()? _pause;
Future<void> Function()? _toggleTorch;
Future<void> resume() async => await (_resume?.call() ?? Future.value());
Future<void> pause() async => await (_pause?.call() ?? Future.value());
Future<void> toggleTorch() async => await (_toggleTorch?.call() ?? Future.value());
}
class ScanTabView extends StatefulWidget {
const ScanTabView({
super.key,
this.onCodeDetected,
this.controller,
});
final void Function(String code, BarcodeFormat format)? onCodeDetected;
final ScanTabController? controller;
@override
State<ScanTabView> createState() => _ScanTabViewState();
}
class _ScanTabViewState extends State<ScanTabView> {
final MobileScannerController _cam = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: [
BarcodeFormat.ean8,
BarcodeFormat.ean13,
BarcodeFormat.pdf417,
BarcodeFormat.code128,
BarcodeFormat.code39,
BarcodeFormat.qrCode,
],
);
bool _locked = false;
@override
void initState() {
super.initState();
widget.controller?._resume = _resume;
widget.controller?._pause = _pause;
widget.controller?._toggleTorch = () => _cam.toggleTorch();
}
@override
void dispose() {
_cam.dispose();
super.dispose();
}
Future<void> _resume() async {
_locked = false;
await _cam.start();
}
Future<void> _pause() async => _cam.stop();
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, c) {
final size = c.biggest;
final side = (size.width - 64).clamp(220.0, size.height * 0.6);
final left = (size.width - side) / 2;
final top = (size.height - side) / 2;
final scanRect = Rect.fromLTWH(left, top, side, side);
return Stack(
children: [
Positioned.fill(
child: MobileScanner(
controller: _cam,
fit: BoxFit.cover,
scanWindow: scanRect,
onDetect: (capture) async {
if (_locked) return;
final b = capture.barcodes.firstOrNull;
final code = b?.rawValue ?? '';
final format = b?.format ?? BarcodeFormat.unknown;
if (code.isEmpty) return;
_locked = true;
await _cam.stop();
widget.onCodeDetected?.call(code, format);
},
),
),
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: _ScannerOverlayPainter(scanWindow: scanRect),
),
),
),
// Caption hướng dẫn
Positioned(
left: 16,
right: 16,
bottom: 90,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.35),
borderRadius: BorderRadius.circular(24),
),
child: const Text(
'Căn chỉnh mã QR vào vị trí trong khung hình.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600),
),
),
),
// Nút bật đèn
Positioned(
bottom: 32,
left: 0,
right: 0,
child: Center(
child: ValueListenableBuilder<TorchState>(
valueListenable: _cam.torchState,
builder: (_, state, __) {
final on = state == TorchState.on;
return InkWell(
onTap: () => _cam.toggleTorch(),
borderRadius: BorderRadius.circular(32),
child: Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.25),
shape: BoxShape.circle,
border: Border.all(color: Colors.white70),
),
alignment: Alignment.center,
child: Icon(
on ? Icons.flashlight_off_outlined : Icons.flashlight_on_outlined,
color: Colors.white, size: 24,
),
),
);
},
),
),
),
],
);
});
}
}
class _ScannerOverlayPainter extends CustomPainter {
_ScannerOverlayPainter({required this.scanWindow});
final Rect scanWindow;
@override
void paint(Canvas canvas, Size size) {
// nền mờ
final bg = Path()..addRect(Offset.zero & size);
final hole = Path()..addRRect(RRect.fromRectAndRadius(scanWindow, const Radius.circular(2)));
final mask = Path.combine(PathOperation.difference, bg, hole);
final paintMask = Paint()..color = Colors.black.withOpacity(0.65);
canvas.drawPath(mask, paintMask);
// viền trắng
final border = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawRRect(RRect.fromRectAndRadius(scanWindow, const Radius.circular(2)), border);
// 4 góc xanh
const cornerLen = 18.0;
final corner = Paint()
..color = const Color(0xFF247CFF)
..style = PaintingStyle.stroke
..strokeWidth = 3;
// TL
canvas.drawLine(scanWindow.topLeft + const Offset(0, 0), scanWindow.topLeft + const Offset(cornerLen, 0), corner);
canvas.drawLine(scanWindow.topLeft + const Offset(0, 0), scanWindow.topLeft + const Offset(0, cornerLen), corner);
// TR
canvas.drawLine(scanWindow.topRight + const Offset(-cornerLen, 0), scanWindow.topRight, corner);
canvas.drawLine(scanWindow.topRight + const Offset(0, 0), scanWindow.topRight + const Offset(0, cornerLen), corner);
// BL
canvas.drawLine(scanWindow.bottomLeft + const Offset(0, -cornerLen), scanWindow.bottomLeft, corner);
canvas.drawLine(scanWindow.bottomLeft + const Offset(0, 0), scanWindow.bottomLeft + const Offset(cornerLen, 0), corner);
// BR
canvas.drawLine(scanWindow.bottomRight + const Offset(-cornerLen, 0), scanWindow.bottomRight, corner);
canvas.drawLine(scanWindow.bottomRight + const Offset(0, -cornerLen), scanWindow.bottomRight, corner);
}
@override
bool shouldRepaint(covariant _ScannerOverlayPainter oldDelegate) =>
oldDelegate.scanWindow != scanWindow;
}
...@@ -4,6 +4,8 @@ import 'package:intl/intl.dart'; ...@@ -4,6 +4,8 @@ import 'package:intl/intl.dart';
import 'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart'; import 'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart';
import 'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'; import 'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart'; import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../extensions/debouncer.dart'; import '../../extensions/debouncer.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
...@@ -11,14 +13,15 @@ import '../../shared/router_gage.dart'; ...@@ -11,14 +13,15 @@ import '../../shared/router_gage.dart';
import '../contacts/contacts_picker.dart'; import '../contacts/contacts_picker.dart';
import 'brand_select_sheet_widget.dart'; import 'brand_select_sheet_widget.dart';
class PhoneTopUpScreen extends StatefulWidget {
class PhoneTopUpScreen extends BaseScreen {
const PhoneTopUpScreen({super.key}); const PhoneTopUpScreen({super.key});
@override @override
State<PhoneTopUpScreen> createState() => _PhoneTopUpScreenState(); State<PhoneTopUpScreen> createState() => _PhoneTopUpScreenState();
} }
class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> { class _PhoneTopUpScreenState extends BaseState<PhoneTopUpScreen> with BasicState {
final TopUpViewModel _viewModel = Get.put(TopUpViewModel()); final TopUpViewModel _viewModel = Get.put(TopUpViewModel());
late final TextEditingController _phoneController; late final TextEditingController _phoneController;
final _deb = Debouncer(ms: 500); final _deb = Debouncer(ms: 500);
...@@ -39,7 +42,7 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> { ...@@ -39,7 +42,7 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
} }
@override @override
Widget build(BuildContext context) { Widget createBody() {
return Scaffold( return Scaffold(
appBar: CustomNavigationBar(title: "Nạp tiền điện thoại"), appBar: CustomNavigationBar(title: "Nạp tiền điện thoại"),
body: Obx(() { body: Obx(() {
......
...@@ -2,6 +2,7 @@ import 'package:get/get.dart'; ...@@ -2,6 +2,7 @@ import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart'; import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/topup/models/brand_network_model.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../preference/contact_storage_service.dart'; import '../../preference/contact_storage_service.dart';
import '../voucher/models/product_brand_model.dart'; import '../voucher/models/product_brand_model.dart';
...@@ -45,39 +46,32 @@ class TopUpViewModel extends RestfulApiViewModel { ...@@ -45,39 +46,32 @@ class TopUpViewModel extends RestfulApiViewModel {
_getTopUpBrands(); _getTopUpBrands();
} }
_getTopUpBrands() { _getTopUpBrands() async {
showLoading(); await callApi<List<ProductBrandModel>>(
client.getTopUpBrands(ProductType.topupMobile).then((response) { request: () => client.getTopUpBrands(ProductType.topupMobile),
topUpBrands.value = response.data ?? []; onSuccess: (data, _) {
hideLoading(); topUpBrands.value = data;
checkMobileNetwork(); checkMobileNetwork();
}).catchError((error) { },
hideLoading(); showAppNavigatorDialog: true,
}); );
} }
checkMobileNetwork() { checkMobileNetwork() async {
showLoading(); await callApi<BrandNameCheckResponse>(
client.checkMobileNetwork(phoneNumber.value).then((response) { request: () => client.checkMobileNetwork(phoneNumber.value),
final brandCode = response.data?.brand ?? ''; onSuccess: (data, _) {
final brand = topUpBrands.isNotEmpty final brandCode = data?.brand ?? '';
? topUpBrands.firstWhere( var brand = topUpBrands.isNotEmpty
(brand) => brand.code == brandCode, ? topUpBrands.firstWhere(
orElse: () => topUpBrands.first, (brand) => brand.code == brandCode,
) orElse: () => topUpBrands.first,
: null; ) : topUpBrands.value.firstOrNull;
selectedBrand.value = brand; selectedBrand.value = brand;
hideLoading(); getTelcoDetail();
getTelcoDetail(); },
}).catchError((error) { showAppNavigatorDialog: true,
final first = topUpBrands.value.firstOrNull; );
if (first != null) {
selectedBrand.value = first;
}
hideLoading();
getTelcoDetail();
print('Error checking mobile network: $error');
});
} }
Future<void> getTelcoDetail({String? selected}) async { Future<void> getTelcoDetail({String? selected}) async {
...@@ -112,23 +106,20 @@ class TopUpViewModel extends RestfulApiViewModel { ...@@ -112,23 +106,20 @@ class TopUpViewModel extends RestfulApiViewModel {
makeSelected(cached); makeSelected(cached);
return; return;
} }
showLoading();
final body = { final body = {
"type": ProductType.topupMobile.value, "type": ProductType.topupMobile.value,
"size": 200, "size": 200,
"index": 0, "index": 0,
"brand_id": selectedBrand.value?.id ?? 0, "brand_id": selectedBrand.value?.id ?? 0,
}; };
try { await callApi<List<ProductModel>>(
final result = await client.getProducts(body); request: () => client.getProducts(body),
final data = result.data ?? []; onSuccess: (data, _) {
_allValue[code] = data; _allValue[code] = data;
products.value = result.data ?? []; products.value = data;
hideLoading(); makeSelected(data);
makeSelected(data); },
} catch (error) { showAppNavigatorDialog: true,
print("Error fetching all products: $error"); );
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