chatapp3-flutter/lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart
2026-04-20 18:52:03 +08:00

464 lines
15 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
enum RechargeMethodType { miFaPay, nativeStore }
class RechargeMethodOption {
const RechargeMethodOption({
required this.type,
required this.title,
this.assetIconPath,
this.iconData,
});
final RechargeMethodType type;
final String title;
final String? assetIconPath;
final IconData? iconData;
}
class RechargePackageOption {
const RechargePackageOption({
required this.id,
required this.coins,
required this.bonusCoins,
required this.priceLabel,
required this.badge,
});
final String id;
final int coins;
final int bonusCoins;
final String priceLabel;
final String badge;
}
class RechargeChannelOption {
const RechargeChannelOption({
required this.code,
required this.name,
this.typeLabel = '',
});
final String code;
final String name;
final String typeLabel;
}
class RechargeMethodBottomSheet extends StatefulWidget {
const RechargeMethodBottomSheet({
super.key,
required this.method,
required this.packages,
required this.channels,
required this.onConfirm,
this.initialPackageId,
this.initialChannelCode,
});
final RechargeMethodOption method;
final List<RechargePackageOption> packages;
final List<RechargeChannelOption> channels;
final void Function(
RechargePackageOption package,
RechargeChannelOption channel,
)
onConfirm;
final String? initialPackageId;
final String? initialChannelCode;
@override
State<RechargeMethodBottomSheet> createState() =>
_RechargeMethodBottomSheetState();
}
class _RechargeMethodBottomSheetState extends State<RechargeMethodBottomSheet> {
late String _selectedPackageId;
late String _selectedChannelCode;
@override
void initState() {
super.initState();
_selectedPackageId =
widget.packages.isEmpty
? ''
: widget.initialPackageId ?? widget.packages.first.id;
_selectedChannelCode =
widget.channels.isEmpty
? ''
: widget.initialChannelCode ?? widget.channels.first.code;
}
@override
Widget build(BuildContext context) {
final bool canConfirm =
widget.packages.isNotEmpty &&
widget.channels.isNotEmpty &&
_selectedPackageId.isNotEmpty &&
_selectedChannelCode.isNotEmpty;
return SafeArea(
top: false,
child: Container(
width: ScreenUtil().screenWidth,
height: MediaQuery.of(context).size.height * 0.72,
padding: EdgeInsets.fromLTRB(14.w, 14.w, 14.w, 14.w),
decoration: BoxDecoration(
color: const Color(0xff03523a),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(14.w),
topRight: Radius.circular(14.w),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
widget.method.title,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
SCDebounceWidget(
onTap: () => Navigator.of(context).pop(),
child: Icon(
CupertinoIcons.clear_thick_circled,
size: 22.w,
color: Colors.white.withValues(alpha: 0.86),
),
),
],
),
SizedBox(height: 6.w),
Text(
'Select amount',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.68),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.w),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.packages.isEmpty)
_buildEmptyHint('MiFaPay products are loading')
else
GridView.builder(
itemCount: widget.packages.length,
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10.w,
crossAxisSpacing: 10.w,
mainAxisExtent: 136.w,
),
itemBuilder: (BuildContext context, int index) {
final RechargePackageOption item =
widget.packages[index];
final bool isSelected = item.id == _selectedPackageId;
return _buildPackageCard(item, index, isSelected);
},
),
SizedBox(height: 16.w),
Text(
'Payment channel',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.68),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 10.w),
if (widget.channels.isEmpty)
_buildEmptyHint('No channel available')
else
Wrap(
spacing: 10.w,
runSpacing: 10.w,
children: widget.channels
.map(_buildChannelChip)
.toList(growable: false),
),
],
),
),
),
SizedBox(height: 8.w),
SCDebounceWidget(
onTap: () {
if (!canConfirm) {
return;
}
_confirmSelection();
},
child: Container(
width: double.infinity,
height: 42.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
canConfirm
? SCGlobalConfig.businessLogicStrategy
.getRechargePageButtonBackgroundColor()
: Colors.white.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(10.w),
),
child: Text(
'Confirm',
style: TextStyle(
fontSize: 15.sp,
color:
SCGlobalConfig.businessLogicStrategy
.getRechargePageButtonTextColor(),
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
);
}
void _confirmSelection() {
if (widget.packages.isEmpty || widget.channels.isEmpty) {
return;
}
final RechargePackageOption selected = widget.packages.firstWhere(
(RechargePackageOption item) => item.id == _selectedPackageId,
orElse: () => widget.packages.first,
);
final RechargeChannelOption selectedChannel = widget.channels.firstWhere(
(RechargeChannelOption item) => item.code == _selectedChannelCode,
orElse: () => widget.channels.first,
);
widget.onConfirm(selected, selectedChannel);
Navigator.of(context).pop();
}
Widget _buildPackageCard(
RechargePackageOption item,
int index,
bool isSelected,
) {
return SCDebounceWidget(
onTap: () {
setState(() {
_selectedPackageId = item.id;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
border: Border.all(
color:
isSelected ? SocialChatTheme.primaryLight : Colors.transparent,
width: 1.4,
),
),
child: Column(
children: [
SizedBox(
height: 24.w,
child:
item.badge.isEmpty
? null
: Align(
alignment: Alignment.topRight,
child: Container(
margin: EdgeInsets.only(top: 8.w, right: 8.w),
padding: EdgeInsets.symmetric(
horizontal: 7.w,
vertical: 2.w,
),
decoration: BoxDecoration(
color: SocialChatTheme.primaryLight.withValues(
alpha: 0.14,
),
borderRadius: BorderRadius.circular(999.w),
),
child: Text(
item.badge,
style: TextStyle(
fontSize: 9.sp,
color: const Color(0xff03523a),
fontWeight: FontWeight.w700,
),
),
),
),
),
Image.asset(
SCGlobalConfig.businessLogicStrategy
.getRechargePageGoldIconByIndex(index),
width: 38.w,
height: 38.w,
),
SizedBox(height: 6.w),
Padding(
padding: EdgeInsets.symmetric(horizontal: 6.w),
child: Text(
_formatWholeNumber(item.coins),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15.sp,
color: Colors.black.withValues(alpha: 0.82),
fontWeight: FontWeight.w700,
),
),
),
SizedBox(height: 2.w),
Padding(
padding: EdgeInsets.symmetric(horizontal: 6.w),
child: Text(
item.bonusCoins > 0
? '+${_formatWholeNumber(item.bonusCoins)}'
: 'No bonus',
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11.sp,
color:
item.bonusCoins > 0
? SocialChatTheme.primaryLight
: Colors.black.withValues(alpha: 0.45),
fontWeight: FontWeight.w600,
),
),
),
const Spacer(),
Container(
width: double.infinity,
height: 26.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xffF5C550),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10.w),
bottomRight: Radius.circular(10.w),
),
),
child: Text(
item.priceLabel,
style: TextStyle(
fontSize: 11.sp,
color: const Color(0xff6F4B00),
fontWeight: FontWeight.w700,
),
),
),
],
),
),
);
}
Widget _buildChannelChip(RechargeChannelOption option) {
final bool isSelected = option.code == _selectedChannelCode;
return SCDebounceWidget(
onTap: () {
setState(() {
_selectedChannelCode = option.code;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
decoration: BoxDecoration(
color:
isSelected
? Colors.white.withValues(alpha: 0.16)
: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color:
isSelected
? SocialChatTheme.primaryLight
: Colors.white.withValues(alpha: 0.14),
width: 1.1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.name,
style: TextStyle(
fontSize: 13.sp,
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
if (option.typeLabel.isNotEmpty) ...[
SizedBox(height: 2.w),
Text(
option.typeLabel,
style: TextStyle(
fontSize: 10.sp,
color: Colors.white.withValues(alpha: 0.62),
fontWeight: FontWeight.w500,
),
),
],
],
),
),
);
}
Widget _buildEmptyHint(String message) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10.w),
),
child: Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.68),
fontWeight: FontWeight.w500,
),
),
);
}
String _formatWholeNumber(int value) {
final String raw = value.toString();
final StringBuffer buffer = StringBuffer();
for (int i = 0; i < raw.length; i++) {
final int position = raw.length - i;
buffer.write(raw[i]);
if (position > 1 && position % 3 == 1) {
buffer.write(',');
}
}
return buffer.toString();
}
}