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
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import '../configs/callbacks.dart';
import '../configs/device_info.dart';
import '../directional/directional_screen.dart';
import '../model/auth/biometric_register_response_model.dart';
import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart';
......@@ -960,4 +961,11 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
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 '../base/base_response_model.dart';
import '../base/base_view_model.dart';
import '../configs/constants.dart';
import 'app_navigator.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 {
final RestfulAPIClient _apiService = RestfulAPIClient(DioHttpService().dio);
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 {
get point => _userPoint.value;
Future fetchUserPoint() async {
if (!DataPreference.instance.logged) return;
Future<int?> fetchUserPoint() async {
if (!DataPreference.instance.logged) return null;
try {
final response = await client.getHomeHeaderData();
if (response.isSuccess && response.data != null) {
_headerInfo = response.data;
_userPoint.value = _headerInfo?.totalPointActive ?? 0;
return _userPoint.value;
} else {
_userPoint.value = 0;
return null;
}
} catch (e) {
_userPoint.value = 0;
return null;
}
}
}
......@@ -23,16 +23,12 @@ class AchievementViewModel extends RestfulApiViewModel {
"start": 0,
"limit": 1000,
};
showLoading();
try {
final response = await client.getAchievementList(body);
if (response.data != null) {
achievements.value = response.data?.achievements ?? [];
}
} catch (error) {
print("Error fetching achievements: $error");
} finally {
hideLoading();
}
await callApi<AchievementListResponse>(
request: () => client.getAchievementList(body),
onSuccess: (data, _) {
achievements.value = data.achievements ?? [];
},
showAppNavigatorDialog: true,
);
}
}
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.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 'model/affiliate_category_type.dart';
......
......@@ -53,10 +53,7 @@ class AffiliateProductTopSale extends StatelessWidget {
final title = product.productName ?? '';
return GestureDetector(
onTap: () {
print("name ${product.productLink}");
product.direcionalScreen?.begin();
},
onTap: () => product.direcionalScreen?.begin(),
child: Container(
width: 160,
padding: const EdgeInsets.all(8),
......@@ -70,14 +67,15 @@ class AffiliateProductTopSale extends StatelessWidget {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: AspectRatio(
aspectRatio: 1,
child: Image.network(
imageUrl,
scale: 1,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Image.asset('assets/images/ic_logo.png'),
),
),
),
const SizedBox(height: 6),
Text(title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13)),
const SizedBox(height: 4),
......
......@@ -71,7 +71,7 @@ class _DailyCheckInScreenState extends BaseState<DailyCheckInScreen> with BasicS
_buildCheckInList(),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
height: 48,
......
......@@ -64,7 +64,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu
link: _layerLink,
child: IconButton(
key: _infoKey,
icon: const Icon(Icons.info_outline, color: Colors.white),
icon: const Icon(Icons.info, color: Colors.white),
onPressed: _togglePopup,
),
),
......
......@@ -208,15 +208,13 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
),
Obx(() {
if (!_showHover.value) return SizedBox.shrink();
return Positioned.fill(
child: HoverView(
return HoverView(
imagePath: _viewModel.hoverData.value?.icon ?? '',
onTap: _handleHoverViewTap,
onClose: _handleCloseHoverView,
backgroundColor: Colors.transparent,
size: 80,
countDownTime: _viewModel.hoverData.value?.countDownTime ?? 0.0,
),
);
}),
],
......
......@@ -296,6 +296,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () {
hideKeyboard();
enabled ? vm.onLoginPressed(phoneNumber) : null;
},
child: const Text(
......
......@@ -189,7 +189,7 @@ class _MembershipScreenState extends BaseState<MembershipScreen> with BasicState
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 {
@JsonKey(name: 'click_action_param')
final String? clickActionParam;
@JsonKey(name: 'seen_at')
final String? seenAt;
String? seenAt;
final String? status;
@JsonKey(name: 'create_time')
final String? createTime;
......
......@@ -167,7 +167,8 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS
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á",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [AlertButton(
buttons: [
AlertButton(
text: "Xoá",
onPressed: () {
Get.back();
......@@ -176,12 +177,8 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(
text: "Huỷ",
onPressed: () => Get.back(),
bgColor: Colors.white,
textColor: BaseColor.second500,
),],
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
);
showAlert(data: dataAlert);
}
......@@ -271,9 +268,11 @@ class _NotificationScreenState extends BaseState<NotificationScreen> with BasicS
},
child: GestureDetector(
onTap: () {
print("Click item ${item.title}");
_viewModel.handleClickNotification(item);
// item.directionalScreen?.begin();
setState(() {
item.seenAt = DateTime.now().toString();
});
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
......
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../extensions/string_extension.dart';
......@@ -52,7 +53,7 @@ class _CampaignDetailScreenState extends BaseState<CampaignDetailScreen> with Ba
body: Obx(() {
CampaignDetailModel? pageDetail = _viewModel.campaignDetail.value.data?.pageDetail;
if (pageDetail == null) {
return const Center(child: CircularProgressIndicator());
return const Center(child: EmptyWidget());
}
final thumbnail = pageDetail.thumbnail ?? "";
final publishDate = pageDetail.publishDate ?? "";
......
......@@ -183,7 +183,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
const Expanded(
child: Text(
'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),
......@@ -202,6 +202,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
'title': 'Săn điểm',
'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.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'},
......
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';
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/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../extensions/debouncer.dart';
import '../../preference/data_preference.dart';
import '../../resources/base_color.dart';
......@@ -11,14 +13,15 @@ import '../../shared/router_gage.dart';
import '../contacts/contacts_picker.dart';
import 'brand_select_sheet_widget.dart';
class PhoneTopUpScreen extends StatefulWidget {
class PhoneTopUpScreen extends BaseScreen {
const PhoneTopUpScreen({super.key});
@override
State<PhoneTopUpScreen> createState() => _PhoneTopUpScreenState();
}
class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
class _PhoneTopUpScreenState extends BaseState<PhoneTopUpScreen> with BasicState {
final TopUpViewModel _viewModel = Get.put(TopUpViewModel());
late final TextEditingController _phoneController;
final _deb = Debouncer(ms: 500);
......@@ -39,7 +42,7 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
}
@override
Widget build(BuildContext context) {
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: "Nạp tiền điện thoại"),
body: Obx(() {
......
......@@ -2,6 +2,7 @@ import 'package:get/get.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/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/topup/models/brand_network_model.dart';
import '../../networking/restful_api_viewmodel.dart';
import '../../preference/contact_storage_service.dart';
import '../voucher/models/product_brand_model.dart';
......@@ -45,39 +46,32 @@ class TopUpViewModel extends RestfulApiViewModel {
_getTopUpBrands();
}
_getTopUpBrands() {
showLoading();
client.getTopUpBrands(ProductType.topupMobile).then((response) {
topUpBrands.value = response.data ?? [];
hideLoading();
_getTopUpBrands() async {
await callApi<List<ProductBrandModel>>(
request: () => client.getTopUpBrands(ProductType.topupMobile),
onSuccess: (data, _) {
topUpBrands.value = data;
checkMobileNetwork();
}).catchError((error) {
hideLoading();
});
},
showAppNavigatorDialog: true,
);
}
checkMobileNetwork() {
showLoading();
client.checkMobileNetwork(phoneNumber.value).then((response) {
final brandCode = response.data?.brand ?? '';
final brand = topUpBrands.isNotEmpty
checkMobileNetwork() async {
await callApi<BrandNameCheckResponse>(
request: () => client.checkMobileNetwork(phoneNumber.value),
onSuccess: (data, _) {
final brandCode = data?.brand ?? '';
var brand = topUpBrands.isNotEmpty
? topUpBrands.firstWhere(
(brand) => brand.code == brandCode,
orElse: () => topUpBrands.first,
)
: null;
) : topUpBrands.value.firstOrNull;
selectedBrand.value = brand;
hideLoading();
getTelcoDetail();
}).catchError((error) {
final first = topUpBrands.value.firstOrNull;
if (first != null) {
selectedBrand.value = first;
}
hideLoading();
getTelcoDetail();
print('Error checking mobile network: $error');
});
},
showAppNavigatorDialog: true,
);
}
Future<void> getTelcoDetail({String? selected}) async {
......@@ -112,23 +106,20 @@ class TopUpViewModel extends RestfulApiViewModel {
makeSelected(cached);
return;
}
showLoading();
final body = {
"type": ProductType.topupMobile.value,
"size": 200,
"index": 0,
"brand_id": selectedBrand.value?.id ?? 0,
};
try {
final result = await client.getProducts(body);
final data = result.data ?? [];
await callApi<List<ProductModel>>(
request: () => client.getProducts(body),
onSuccess: (data, _) {
_allValue[code] = data;
products.value = result.data ?? [];
hideLoading();
products.value = data;
makeSelected(data);
} catch (error) {
print("Error fetching all products: $error");
hideLoading();
}
},
showAppNavigatorDialog: true,
);
}
}
\ 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