Commit 33ec1dde authored by DatHV's avatar DatHV
Browse files

update game, notify

parent 8bef97c9
class OrderItemsProductResponse {
final String? itemIds;
final String? itemExpireTime;
final String? productType;
OrderItemsProductResponse({
this.itemIds,
this.itemExpireTime,
this.productType,
});
factory OrderItemsProductResponse.fromJson(Map<String, dynamic> json) {
return OrderItemsProductResponse(
itemIds: json['item_ids'] as String?,
itemExpireTime: json['item_expire_time'] as String?,
productType: json['product_type'] as String?,
);
}
Map<String, dynamic> toJson() => {
'item_ids': itemIds,
'item_expire_time': itemExpireTime,
'product_type': productType,
};
}
import 'package:json_annotation/json_annotation.dart';
import '../../voucher/models/product_type.dart';
import 'order_items_product_payment_response_model.dart';
part 'order_product_payment_response_model.g.dart';
@JsonSerializable()
class OrderProductPaymentResponseModel {
final String? id;
@JsonKey(name: 'payment_method')
final String? paymentMethod;
@JsonKey(name: "payment_type")
final String? paymentType;
@JsonKey(name: "payment_partner")
final String? paymentPartner;
@JsonKey(name: "payment_url")
final String? paymentUrl;
@JsonKey(name: "request_id")
final String? requestId;
final List<OrderItemsProductResponse>? items;
final int? subtotal;
@JsonKey(name: "payment_transaction_id")
final String? paymentTransactionId;
@JsonKey(name: "created_at")
final String? createdAt;
OrderProductPaymentResponseModel({
this.id,
this.paymentMethod,
this.paymentType,
this.paymentPartner,
this.paymentUrl,
this.requestId,
this.items,
this.subtotal,
this.paymentTransactionId,
this.createdAt,
});
factory OrderProductPaymentResponseModel.fromJson(Map<String, dynamic> json) =>
_$OrderProductPaymentResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$OrderProductPaymentResponseModelToJson(this);
/// Custom Getter: redeemId
String? get redeemId {
final firstItemIds = items?.firstOrNull?.itemIds;
return firstItemIds?.split(',').first;
}
/// Custom Getter: expireTime
String? get expireTime {
return items?.firstOrNull?.itemExpireTime;
}
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_product_payment_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OrderProductPaymentResponseModel _$OrderProductPaymentResponseModelFromJson(
Map<String, dynamic> json,
) => OrderProductPaymentResponseModel(
id: json['id'] as String?,
paymentMethod: json['payment_method'] as String?,
paymentType: json['payment_type'] as String?,
paymentPartner: json['payment_partner'] as String?,
paymentUrl: json['payment_url'] as String?,
requestId: json['request_id'] as String?,
items:
(json['items'] as List<dynamic>?)
?.map(
(e) =>
OrderItemsProductResponse.fromJson(e as Map<String, dynamic>),
)
.toList(),
subtotal: (json['subtotal'] as num?)?.toInt(),
paymentTransactionId: json['payment_transaction_id'] as String?,
createdAt: json['created_at'] as String?,
);
Map<String, dynamic> _$OrderProductPaymentResponseModelToJson(
OrderProductPaymentResponseModel instance,
) => <String, dynamic>{
'id': instance.id,
'payment_method': instance.paymentMethod,
'payment_type': instance.paymentType,
'payment_partner': instance.paymentPartner,
'payment_url': instance.paymentUrl,
'request_id': instance.requestId,
'items': instance.items,
'subtotal': instance.subtotal,
'payment_transaction_id': instance.paymentTransactionId,
'created_at': instance.createdAt,
};
......@@ -8,9 +8,10 @@ class PaymentMethodModel {
final String? code;
final String? name;
final String? logo;
@JsonKey(name: 'save_token')
final bool? saveToken;
final bool? isSelected;
final bool? needSaveTokenWhenOrder;
bool? needSaveTokenWhenOrder;
PaymentMethodType? get type {
if (code == null) return null;
......
......@@ -12,7 +12,7 @@ PaymentMethodModel _$PaymentMethodModelFromJson(Map<String, dynamic> json) =>
code: json['code'] as String?,
name: json['name'] as String?,
logo: json['logo'] as String?,
saveToken: json['saveToken'] as bool?,
saveToken: json['save_token'] as bool?,
isSelected: json['isSelected'] as bool?,
needSaveTokenWhenOrder: json['needSaveTokenWhenOrder'] as bool?,
);
......@@ -23,7 +23,7 @@ Map<String, dynamic> _$PaymentMethodModelToJson(PaymentMethodModel instance) =>
'code': instance.code,
'name': instance.name,
'logo': instance.logo,
'saveToken': instance.saveToken,
'save_token': instance.saveToken,
'isSelected': instance.isSelected,
'needSaveTokenWhenOrder': instance.needSaveTokenWhenOrder,
};
......@@ -19,6 +19,19 @@ enum PaymentMethodType {
}
}
String get rawValue {
switch (this) {
case PaymentMethodType.card:
return 'CARD';
case PaymentMethodType.wallet:
return 'WALLET';
case PaymentMethodType.transfer:
return 'VA_QRCODE';
case PaymentMethodType.internalCard:
return 'INTERNATIONAL_CARD';
}
}
String get methodBillEVN {
switch (this) {
case PaymentMethodType.card:
......
......@@ -14,6 +14,8 @@ class PreviewOrderPaymentPointDataModel {
this.textDisplay,
});
bool get isEnableUsePoint => status == 1;
factory PreviewOrderPaymentPointDataModel.fromJson(Map<String, dynamic> json) =>
_$PreviewOrderPaymentPointDataModelFromJson(json);
Map<String, dynamic> toJson() => _$PreviewOrderPaymentPointDataModelToJson(this);
......
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/dashed_line.dart';
import '../../widgets/image_loader.dart';
import '../voucher/models/product_model.dart';
import 'model/payment_bank_account_info_model.dart';
import 'model/payment_method_model.dart';
import 'model/preview_order_payment_model.dart';
import 'model/preview_order_payment_point_data_model.dart';
import 'transaction_detail_viewmodel.dart';
class TransactionDetailScreen extends StatefulWidget {
class TransactionDetailScreen extends BaseScreen {
const TransactionDetailScreen({super.key});
@override
State<TransactionDetailScreen> createState() => _TransactionDetailScreenState();
}
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
final TransactionDetailViewModel viewModel = Get.put(TransactionDetailViewModel());
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
bool usePoints = true;
int selectedPaymentMethodIndex = -1;
bool isPaymentMethodsExpanded = true;
class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> with BasicState {
late final TransactionDetailViewModel _viewModel;
final currencyFormatter = NumberFormat.currency(locale: 'vi_VN', symbol: 'đ', decimalDigits: 0);
ProductModel? _product;
int _quantity = 1;
bool _isPaymentMethodsExpanded = true;
bool shouldDisablePaymentMethods = false;
@override
void initState() {
super.initState();
viewModel.refreshData();
final args = Get.arguments;
if (args is Map) {
_product = args['product'];
_quantity = args['quantity'] ?? 1;
}
if (_product == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel = Get.put(TransactionDetailViewModel(product: _product!, quantity: _quantity));
_viewModel.refreshData();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget build(BuildContext context) {
Widget createBody() {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: CustomAppBar.back(title: "Thông tin thanh toán"),
body: Obx(() {
if (viewModel.isLoading.value) {
if (_viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final previewData = viewModel.previewData.value;
final previewData = _viewModel.previewData.value;
if (previewData == null) {
return const Center(child: Text('Không có dữ liệu'));
}
final totalPrice = previewData.totalPrice ?? 0;
final pointValue = previewData.pointData?.point ?? 0;
final finalTotal = usePoints && previewData.pointData?.status == 1
? totalPrice - pointValue
: totalPrice;
shouldDisablePaymentMethods = _viewModel.usePoints.value && (_viewModel.finalTotal <= 0);
return Column(
children: [
Expanded(
......@@ -63,20 +75,16 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('Chi tiết giao dịch'),
_buildProductInfoSection(previewData),
if (previewData.pointData != null && previewData.pointData!.status == 1)
_buildPointToggleSection(previewData.pointData!),
_buildTotalSection(totalPrice, usePoints ? pointValue : 0, finalTotal),
if (previewData.pointData != null) _buildPointToggleSection(),
_buildTotalSection(totalPrice, _viewModel.usePoints.value ? pointValue : 0, _viewModel.finalTotal),
_buildSavedCardsSection(),
_buildPaymentMethodsSection(),
const SizedBox(height: 100), // Khoảng trống để không bị che bởi bottom bar
],
),
),
),
// Bottom bar với tổng thanh toán và nút tiếp tục
_buildBottomBar(finalTotal),
_buildBottomBar(),
],
);
}),
......@@ -84,15 +92,12 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
return Container(
color: Colors.grey.shade50,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)),
),
);
}
......@@ -103,35 +108,34 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
return Container(
color: Colors.white,
child: Column(
children: productInfo.map((item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.name ?? '',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
),
),
Text(
item.value ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('Chi tiết giao dịch'),
Column(
children:
productInfo.map((item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name ?? '', style: TextStyle(fontSize: 16, color: Colors.grey.shade700)),
Text(item.value ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
],
),
);
}).toList(),
),
],
),
);
}
Widget _buildPointToggleSection(PreviewOrderPaymentPointDataModel pointData) {
Widget _buildPointToggleSection() {
final pointData = _viewModel.previewData.value?.pointData;
if (pointData == null) {
return const SizedBox.shrink();
}
return Container(
color: Colors.white,
margin: const EdgeInsets.only(top: 8),
......@@ -141,28 +145,34 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
children: [
Row(
children: [
Center(
child: Image.asset('assets/images/ic_point.png', width: 30, height: 30),
),
Center(child: Image.asset('assets/images/ic_point.png', width: 30, height: 30)),
const SizedBox(width: 12),
Text(
pointData.textDisplay ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(pointData!.textDisplay ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
],
),
CupertinoSwitch(
value: usePoints,
activeColor: Colors.green,
onChanged: (value) {
setState(() {
usePoints = value;
});
},
),
if (pointData!.isEnableUsePoint)
Switch(
activeColor: Colors.white,
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey.shade400,
value: _viewModel.usePoints.value,
onChanged: (value) {
setState(() {
_viewModel.usePoints.value = value;
final total = _viewModel.previewData.value?.totalPrice ?? 0;
final point = pointData!.point ?? 0;
shouldDisablePaymentMethods = value && (total - point <= 0);
if (!value && _viewModel.selectedPaymentMethodIndex < 0) {
if (_viewModel.paymentBankAccounts.value.isNotEmpty) {
_viewModel.selectedPaymentMethodIndex = _viewModel.definedCodeIndexBankAccount;
} else if (_viewModel.paymentMethods.value.isNotEmpty) {
_viewModel.selectedPaymentMethodIndex = 0;
}
}
});
},
),
],
),
);
......@@ -176,9 +186,8 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
child: Column(
children: [
_buildTotalRow('Tổng số tiền', totalPrice, false),
if (pointsUsed > 0)
_buildTotalRow('Sử dụng điểm', -pointsUsed, false),
_buildTotalRow('Tổng tạm tính', finalTotal, true),
_buildTotalRow('Sử dụng điểm', -pointsUsed, false),
_buildTotalRow('Tổng tạm tính', finalTotal, false),
],
),
);
......@@ -193,13 +202,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: isHighlighted ? Colors.black87 : Colors.grey.shade700,
),
),
Text(label, style: TextStyle(fontSize: 16, color: isHighlighted ? Colors.black87 : Colors.grey.shade700)),
Text(
displayAmount,
style: TextStyle(
......@@ -215,20 +218,26 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
Widget _buildSavedCardsSection() {
return Obx(() {
final bankAccounts = viewModel.paymentBankAccounts;
final bankAccounts = _viewModel.paymentBankAccounts;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('Tài khoản/Thẻ đã lưu'),
Container(
color: Colors.white,
child: bankAccounts.isEmpty
? _buildNoSavedCardsItem()
: Column(
children: List.generate(
bankAccounts.length,
(index) => _buildSavedCardItem(bankAccounts[index], index),
IgnorePointer(
ignoring: shouldDisablePaymentMethods,
child: Opacity(
opacity: shouldDisablePaymentMethods ? 0.6 : 1.0,
child: Container(
color: Colors.white,
child:
bankAccounts.isEmpty
? _buildNoSavedCardsItem()
: Row(
children: List.generate(
bankAccounts.length,
(index) => _buildSavedCardItem(bankAccounts[index], index),
),
),
),
),
),
......@@ -245,90 +254,60 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: Icon(
Icons.credit_card,
color: Colors.grey,
),
),
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(4)),
child: const Center(child: Icon(Icons.credit_card, color: Colors.grey)),
),
const SizedBox(width: 12),
const Text(
'Không có thẻ đã lưu',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const Text('Không có thẻ đã lưu', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
],
),
);
}
Widget _buildSavedCardItem(PaymentBankAccountInfoModel account, int index) {
final isSelected = selectedPaymentMethodIndex == -1000 - index;
final isSelected = _viewModel.selectedPaymentMethodIndex == _viewModel.definedCodeIndexBankAccount + index;
return InkWell(
onTap: () {
setState(() {
selectedPaymentMethodIndex = -1000 - index; // tránh đụng index method
_viewModel.selectedPaymentMethodIndex =
_viewModel.definedCodeIndexBankAccount + index; // tránh đụng index method
});
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isSelected ? Colors.red : Colors.grey.shade300,
width: 1.5,
child: IntrinsicWidth(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 0, top: 8, bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: isSelected ? Colors.red : Colors.grey.shade300, width: 1.5),
borderRadius: BorderRadius.circular(8),
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
if (account.bankLogo != null)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: account.bankLogo!,
width: 32,
height: 32,
placeholder: (context, url) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
),
errorWidget: (context, url, error) => Container(
child: Row(
children: [
if (account.bankLogo != null)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: loadNetworkImage(
url: account.bankLogo,
width: 32,
height: 32,
color: Colors.grey.shade200,
child: const Icon(Icons.error, size: 16),
placeholderAsset: 'assets/images/ic_logo.png',
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(account.bankShortName ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
if (account.cardNumber != null)
Text(
'**** ${account.cardNumber!.substring(account.cardNumber!.length - 4)}',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
account.bankName ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
if (account.cardNumber != null)
Text(
'**** ${account.cardNumber!.substring(account.cardNumber!.length - 4)}',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
],
],
),
),
),
);
......@@ -336,8 +315,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
Widget _buildPaymentMethodsSection() {
return Obx(() {
final methods = viewModel.paymentMethods;
final methods = _viewModel.paymentMethods;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......@@ -346,35 +324,37 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Phương thức thanh toán khác',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
Padding(
padding: const EdgeInsets.all(8.0),
child: const Text(
'Phương thức thanh toán khác',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
),
IconButton(
icon: Icon(
isPaymentMethodsExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
_isPaymentMethodsExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
color: Colors.grey,
),
onPressed: () {
setState(() {
isPaymentMethodsExpanded = !isPaymentMethodsExpanded;
_isPaymentMethodsExpanded = !_isPaymentMethodsExpanded;
});
},
),
],
),
),
if (isPaymentMethodsExpanded)
Container(
color: Colors.white,
child: Column(
children: List.generate(
methods.length,
(index) => _buildPaymentMethodItem(methods[index], index),
if (_isPaymentMethodsExpanded)
IgnorePointer(
ignoring: shouldDisablePaymentMethods,
child: Opacity(
opacity: shouldDisablePaymentMethods ? 0.6 : 1.0,
child: Container(
color: Colors.white,
child: Column(
children: List.generate(methods.length, (index) => _buildPaymentMethodItem(methods[index], index)),
),
),
),
),
......@@ -384,152 +364,120 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
}
Widget _buildPaymentMethodItem(PaymentMethodModel method, int index) {
final isSelected = selectedPaymentMethodIndex == index;
return InkWell(
onTap: () {
setState(() {
selectedPaymentMethodIndex = index;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
border: index < viewModel.paymentMethods.length - 1
? Border(bottom: BorderSide(color: Colors.grey.shade200, width: 0.5))
: null,
),
child: Row(
children: [
Radio<int>(
value: index,
groupValue: selectedPaymentMethodIndex,
onChanged: (value) {
setState(() {
selectedPaymentMethodIndex = value!;
});
},
activeColor: Colors.red,
),
if (method.logo != null && method.logo!.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: method.logo!,
width: 32,
height: 32,
placeholder: (context, url) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
),
errorWidget: (context, url, error) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
child: const Icon(Icons.error, size: 16),
final isSelected = _viewModel.selectedPaymentMethodIndex == index;
final canSaveToken = method.saveToken == true;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RadioListTile<int>(
activeColor: BaseColor.primary400,
value: index,
groupValue: _viewModel.selectedPaymentMethodIndex,
onChanged: (val) {
setState(() {
_viewModel.selectedPaymentMethodIndex = val!;
});
},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Row(
children: [
if (method.logo != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Image.network(
method.logo!,
width: 24,
height: 24,
errorBuilder: (_, __, ___) => const Icon(Icons.payment),
),
),
),
const SizedBox(width: 12),
Text(
method.name ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
Text(method.name ?? '', style: const TextStyle(fontSize: 16)),
],
),
),
),
if (isSelected && canSaveToken)
Padding(
padding: const EdgeInsets.only(left: 48, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Lưu lại thanh toán sau", style: TextStyle(fontSize: 14, color: Colors.black87)),
Switch(
activeColor: Colors.white,
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey.shade400,
value: method.needSaveTokenWhenOrder ?? true,
onChanged: (val) {
setState(() {
method.needSaveTokenWhenOrder = val;
});
},
),
],
),
),
Container(height: 1, color: Colors.grey.shade200, margin: const EdgeInsets.symmetric(horizontal: 16)),
],
);
}
Widget _buildBottomBar(int finalTotal) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Column(
children: [
if ((viewModel.previewData.value?.feeNote ?? "").isNotEmpty)
Text(
viewModel.previewData.value?.feeNote ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
Widget _buildBottomBar() {
return SafeArea(
top: false,
minimum: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -5))],
),
child: Column(
children: [
if ((_viewModel.previewData.value?.feeNote ?? "").isNotEmpty)
Text(
_viewModel.previewData.value?.feeNote ?? '',
style: TextStyle(fontSize: 12, color: Colors.orange.shade700),
),
),
const SizedBox(height: 12),
Container(
height: 1,
color: Colors.grey.shade200,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Tổng thanh toán',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
currencyFormatter.format(finalTotal),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
const SizedBox(height: 12),
Container(height: 1, color: Colors.grey.shade200),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Tổng thanh toán', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 4),
Text(
currencyFormatter.format(_viewModel.finalTotal),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
),
],
),
),
ElevatedButton(
onPressed: selectedPaymentMethodIndex >= 0 ? () {
// Xử lý khi nhấn nút tiếp tục
final selectedMethod = viewModel.paymentMethods[selectedPaymentMethodIndex];
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Đang xử lý thanh toán với ${selectedMethod.name}...')),
);
} : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
],
),
disabledBackgroundColor: Colors.red.withOpacity(0.6),
disabledForegroundColor: Colors.white.withOpacity(0.8),
),
child: const Text(
'Tiếp tục',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
ElevatedButton(
onPressed:
_viewModel.finalTotal > 0 && _viewModel.selectedPaymentMethodIndex < 0
? null : _viewModel.requestPaymentProduct,
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
disabledBackgroundColor: Colors.red.withOpacity(0.6),
disabledForegroundColor: Colors.white.withOpacity(0.8),
),
child: const Text('Tiếp tục', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
),
const SizedBox(height: 44),
],
],
),
// const SizedBox(height: 44),
],
),
),
);
}
}
\ No newline at end of file
}
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:uuid/uuid.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import '../../preference/data_preference.dart';
import '../voucher/models/product_model.dart';
import '../webview/payment_web_view_screen.dart';
import '../webview/web_view_screen.dart';
import 'model/payment_bank_account_info_model.dart';
import 'model/payment_method_model.dart';
import 'model/preview_order_payment_model.dart';
class TransactionDetailViewModel extends RestfulApiViewModel {
final int definedCodeIndexBankAccount = 1000;
var previewData = Rxn<PreviewOrderPaymentModel>();
var paymentMethods = RxList<PaymentMethodModel>();
var paymentBankAccounts = RxList<PaymentBankAccountInfoModel>();
final RxBool isLoading = false.obs;
final ProductModel product;
final int quantity;
final RxBool usePoints = true.obs;
var selectedPaymentMethodIndex = -1.obs;
void Function(String message)? onShowAlertError;
int get finalTotal {
final totalPrice = previewData.value?.totalPrice ?? 0;
final pointValue = previewData.value?.pointData?.point ?? 0;
final finalTotal = usePoints.value && previewData.value?.pointData?.status == 1
? totalPrice - pointValue
: totalPrice;
return finalTotal;
}
TransactionDetailViewModel({required this.product, required this.quantity});
@override
void onInit() {
......@@ -20,23 +45,80 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([
_getPreviewOrderPayment(),
_getPaymentMethods(),
_getPaymentBankAccounts(),
]);
await Future.wait([_getPreviewOrderPayment(), _getPaymentMethods(), _getPaymentBankAccounts()]);
isLoading.value = false;
}
requestPaymentProduct() {
showLoading();
final requestId = Uuid().v4();
int? point = usePoints.value ? previewData.value?.pointData?.point : 0;
PaymentBankAccountInfoModel? selectedBankAccount;
PaymentMethodModel? selectedPaymentMethod;
bool? saveToken;
if (finalTotal == 0) {
point = previewData.value?.pointData?.point ?? 0;
} else if (selectedPaymentMethodIndex >= definedCodeIndexBankAccount) {
selectedBankAccount = paymentBankAccounts.value[selectedPaymentMethodIndex - definedCodeIndexBankAccount];
} else if (selectedPaymentMethodIndex >= 0) {
selectedPaymentMethod = paymentMethods.value[selectedPaymentMethodIndex];
saveToken = selectedPaymentMethod?.saveToken == true && selectedPaymentMethod?.needSaveTokenWhenOrder == true;
}
client
.orderSubmitPayment(
products: [product],
quantity: quantity,
requestId: requestId,
point: point,
cash: finalTotal,
method: selectedPaymentMethod,
paymentTokenId: selectedBankAccount?.id,
saveToken: saveToken,
metadata: "",
)
.then((value) {
hideLoading();
if (value.isSuccess) {
final data = value.data;
if ((data?.paymentUrl ?? "").isNotEmpty) {
Get.toNamed(
paymentWebViewScreen,
arguments: PaymentWebViewInput(
url: data?.paymentUrl ?? "",
isContract: false,
orderId: data?.id ?? "",
showAlertBack: true,
callback: (result) {},
)
);
} else if ((data?.redeemId ?? "").isNotEmpty && (data?.id ?? "").isNotEmpty) {
Get.offNamed(transactionHistoryDetailScreen,
arguments: {
"orderId": data?.id ?? "",
"canBack": true,
}
);
} else {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
} else {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
});
}
Future<void> _getPreviewOrderPayment() async {
String? token = DataPreference.instance.token ?? "";
try {
final body = {
"product_id": 13796,
"quantity": 1,
"product_id": product.id,
"quantity": quantity,
"price": product.amountToBePaid ?? 0,
"access_token": token,
"price": 100000,
};
if (product.previewFlashSale?.isFlashSalePrice == true && product.previewFlashSale?.id != null) {
body["flash_sale_id"] = product.previewFlashSale!.id;
}
final response = await client.getPreviewOrderInfo(body);
previewData.value = response.data;
} catch (error) {
......@@ -61,4 +143,4 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
print("Error fetching payment bank accounts: $error");
}
}
}
\ No newline at end of file
}
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_type.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../preference/point/point_manager.dart';
import '../../../resouce/base_color.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/alert/data_alert_model.dart';
import '../../../widgets/back_button.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_point_text_tag.dart';
......@@ -181,12 +185,8 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
const SizedBox(width: 8),
Expanded(child: Text(product.brand?.name ?? '', style: const TextStyle(fontSize: 14))),
// PriceTagWidget(point: product.amountToBePaid ?? 0),
CustomPointText(
point: product.amountToBePaid ?? 0,
type: product.price?.method,
),
CustomPointText(point: product.amountToBePaid ?? 0, type: product.price?.method),
],
),
],
),
......@@ -203,8 +203,10 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
if (hasExpire)
Text('Hạn dùng: ', style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 12)),
if (hasExpire)
Text(product.expired ? "Hết hạn" : product.expire,
style: const TextStyle(color: BaseColor.primary500, fontWeight: FontWeight.bold, fontSize: 12)),
Text(
product.expired ? "Hết hạn" : product.expire,
style: const TextStyle(color: BaseColor.primary500, fontWeight: FontWeight.bold, fontSize: 12),
),
if (isOutOfStock)
Container(
margin: const EdgeInsets.only(left: 8),
......@@ -219,14 +221,6 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
);
}
Widget _buildDetailBlock(ProductModel product) {
return _buildTextBlock("Chi tiết ưu đãi:", product.content?.detail);
}
Widget _buildConditionBlock(ProductModel product) {
return _buildTextBlock("Điều kiện áp dụng:", product.content?.termAndCondition);
}
Widget _buildTextBlock(String title, String? content) {
if (content == null || content.isEmpty) return const SizedBox();
return Container(
......@@ -379,8 +373,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
height: 48,
child: ElevatedButton(
onPressed: () {
Get.toNamed(registerFormInputScreen, arguments: {"id": 13484});
// TODO: Handle đổi ưu đãi
_handleContinueButtonAction();
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
......@@ -399,48 +392,49 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
return _buildBottomActionContainer(
child: Row(
children: [
Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(50),
),
child: IconButton(
icon: const Icon(Icons.remove, color: Colors.black),
onPressed: () {
if (_viewModel.quantity.value > 1) {
_viewModel.quantity.value--;
}
},
),
),
const SizedBox(width: 12),
Obx(() => Text('${_viewModel.quantity.value}', style: const TextStyle(fontSize: 16))),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: BaseColor.primary500,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(50),
if (_viewModel.product.value?.productType == ProductType.voucher)
Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(50),
),
child: IconButton(
icon: const Icon(Icons.remove, color: Colors.black),
onPressed: () {
if (_viewModel.quantity.value > 1) {
_viewModel.quantity.value--;
}
},
),
),
child: IconButton(
icon: const Icon(Icons.add, color: Colors.white),
onPressed: () {
_viewModel.quantity.value++;
},
const SizedBox(width: 12),
Obx(() => Text('${_viewModel.quantity.value}', style: const TextStyle(fontSize: 16))),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: BaseColor.primary500,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(50),
),
child: IconButton(
icon: const Icon(Icons.add, color: Colors.white),
onPressed: () {
_viewModel.quantity.value++;
},
),
),
),
],
),
],
),
const SizedBox(width: 36),
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: () {
_viewModel.verifyOrderProduct();
_handleContinueButtonAction();
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
......@@ -458,6 +452,50 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
);
}
_handleContinueButtonAction() {
final product = _viewModel.product.value;
if (product?.requireFormRegis == true) {
Get.toNamed(registerFormInputScreen, arguments: {"product": product});
return;
}
_viewModel.verifyOrderProduct(() {
if (product?.price?.method == CashType.point) {
_handleRedeemProduct();
} else {
Get.toNamed(transactionDetailScreen, arguments: {"product": product, "quantity": _viewModel.quantity.value});
}
});
}
_handleRedeemProduct() {
final amountToBePaid = _viewModel.product.value?.amountToBePaid ?? 0;
if (UserPointManager().point < amountToBePaid) {
showAlertError(content: "Bạn không đủ điểm để đổi ưu đãi này");
return;
}
final dataAlert = DataAlertModel(
title: "Xác nhận",
description: "Bạn có muốn sử dụng <b style=\"color:#E71D28\"> ${amountToBePaid.money(CurrencyUnit.point)}</b> MyPoint để đổi Ưu Đãi này không?",
localHeaderImage: "assets/images/ic_pipi_02.png",
buttons: [AlertButton(
text: "Đồng ý",
onPressed: () {
Get.back();
_viewModel.redeemProduct();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(
text: "Huỷ",
onPressed: () => Get.back(),
bgColor: Colors.white,
textColor: BaseColor.second500,
),],
);
showAlert(data: dataAlert);
}
Widget _buildFavoriteButton() {
return Obx(() {
final isFavorite = _viewModel.liked.value;
......
import 'dart:ui';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:uuid/uuid.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../../../configs/constants.dart';
import '../../../shared/router_gage.dart';
......@@ -38,7 +41,6 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
}
} catch (error) {
onShowAlertError?.call("Error toggling favorite: $error");
print("Error toggling favorite: $error");
}
}
......@@ -51,7 +53,6 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
liked.value = product.value?.liked == true;
} catch (error) {
onShowAlertError?.call("Error fetching product detail: $error");
print("Error fetching product detail: $error");
} finally {
isLoading.value = false;
}
......@@ -67,13 +68,9 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
} finally {}
}
verifyOrderProduct() async {
verifyOrderProduct(Function verified) async {
final value = product.value;
var body = {
"product_id": productId,
"price": value?.amountToBePaid,
"quantity": quantity.value,
};
var body = {"product_id": productId, "price": value?.amountToBePaid, "quantity": quantity.value};
if (value?.previewFlashSale?.isFlashSalePrice == true) {
final flashSaleId = value?.previewFlashSale?.id;
if (flashSaleId != null) {
......@@ -86,9 +83,29 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
} else {
Get.toNamed(transactionDetailScreen);
// onShowAlertError?.call("Verify Order Product Success -> Go To Payment Detail");
verified.call();
}
});
}
redeemProduct() {
showLoading();
final requestId = Uuid().v4();
client
.orderSubmitPayment(
products: [product.value!],
quantity: 1,
requestId: requestId,
point: product.value?.amountToBePaid ?? 0,
)
.then((value) {
hideLoading();
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
} else {
// Success -> go to transaction detail screen
onShowAlertError?.call("Redeem success -> go to transaction detail screen");
}
});
}
}
......@@ -9,6 +9,7 @@ import 'package:mypoint_flutter_app/screen/voucher/models/product_item_model.dar
import 'package:mypoint_flutter_app/screen/voucher/models/product_media_item.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_price_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_properties_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_type.dart';
import '../../flash_sale/preview_flash_sale_model.dart';
import 'media_type.dart';
import 'my_product_status_type.dart';
......@@ -35,6 +36,9 @@ class ProductModel {
final ProductItemModel? itemModel;
@JsonKey(name: 'expire_time')
final String? expireTime;
@JsonKey(name: 'require_form_regis')
final bool? requireFormRegis;
final String? type;
ProductModel({
this.id,
......@@ -49,12 +53,18 @@ class ProductModel {
this.customerInfoModel,
this.itemModel,
this.expireTime,
this.requireFormRegis,
this.type,
});
String? get name {
return content?.name;
}
ProductType get productType {
return ProductTypeExt.from(type) ?? ProductType.voucher;
}
String get expire {
final ex = (isMyProduct ? itemModel?.expireTime : expireTime) ?? "";
return ex.toDate()?.toFormattedString() ?? "";
......
......@@ -53,6 +53,8 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
json['product_item'] as Map<String, dynamic>,
),
expireTime: json['expire_time'] as String?,
requireFormRegis: json['require_form_regis'] as bool?,
type: json['type'] as String?,
);
Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
......@@ -69,4 +71,6 @@ Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
'customer_product_info': instance.customerInfoModel,
'product_item': instance.itemModel,
'expire_time': instance.expireTime,
'require_form_regis': instance.requireFormRegis,
'type': instance.type,
};
......@@ -4,6 +4,7 @@ import '../../../shared/router_gage.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/custom_search_navigation_bar.dart';
import '../../transaction/history/transaction_history_detail_screen.dart';
import '../sub_widget/voucher_item_list.dart';
import 'voucher_list_viewmodel.dart';
......@@ -82,7 +83,12 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
final product = _viewModel.products[index];
return GestureDetector(
onTap: () {
Get.toNamed(voucherDetailScreen, arguments: product.id);
// // TODO
Get.toNamed(transactionHistoryDetailScreen, arguments: {
'orderId': "02744757-a5ef-420d-a737-c0bc93d767b7",
'canBack': true,
});
// Get.toNamed(voucherDetailScreen, arguments: product.id);
},
child: VoucherListItem(product: product),
);
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/back_button.dart';
enum PaymentProcess {
begin,
processing,
success,
failure;
String get title {
switch (this) {
case PaymentProcess.begin:
return 'Bắt đầu thanh toán';
case PaymentProcess.processing:
return 'Đang xử lý thanh toán';
case PaymentProcess.success:
return 'Thanh toán thành công';
case PaymentProcess.failure:
return 'Thanh toán thất bại';
}
}
String get content {
switch (this) {
case PaymentProcess.begin:
return 'Vui lòng tiến hành thanh toán.';
case PaymentProcess.processing:
return 'Hệ thống đang xử lý giao dịch của bạn.';
case PaymentProcess.success:
return 'Giao dịch của bạn đã hoàn tất.';
case PaymentProcess.failure:
return 'Giao dịch thất bại. Vui lòng thử lại.';
}
}
}
class PaymentWebViewInput {
final String url;
final String orderId;
final bool isContract;
final bool showAlertBack;
final Function(PaymentProcess result)? callback;
PaymentWebViewInput({
required this.url,
required this.orderId,
this.isContract = false,
this.showAlertBack = true,
this.callback,
});
}
class PaymentWebViewScreen extends BaseScreen {
const PaymentWebViewScreen({super.key});
@override
State<PaymentWebViewScreen> createState() => _PaymentWebViewScreenState();
}
class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with BasicState {
late final PaymentWebViewInput input;
late final WebViewController _controller;
bool _isLoading = true;
final List<String> paymentSuccessUrls = [
"https://localhost/paymentGatewayRequestSuccessful",
"mypointapp://open?click_action_type=PAYMENT_SUCCESS",
"https://localhost/paymentGatewayAutoDebitRequestSuccessful",
];
final List<String> paymentFailedUrls = [
"https://localhost/paymentGatewayRequestFailed",
"mypointapp://open?click_action_type=PAYMENT_FAIL",
"https://localhost/paymentGatewayAutoDebitRequestFailed",
];
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is! PaymentWebViewInput) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
input = args;
_controller =
WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (_) {
setState(() {
_isLoading = true;
});
},
onPageFinished: (_) {
setState(() {
_isLoading = false;
});
},
onNavigationRequest: _handleNavigation,
),
)
..loadRequest(Uri.parse(input.url));
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomAppBar(
title: "Thanh toán",
leftButtons: [
CustomBackButton(
onPressed: () async {
if (input.showAlertBack) {
_onBackPressed();
return;
}
Get.back();
},
),
],
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading) const Center(child: CircularProgressIndicator()),
],
),
);
}
NavigationDecision _handleNavigation(NavigationRequest request) {
final url = request.url;
debugPrint("➡️ Navigating: $url");
if (paymentSuccessUrls.any((success) => url.startsWith(success))) {
_onPaymentResult(PaymentProcess.success);
return NavigationDecision.prevent;
}
if (paymentFailedUrls.any((fail) => url.startsWith(fail))) {
_onPaymentResult(PaymentProcess.failure);
return NavigationDecision.prevent;
}
// Mở app Zalo nếu redirect đến scheme của nó
final uri = Uri.tryParse(url);
final zaloSchemes = ["zalo", "zalopay", "zalopay.api.v2"];
if (uri != null && zaloSchemes.contains(uri.scheme)) {
launchUrl(uri, mode: LaunchMode.externalApplication);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
void _onPaymentResult(PaymentProcess result) {
if (input.isContract) {
_navigateToContractScreen();
} else if (input.callback != null) {
input.callback!(result);
Get.back(); // hoặc điều hướng phù hợp
} else {
_backToRoot();
}
}
void _backToRoot() {
Get.until((route) => route.isFirst);
}
void _navigateToContractScreen() {
Get.snackbar('Thông báo', 'Đi tới danh sách hợp đồng điện'); // placeholder
}
_onBackPressed() {
final dataAlert = DataAlertModel(
title: "Huỷ đơn hàng?",
description: "Bạn có chắc muốn huỷ thanh toán đơn hàng này?",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
Get.offNamed(transactionHistoryDetailScreen, arguments: {"orderId": input.orderId ?? "", "canBack": false});
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(
text: "Huỷ",
onPressed: () => Navigator.pop(context, false),
bgColor: Colors.white,
textColor: BaseColor.second500,
),
],
);
showAlert(data: dataAlert);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
class BaseWebViewInput {
final String? title;
final String url;
final bool isFullScreen;
const BaseWebViewInput({
this.title,
required this.url,
this.isFullScreen = false,
});
}
class BaseWebViewScreen extends StatefulWidget {
const BaseWebViewScreen({super.key});
@override
State<BaseWebViewScreen> createState() => _BaseWebViewScreenState();
}
class _BaseWebViewScreenState extends State<BaseWebViewScreen> {
late final BaseWebViewInput input;
late final WebViewController _controller;
String? _dynamicTitle;
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is BaseWebViewInput) {
input = args;
} else {
throw Exception('BaseWebViewInput is required in arguments');
}
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) async {
final title = await _controller.getTitle();
setState(() {
_dynamicTitle = title;
});
},
onWebResourceError: (_) {
_showWebErrorDialog();
},
),
)
..loadRequest(Uri.parse(input.url));
_clearCookies();
}
Future<void> _clearCookies() async {
final cookieManager = WebViewCookieManager();
await cookieManager.clearCookies();
}
void _showWebErrorDialog() {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Lỗi"),
content: const Text("Không thể tải nội dung. Vui lòng thử lại sau."),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Đóng"),
)
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: input.isFullScreen
? null
: AppBar(
title: Text(
input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
style: const TextStyle(fontSize: 16),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _handleBack,
),
),
body: SafeArea(
child: WebViewWidget(controller: _controller),
),
);
}
void _handleBack() async {
if (await _controller.canGoBack()) {
_controller.goBack();
} else {
if (context.mounted) Navigator.of(context).pop();
}
}
}
\ No newline at end of file
......@@ -2,13 +2,18 @@ import 'package:get/get_navigation/src/routes/get_route.dart';
import '../screen/game/game_cards/game_card_screen.dart';
import '../screen/login/login_screen.dart';
import '../screen/main_tab_screen/main_tab_screen.dart';
import '../screen/notification/notification_screen.dart';
import '../screen/onboarding/onboarding_screen.dart';
import '../screen/register_campaign/register_form_input_screen.dart';
import '../screen/setting/setting_screen.dart';
import '../screen/splash/splash_screen.dart';
import '../screen/support/support_screen.dart';
import '../screen/transaction/history/transaction_history_detail_screen.dart';
import '../screen/transaction/transaction_detail_screen.dart';
import '../screen/voucher/detail/voucher_detail_screen.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart';
import '../screen/webview/payment_web_view_screen.dart';
import '../screen/webview/web_view_screen.dart';
const splashScreen = '/splash';
const onboardingScreen = '/onboarding';
......@@ -20,6 +25,11 @@ const voucherDetailScreen = '/voucherDetail';
const gameCardScreen = '/gameCardScreen';
const registerFormInputScreen = '/registerFormInputScreen';
const transactionDetailScreen = '/transactionDetailScreen';
const baseWebViewScreen = '/baseWebViewScreen';
const paymentWebViewScreen = '/paymentWebViewScreen';
const transactionHistoryDetailScreen = '/transactionHistoryDetailScreen';
const supportScreen = '/supportScreen';
const notificationScreen = '/notificationScreen';
class RouterPage {
static List<GetPage> pages() {
......@@ -40,6 +50,11 @@ class RouterPage {
GetPage(name: gameCardScreen, page: () => GameCardScreen(),),
GetPage(name: registerFormInputScreen, page: () => RegisterFormInputScreen(),),
GetPage(name: transactionDetailScreen, page: () => TransactionDetailScreen(),),
GetPage(name: baseWebViewScreen, page: () => BaseWebViewScreen(),),
GetPage(name: paymentWebViewScreen, page: () => PaymentWebViewScreen(),),
GetPage(name: transactionHistoryDetailScreen, page: () => TransactionHistoryDetailScreen(),),
GetPage(name: supportScreen, page: () => SupportScreen(),),
GetPage(name: notificationScreen, page: () => NotificationScreen(),),
];
}
}
}
\ No newline at end of file
......@@ -34,7 +34,7 @@ class CustomAlertDialog extends StatelessWidget {
_buildHeaderImage(),
const SizedBox(height: 2),
// Title
if (alertData.title != null)
if ((alertData.title ?? "").isNotEmpty)
Text(
alertData.title!,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
......@@ -48,7 +48,7 @@ class CustomAlertDialog extends StatelessWidget {
</div>
'''),
const SizedBox(height: 4),
if (alertData.content != null)
if ((alertData.content ?? "").isNotEmpty)
Text(
alertData.content!,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: BaseColor.primary500),
......
......@@ -48,6 +48,7 @@ dependencies:
local_auth:
pin_code_fields:
intl: ^0.18.1
webview_flutter: ^4.2.2
game_miniapp:
path: ../mini_app/game_miniapp
dev_dependencies:
......
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