485 lines
14 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|