yumi-flutter/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart
2026-06-12 14:34:12 +08:00

485 lines
14 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/main.dart';
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/payment/google_payment_manager.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
class RoomRechargeBottomSheet extends StatefulWidget {
const RoomRechargeBottomSheet({super.key});
static const String dialogTag = 'showRoomRechargeBottomSheet';
static void show(BuildContext context) {
SmartDialog.dismiss(tag: dialogTag);
SmartDialog.show(
tag: dialogTag,
alignment: Alignment.bottomCenter,
animationType: SmartAnimationType.fade,
maskColor: Colors.black.withValues(alpha: 0.6),
clickMaskDismiss: true,
builder:
(_) => const SCRoomTopLayerSuppressor(
reason: 'room_recharge_bottom_sheet',
child: RoomRechargeBottomSheet(),
),
);
}
@override
State<RoomRechargeBottomSheet> createState() =>
_RoomRechargeBottomSheetState();
}
class _RoomRechargeBottomSheetState extends State<RoomRechargeBottomSheet> {
static const Color _sheetColor = Color(0xFF072121);
static const Color _cardColor = Color(0xFF011414);
static const Color _accentColor = Color(0xFF18F2B1);
static const String _coinAsset =
'sc_images/room/sc_room_recharge_modal_coin.png';
int _fallbackSelectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_initializeRecharge();
}
});
}
void _initializeRecharge() {
final paymentContext = navigatorKey.currentState?.context ?? context;
Provider.of<SocialChatUserProfileManager>(
paymentContext,
listen: false,
).balance();
if (!Platform.isAndroid) {
return;
}
final processor = Provider.of<AndroidPaymentProcessor>(
paymentContext,
listen: false,
);
if (processor.products.isEmpty) {
processor.initializePaymentProcessor(paymentContext);
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: ScreenUtil().screenWidth,
height: 306.w,
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: _sheetColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 24.w,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)),
),
),
),
PositionedDirectional(
top: 7.w,
start: 16.w,
child: Text(
'Your Balance Is Low. Please Add Funds.',
style: TextStyle(
color: Colors.white,
fontSize: 10.sp,
height: 1,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
Positioned(
top: 36.w,
left: 0,
right: 0,
child: Center(
child: Text(
'Payment Methods',
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
height: 1,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
),
),
),
PositionedDirectional(
top: 74.w,
start: 16.w,
end: 16.w,
child: _buildPaymentMethodRow(),
),
PositionedDirectional(
top: 126.w,
start: 16.w,
end: 16.w,
child: _buildProductOptions(),
),
PositionedDirectional(
top: 222.w,
start: 24.w,
end: 24.w,
child: _buildConfirmButton(),
),
],
),
);
}
Widget _buildPaymentMethodRow() {
return Container(
height: 40.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
decoration: BoxDecoration(
color: _cardColor,
borderRadius: BorderRadius.circular(8.w),
),
child: Row(
children: [
SizedBox(
width: 28.w,
height: 28.w,
child: Center(
child: Text(
'G',
style: TextStyle(
color: Colors.white,
fontSize: 22.sp,
height: 1,
fontWeight: FontWeight.w700,
decoration: TextDecoration.none,
),
),
),
),
SizedBox(width: 6.w),
Expanded(
child: Text(
'Google Pay',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
height: 1,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
_buildSelectedRadio(),
SizedBox(width: 7.w),
],
),
);
}
Widget _buildSelectedRadio() {
return Container(
width: 16.w,
height: 16.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.w),
),
alignment: Alignment.center,
child: Container(
width: 10.w,
height: 10.w,
decoration: const BoxDecoration(
color: _accentColor,
shape: BoxShape.circle,
),
),
);
}
Widget _buildProductOptions() {
if (!Platform.isAndroid) {
return Row(
children: [
Expanded(child: _buildFallbackProductItem(0)),
SizedBox(width: 13.w),
Expanded(child: _buildFallbackProductItem(1)),
],
);
}
return Consumer<AndroidPaymentProcessor>(
builder: (context, processor, child) {
final products = processor.products.take(2).toList();
if (products.isEmpty) {
return Row(
children: [
Expanded(child: _buildFallbackProductItem(0)),
SizedBox(width: 13.w),
Expanded(child: _buildFallbackProductItem(1)),
],
);
}
return Row(
children: [
for (int index = 0; index < products.length; index++) ...[
if (index > 0) SizedBox(width: 13.w),
Expanded(
child: _buildStoreProductItem(
processor: processor,
productConfig: products[index],
index: index,
),
),
],
if (products.length == 1) ...[
SizedBox(width: 13.w),
const Spacer(),
],
],
);
},
);
}
Widget _buildFallbackProductItem(int index) {
return _RechargeProductCard(
selected: index == _fallbackSelectedIndex,
amount: '100,000',
price: 'USD 4.99',
onTap: () => setState(() => _fallbackSelectedIndex = index),
);
}
Widget _buildStoreProductItem({
required AndroidPaymentProcessor processor,
required SelecteProductConfig productConfig,
required int index,
}) {
final product = processor.productMap[productConfig.produc.id];
return _RechargeProductCard(
selected: productConfig.isSelecte,
amount: _formatAmount((product?.obtainCandy ?? 0).round()),
price: productConfig.produc.price,
onTap: () => processor.chooseProductConfig(index),
);
}
String _formatAmount(int value) {
final sign = value < 0 ? '-' : '';
final digits = value.abs().toString();
final buffer = StringBuffer();
for (int index = 0; index < digits.length; index++) {
final remaining = digits.length - index;
buffer.write(digits[index]);
if (remaining > 1 && remaining % 3 == 1) {
buffer.write(',');
}
}
return '$sign$buffer';
}
Widget _buildConfirmButton() {
return SCDebounceWidget(
onTap: _processGooglePurchase,
child: Container(
height: 44.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _accentColor,
borderRadius: BorderRadius.circular(22.w),
),
child: Text(
'Confirm Top-Up',
style: TextStyle(
color: Colors.black,
fontSize: 20.sp,
height: 1,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
),
),
);
}
Future<void> _processGooglePurchase() async {
if (!Platform.isAndroid) {
SCTts.show('Google Play payment is only available on Android');
return;
}
final pleaseSelectMessage =
SCAppLocalizations.of(context)!.pleaseSelectaItem;
final paymentContext = navigatorKey.currentState?.context ?? context;
final processor = Provider.of<AndroidPaymentProcessor>(
paymentContext,
listen: false,
);
if (processor.products.isEmpty) {
await processor.initializePaymentProcessor(paymentContext);
if (!mounted) {
return;
}
}
final product = _selectedProduct(processor.products);
if (product == null) {
SCTts.show(pleaseSelectMessage);
return;
}
try {
await processor.recoverTransactions();
SCLoadingManager.show();
final success = await processor.iap.buyConsumable(
purchaseParam: PurchaseParam(productDetails: product.produc),
autoConsume: false,
);
if (!success) {
SCTts.show('Purchase failed, please check your network connection');
}
} catch (error) {
SCTts.show('Purchase failed: $error');
} finally {
SCLoadingManager.hide();
}
}
SelecteProductConfig? _selectedProduct(List<SelecteProductConfig> products) {
for (final product in products) {
if (product.isSelecte) {
return product;
}
}
return products.isEmpty ? null : products.first;
}
}
class _RechargeProductCard extends StatelessWidget {
const _RechargeProductCard({
required this.selected,
required this.amount,
required this.price,
required this.onTap,
});
final bool selected;
final String amount;
final String price;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SCDebounceWidget(
onTap: onTap,
child: Container(
height: 72.w,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: _RoomRechargeBottomSheetState._cardColor,
borderRadius: BorderRadius.circular(8.w),
border:
selected
? Border.all(
color: _RoomRechargeBottomSheetState._accentColor,
width: 1.w,
)
: null,
),
child: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
_RoomRechargeBottomSheetState._coinAsset,
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
),
SizedBox(width: 2.w),
Flexible(
child: Text(
amount,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
height: 1,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
),
),
],
),
],
),
),
Container(
height: 20.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(8.w),
),
),
child: Text(
price,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
height: 1,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
],
),
),
);
}
}