464 lines
15 KiB
Dart
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();
|
|
}
|
|
}
|