import 'dart:io'; import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/payment/google_payment_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; enum FirstRechargeRewardStyle { coin, prop } class FirstRechargeReward { const FirstRechargeReward({ required this.title, required this.style, this.badgeText = '', this.cover = '', }); final String title; final FirstRechargeRewardStyle style; final String badgeText; final String cover; bool get isCoinReward => style == FirstRechargeRewardStyle.coin; } class FirstRechargePackage { const FirstRechargePackage({ required this.priceText, required this.rewards, this.discountText = '', this.totalValueText = '', this.rechargeAmountCents = 0, this.productPackage = '', }); final String priceText; final String discountText; final String totalValueText; final int rechargeAmountCents; final String productPackage; final List rewards; } class FirstRechargeDialogData { const FirstRechargeDialogData({ required this.packages, this.timeRemainingText = '', this.initialPackageIndex = 0, }); final List packages; final String timeRemainingText; final int initialPackageIndex; factory FirstRechargeDialogData.mock() { return const FirstRechargeDialogData( timeRemainingText: 'Time remaining: 6 D 59 H 59 M 59S', packages: [ FirstRechargePackage( priceText: r'$0.99', discountText: '70%', totalValueText: 'Total value of gifts: xxx gold coins', rewards: [ FirstRechargeReward( title: 'NameName', badgeText: '80000', cover: _FirstRechargeAssets.rewardAvatarFrame, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'NameName', badgeText: '240000', cover: _FirstRechargeAssets.rewardAvatarFrame, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: '240,000 Coins', cover: _FirstRechargeAssets.rewardCoinStack, style: FirstRechargeRewardStyle.coin, ), FirstRechargeReward( title: 'Vehicle', badgeText: '3d', cover: _FirstRechargeAssets.rewardVehicle, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'Frame', badgeText: '3d', cover: _FirstRechargeAssets.rewardFrame, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'Gift', badgeText: 'x1', cover: _FirstRechargeAssets.rewardGift, style: FirstRechargeRewardStyle.prop, ), ], ), FirstRechargePackage( priceText: r'$2.99', discountText: '60%', totalValueText: 'Total value of gifts: 720,000 gold coins', rewards: [ FirstRechargeReward( title: 'Avatar Frame', badgeText: '7d', cover: _FirstRechargeAssets.rewardAvatarFrame, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'Room Frame', badgeText: '7d', cover: _FirstRechargeAssets.rewardFrame, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: '720,000 Coins', cover: _FirstRechargeAssets.rewardCoinStack, style: FirstRechargeRewardStyle.coin, ), FirstRechargeReward( title: 'Vehicle', badgeText: '7d', cover: _FirstRechargeAssets.rewardVehicle, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'Gift', badgeText: 'x3', cover: _FirstRechargeAssets.rewardGift, style: FirstRechargeRewardStyle.prop, ), FirstRechargeReward( title: 'Profile Frame', badgeText: '7d', cover: _FirstRechargeAssets.rewardAvatarFrame, style: FirstRechargeRewardStyle.prop, ), ], ), ], ); } factory FirstRechargeDialogData.fromRewardHome( SCFirstRechargeRewardHomeRes home, ) { final levels = home.availableLevelConfigs.take(2).toList(); final packages = []; var selectedPackageIndex = 0; for (final level in levels) { final rewards = level.rewardItems.where(_isDisplayableRewardItem).map((item) { return FirstRechargeReward( title: _rewardTitle(item), style: item.isCoinReward ? FirstRechargeRewardStyle.coin : FirstRechargeRewardStyle.prop, badgeText: item.displayBadgeText, cover: item.cover, ); }).toList(); if (rewards.isEmpty) { continue; } if (level.current) { selectedPackageIndex = packages.length; } packages.add( FirstRechargePackage( priceText: _formatPriceText(level), discountText: level.discountText, totalValueText: level.totalValueText, rechargeAmountCents: level.rechargeAmountCents, productPackage: level.productPackage, rewards: rewards, ), ); } return FirstRechargeDialogData( packages: packages, initialPackageIndex: selectedPackageIndex, ); } static bool _isDisplayableRewardItem(SCFirstRechargeRewardItem item) { if (item.isCoinReward) { return item.coinAmount > 0; } return item.displayTitle.trim().isNotEmpty || item.cover.trim().isNotEmpty; } static String _rewardTitle(SCFirstRechargeRewardItem item) { if (item.isCoinReward) { return item.displayTitle; } return item.displayTitle; } static String _formatPriceText(SCFirstRechargeLevelConfig level) { final amount = level.rechargeAmount.trim(); if (amount.isNotEmpty) { return amount.startsWith(r'$') ? amount : '\$$amount'; } if (level.rechargeAmountCents > 0) { return '\$${(level.rechargeAmountCents / 100).toStringAsFixed(2)}'; } return r'$0.00'; } } class FirstRechargeDialog extends StatefulWidget { const FirstRechargeDialog({super.key, required this.data, this.onBuyNow}); static const String dialogTag = 'showFirstRechargeDialog'; static const double _designWidth = 340; static const double _designHeight = 528; final FirstRechargeDialogData data; final ValueChanged? onBuyNow; static Future show( BuildContext context, { required FirstRechargeDialogData data, ValueChanged? onBuyNow, }) async { SmartDialog.dismiss(tag: dialogTag); await SmartDialog.show( tag: dialogTag, alignment: Alignment.center, animationType: SmartAnimationType.fade, clickMaskDismiss: true, maskColor: Colors.black.withValues(alpha: 0.5), builder: (_) => FirstRechargeDialog(data: data, onBuyNow: onBuyNow), ); } static Future showFromRemote( BuildContext context, { bool forceRefresh = false, }) async { try { SCLoadingManager.show(context: context); final home = await Provider.of( context, listen: false, ).refreshFirstRechargeRewardHome(forceRefresh: forceRefresh); if (!context.mounted) { return; } if (home == null || !home.shouldShowRewardDialog) { SmartDialog.dismiss(tag: dialogTag); return; } final data = FirstRechargeDialogData.fromRewardHome(home); if (data.packages.isEmpty) { SmartDialog.dismiss(tag: dialogTag); return; } SCLoadingManager.hide(); await show( context, data: data, onBuyNow: (package) { _startGooglePay(context, package); }, ); } catch (e) { SCTts.show('Failed to load first recharge rewards'); } finally { SCLoadingManager.hide(); } } static Future _startGooglePay( BuildContext context, FirstRechargePackage package, ) async { if (!Platform.isAndroid) { SCTts.show('Google Play payment is only available on Android'); return; } SmartDialog.dismiss(tag: dialogTag); final processor = Provider.of( context, listen: false, ); await processor.processFirstRechargePurchase( context: context, rechargeAmountCents: package.rechargeAmountCents, productPackage: package.productPackage, ); } @override State createState() => _FirstRechargeDialogState(); } class _FirstRechargeDialogState extends State { late int _selectedPackageIndex; @override void initState() { super.initState(); _selectedPackageIndex = widget.data.initialPackageIndex; } @override void didUpdateWidget(covariant FirstRechargeDialog oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.data != widget.data) { _selectedPackageIndex = _clampedPackageIndex(_selectedPackageIndex); } } @override Widget build(BuildContext context) { if (widget.data.packages.isEmpty) { return const SizedBox.shrink(); } final screenWidth = MediaQuery.sizeOf(context).width; final dialogWidth = math.min( screenWidth - 35.w, FirstRechargeDialog._designWidth.w, ); return Material( color: Colors.transparent, child: Transform.translate( offset: Offset(0, 11.w), child: SizedBox( width: dialogWidth, child: AspectRatio( aspectRatio: FirstRechargeDialog._designWidth / FirstRechargeDialog._designHeight, child: LayoutBuilder( builder: (context, constraints) { final scale = constraints.maxWidth / FirstRechargeDialog._designWidth; final selectedPackage = widget.data.packages[_clampedPackageIndex( _selectedPackageIndex, )]; return Stack( clipBehavior: Clip.none, children: [ _positionedImage( asset: _FirstRechargeAssets.panelTop, left: 0, top: 0, width: 340, height: 219, scale: scale, ), _positionedImage( asset: _FirstRechargeAssets.panelMiddle, left: 0, top: 219, width: 340, height: 166, scale: scale, ), _positionedImage( asset: _FirstRechargeAssets.panelBottom, left: 0, top: 351, width: 340, height: 177, scale: scale, ), _positionedImage( asset: _FirstRechargeAssets.title, left: 45, top: 26, width: 246, height: 116, scale: scale, ), Positioned( top: 146 * scale, left: 0, right: 0, child: _SingleLineText( widget.data.timeRemainingText, height: 12 * scale, fontSize: 10 * scale, fontWeight: FontWeight.w400, color: Colors.black, ), ), Positioned( left: 50 * scale, top: 168 * scale, width: 240 * scale, height: 31 * scale, child: _PriceSelector( packages: widget.data.packages, selectedIndex: _clampedPackageIndex( _selectedPackageIndex, ), scale: scale, onSelected: (index) { setState(() { _selectedPackageIndex = index; }); }, ), ), ..._buildRewardCards(selectedPackage.rewards, scale), Positioned( top: 425 * scale, left: 0, right: 0, child: _SingleLineText( selectedPackage.totalValueText, height: 12 * scale, fontSize: 10 * scale, fontWeight: FontWeight.w400, color: Colors.black, ), ), Positioned( left: 70 * scale, top: 447 * scale, width: 200 * scale, height: 42 * scale, child: SCDebounceWidget( onTap: () => widget.onBuyNow?.call(selectedPackage), child: Image.asset( _FirstRechargeAssets.buyNowButton, fit: BoxFit.fill, ), ), ), if (selectedPackage.discountText.trim().isNotEmpty) Positioned( left: 238 * scale, top: 435 * scale, width: 38 * scale, height: 38 * scale, child: _DiscountBadge( text: selectedPackage.discountText, scale: scale, ), ), Positioned( left: 294 * scale, top: 14 * scale, width: 44 * scale, height: 44 * scale, child: SCDebounceWidget( onTap: () => SmartDialog.dismiss( tag: FirstRechargeDialog.dialogTag, ), child: Center( child: Image.asset( _FirstRechargeAssets.close, width: 24 * scale, height: 25 * scale, fit: BoxFit.fill, ), ), ), ), ], ); }, ), ), ), ), ); } int _clampedPackageIndex(int value) { return value.clamp(0, widget.data.packages.length - 1).toInt(); } List _buildRewardCards( List rewards, double scale, ) { const positions = [ Offset(20, 211), Offset(124, 211), Offset(227, 211), Offset(20, 316), Offset(124, 316), Offset(227, 316), ]; final visibleRewards = rewards.take(positions.length).toList(); return List.generate(visibleRewards.length, (index) { final reward = visibleRewards[index]; final position = positions[index]; return Positioned( left: position.dx * scale, top: position.dy * scale, width: 93 * scale, height: 97 * scale, child: _RewardCard(reward: reward, scale: scale), ); }); } Widget _positionedImage({ required String asset, required double left, required double top, required double width, required double height, required double scale, }) { return Positioned( left: left * scale, top: top * scale, width: width * scale, height: height * scale, child: Image.asset(asset, fit: BoxFit.fill), ); } } class _PriceSelector extends StatelessWidget { const _PriceSelector({ required this.packages, required this.selectedIndex, required this.scale, required this.onSelected, }); final List packages; final int selectedIndex; final double scale; final ValueChanged onSelected; @override Widget build(BuildContext context) { final visiblePackages = packages.take(2).toList(); final normalizedSelectedIndex = selectedIndex.clamp(0, 1).toInt(); final selectedIsTrailing = normalizedSelectedIndex == 1; return Stack( children: [ Positioned.fill( child: Image.asset( _FirstRechargeAssets.priceTabsBg, fit: BoxFit.fill, ), ), AnimatedPositioned( duration: const Duration(milliseconds: 180), curve: Curves.easeOutCubic, left: (selectedIsTrailing ? 120 : 5) * scale, top: 2 * scale, width: (selectedIsTrailing ? 120 : 110) * scale, height: 27 * scale, child: _PriceSelectedBackground( scale: scale, trailing: selectedIsTrailing, ), ), for (var index = 0; index < visiblePackages.length; index++) Positioned( left: (index == 0 ? 5 : 125) * scale, top: 7 * scale, width: 110 * scale, height: 16 * scale, child: SCDebounceWidget( onTap: () => onSelected(index), child: _SingleLineText( visiblePackages[index].priceText, height: 16 * scale, fontSize: 14 * scale, fontWeight: FontWeight.w500, color: selectedIndex == index ? Colors.white : const Color(0xFF6F4C2B), ), ), ), ], ); } } class _PriceSelectedBackground extends StatelessWidget { const _PriceSelectedBackground({required this.scale, required this.trailing}); final double scale; final bool trailing; @override Widget build(BuildContext context) { final texture = SizedBox( width: 188.25 * scale, height: 39.52 * scale, child: Image.asset( _FirstRechargeAssets.priceSelectedTexture, fit: BoxFit.fill, ), ); return ClipPath( clipper: _PriceSelectionClipper(trailing: trailing), child: Stack( clipBehavior: Clip.hardEdge, children: [ Positioned( left: trailing ? null : -19.58 * scale, right: trailing ? -19.58 * scale : null, top: -5.37 * scale, child: trailing ? Transform( alignment: Alignment.center, transform: Matrix4.diagonal3Values(-1.0, 1.0, 1.0), child: texture, ) : texture, ), ], ), ); } } class _PriceSelectionClipper extends CustomClipper { const _PriceSelectionClipper({required this.trailing}); final bool trailing; @override Path getClip(Size size) { final w = size.width; final h = size.height; final curveX = w * (11.2769 / 110); final centerY = h * (13.3043 / 27); if (trailing) { return Path() ..moveTo(0, h) ..lineTo(0, 0) ..lineTo(w - curveX, 0) ..cubicTo( w - w * (7.51795 / 110), h * (0.521738 / 27), w, h * (3.91303 / 27), w, centerY, ) ..cubicTo( w, h * (22.6956 / 27), w - w * (7.51795 / 110), h * (26.3478 / 27), w - curveX, h, ) ..lineTo(0, h) ..close(); } return Path() ..moveTo(w, h) ..lineTo(w, 0) ..lineTo(curveX, 0) ..cubicTo( w * (7.51795 / 110), h * (0.521738 / 27), 0, h * (3.91303 / 27), 0, centerY, ) ..cubicTo( 0, h * (22.6956 / 27), w * (7.51795 / 110), h * (26.3478 / 27), curveX, h, ) ..lineTo(w, h) ..close(); } @override bool shouldReclip(covariant _PriceSelectionClipper oldClipper) { return oldClipper.trailing != trailing; } } class _RewardCard extends StatelessWidget { const _RewardCard({required this.reward, required this.scale}); final FirstRechargeReward reward; final double scale; @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( child: Image.asset( reward.isCoinReward ? _FirstRechargeAssets.rewardCardCoin : _FirstRechargeAssets.rewardCard, fit: BoxFit.fill, ), ), if (!reward.isCoinReward && reward.badgeText.trim().isNotEmpty) Positioned( left: 8 * scale, top: 3 * scale, width: 42 * scale, height: 10 * scale, child: _SingleLineText( reward.badgeText, height: 10 * scale, fontSize: 8 * scale, fontWeight: FontWeight.w400, color: Colors.white, alignment: Alignment.centerLeft, ), ), Positioned( left: 17 * scale, top: 14 * scale, width: 59 * scale, height: 59 * scale, child: _RewardCover(reward: reward, scale: scale), ), Positioned( left: 5 * scale, right: 5 * scale, top: 75 * scale, height: 15 * scale, child: _SingleLineText( reward.title, height: 15 * scale, fontSize: 12 * scale, fontWeight: FontWeight.w400, color: const Color(0xFF6F4C2B), ), ), ], ); } } class _RewardCover extends StatelessWidget { const _RewardCover({required this.reward, required this.scale}); final FirstRechargeReward reward; final double scale; @override Widget build(BuildContext context) { final cover = reward.cover.trim(); if (cover.startsWith('http://') || cover.startsWith('https://')) { return CachedNetworkImage( imageUrl: cover, fit: BoxFit.contain, placeholder: (_, __) => _fallbackCover(), errorWidget: (_, __, ___) => _fallbackCover(), ); } if (cover.isNotEmpty) { return Image.asset( cover, fit: BoxFit.contain, errorBuilder: (_, __, ___) => _fallbackCover(), ); } return _fallbackCover(); } Widget _fallbackCover() { if (reward.isCoinReward) { return Image.asset( _FirstRechargeAssets.yumiCoin, width: 42 * scale, height: 42 * scale, fit: BoxFit.contain, ); } final fallbackAsset = _fallbackAsset; if (fallbackAsset.isNotEmpty) { return Image.asset(fallbackAsset, fit: BoxFit.contain); } return Container( width: 48 * scale, height: 48 * scale, decoration: const BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFFFE5A8), Color(0xFFFFB04F)], ), ), child: Icon( Icons.auto_awesome_rounded, size: 28 * scale, color: const Color(0xFF6F4C2B), ), ); } String get _fallbackAsset { final title = reward.title.trim().toLowerCase(); if (title.contains('vehicle')) { return _FirstRechargeAssets.rewardVehicle; } if (title.contains('frame')) { return _FirstRechargeAssets.rewardFrame; } if (title == 'vip' || title.contains('vip')) { return _FirstRechargeAssets.rewardVip; } if (title.contains('gift')) { return _FirstRechargeAssets.rewardGift; } if (title.contains('badge') || title.contains('medal')) { return _FirstRechargeAssets.rewardBadge; } return ''; } } class _DiscountBadge extends StatelessWidget { const _DiscountBadge({required this.text, required this.scale}); final String text; final double scale; @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ Positioned.fill( child: Image.asset( _FirstRechargeAssets.discountBadge, fit: BoxFit.fill, ), ), Positioned( left: 5 * scale, right: 5 * scale, top: 8 * scale, height: 11 * scale, child: _SingleLineText( text, height: 11 * scale, fontSize: 10 * scale, fontWeight: FontWeight.w700, color: const Color(0xFFFCF0B7), shadows: [ Shadow( color: const Color(0xFF01378F).withValues(alpha: 0.89), offset: Offset(0, 1 * scale), blurRadius: 1 * scale, ), ], ), ), Positioned( left: 10 * scale, right: 10 * scale, top: 20 * scale, height: 9 * scale, child: _SingleLineText( 'off', height: 9 * scale, fontSize: 8 * scale, fontWeight: FontWeight.w500, color: const Color(0xFFFCF0B7), shadows: [ Shadow( color: const Color(0xFF01378F).withValues(alpha: 0.89), offset: Offset(0, 1 * scale), blurRadius: 1 * scale, ), ], ), ), ], ); } } class _SingleLineText extends StatelessWidget { const _SingleLineText( this.text, { required this.height, required this.fontSize, required this.fontWeight, required this.color, this.alignment = Alignment.center, this.shadows, }); final String text; final double height; final double fontSize; final FontWeight fontWeight; final Color color; final Alignment alignment; final List? shadows; @override Widget build(BuildContext context) { return Align( alignment: alignment, child: FittedBox( fit: BoxFit.scaleDown, alignment: alignment, child: SizedBox( height: height, child: Text( text, maxLines: 1, overflow: TextOverflow.visible, textAlign: alignment == Alignment.centerLeft ? TextAlign.left : TextAlign.center, style: TextStyle( height: 1, fontSize: fontSize, fontWeight: fontWeight, color: color, letterSpacing: 0, shadows: shadows, ), ), ), ), ); } } class _FirstRechargeAssets { static const String panelTop = 'sc_images/first_recharge/panel_top.png'; static const String panelMiddle = 'sc_images/first_recharge/panel_middle.png'; static const String panelBottom = 'sc_images/first_recharge/panel_bottom.png'; static const String close = 'sc_images/first_recharge/close.png'; static const String title = 'sc_images/first_recharge/title.png'; static const String priceTabsBg = 'sc_images/first_recharge/price_tabs_bg.png'; static const String priceSelectedTexture = 'sc_images/first_recharge/price_selected_texture.png'; static const String rewardCard = 'sc_images/first_recharge/reward_card.png'; static const String rewardCardCoin = 'sc_images/first_recharge/reward_card_coin.png'; static const String rewardAvatarFrame = 'sc_images/first_recharge/reward_avatar_frame_1.png'; static const String rewardVehicle = 'sc_images/first_recharge/reward_vehicle.png'; static const String rewardFrame = 'sc_images/first_recharge/reward_frame.png'; static const String rewardCoinStack = 'sc_images/first_recharge/reward_coin_stack.png'; static const String rewardGift = 'sc_images/first_recharge/reward_gift.png'; static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png'; static const String rewardBadge = 'sc_images/index/sc_icon_medals.png'; static const String buyNowButton = 'sc_images/first_recharge/buy_now_button.png'; static const String discountBadge = 'sc_images/first_recharge/discount_badge.png'; static const String yumiCoin = 'sc_images/register_reward/yumi_coin.png'; }