import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../design/pencil_theme.dart'; /// 本页「图标」相对设计稿的缩放(支付方式 logo、摘要钻石等)。 const double _paymentSheetIconScale = 1.5; /// 与 `funymee_home.pen` **zL8hY / p7kQm** 对齐的第三方支付底部表单。 /// 先点选支付方式,再点底部 **Pay** 确认(与画板 `btnPayPrimary` 一致)。 /// /// [summaryTierCredits] / [summaryTierBonus] 与购买页档位一致(原始档位积分 + 原始档位赠送); /// 均为 `null` 时从 [product] 读取。 Future showThirdPartyPaymentMethodSheet( BuildContext context, { required List methods, required PaymentProductItem product, int? summaryTierCredits, int? summaryTierBonus, }) { if (methods.isEmpty) return Future.value(null); return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, // 使用框架 ModalBarrier 遮罩,避免与下层页面视觉上糊在一起。 barrierColor: PencilTheme.paymentModalDimScrim, isDismissible: true, builder: (ctx) { final w = MediaQuery.sizeOf(ctx).width; final sheetW = w < PencilTheme.designWidth ? w : PencilTheme.designWidth; return Align( alignment: Alignment.bottomCenter, child: SizedBox( width: sheetW, child: _ThirdPartyPaymentSheetBody( methods: methods, product: product, summaryTierCredits: summaryTierCredits, summaryTierBonus: summaryTierBonus, ), ), ); }, ); } class _ThirdPartyPaymentSheetBody extends StatefulWidget { const _ThirdPartyPaymentSheetBody({ required this.methods, required this.product, this.summaryTierCredits, this.summaryTierBonus, }); final List methods; final PaymentProductItem product; /// 购买页传入的档位积分;`null` 则用 [product.credits]。 final int? summaryTierCredits; /// 购买页传入的档位赠送;`null` 则用 [product.bonus]。 final int? summaryTierBonus; @override State<_ThirdPartyPaymentSheetBody> createState() => _ThirdPartyPaymentSheetBodyState(); } class _ThirdPartyPaymentSheetBodyState extends State<_ThirdPartyPaymentSheetBody> { late int _selectedIndex; @override void initState() { super.initState(); _selectedIndex = 0; } String get _payLabel => 'Pay Now'; @override Widget build(BuildContext context) { final bottomSafe = MediaQuery.paddingOf(context).bottom; final bottomPad = bottomSafe < 34 ? 34.0 : bottomSafe.toDouble(); const sheetRadius = BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ); return Container( decoration: BoxDecoration( borderRadius: sheetRadius, boxShadow: PencilTheme.paymentSheetOuterShadow, ), child: ClipRRect( borderRadius: sheetRadius, clipBehavior: Clip.antiAlias, child: Container( foregroundDecoration: BoxDecoration( borderRadius: sheetRadius, border: const Border( top: BorderSide(color: PencilTheme.genHintBorder, width: 1), left: BorderSide(color: PencilTheme.genHintBorder, width: 1), right: BorderSide(color: PencilTheme.genHintBorder, width: 1), ), ), child: Stack( fit: StackFit.loose, children: [ const Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: PencilTheme.paymentSheetBodyGradient, ), ), ), Positioned.fill( child: Opacity( opacity: 0.2, child: Image.asset( 'assets/images/checker_20px_500.png', fit: BoxFit.cover, alignment: Alignment.topCenter, filterQuality: FilterQuality.medium, ), ), ), Material( color: Colors.transparent, child: SingleChildScrollView( padding: EdgeInsets.fromLTRB(20, 10, 20, bottomPad), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Container( width: 40, height: 5, decoration: BoxDecoration( color: PencilTheme.paymentRadioOffStroke, borderRadius: BorderRadius.circular(99), ), ), ), const SizedBox(height: 18), Text( 'Payment Methods', textAlign: TextAlign.center, style: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w700, color: PencilTheme.stone900, height: 1.2, ), ), const SizedBox(height: 18), _PaymentSummaryCard( product: widget.product, tierCredits: widget.summaryTierCredits ?? widget.product.credits, tierBonus: widget.summaryTierBonus ?? widget.product.bonus, ), const SizedBox(height: 18), Text( 'Payment', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: PencilTheme.stone600, height: 1.2, ), ), const SizedBox(height: 12), for (var i = 0; i < widget.methods.length; i++) ...[ if (i > 0) const SizedBox(height: 12), _PaymentMethodOptionCard( method: widget.methods[i], tierCreditsForChannelPercent: widget.summaryTierCredits ?? widget.product.credits ?? 0, selected: _selectedIndex == i, iconColor: _iconAccent(widget.methods[i]), onSelect: () => setState(() => _selectedIndex = i), ), ], const SizedBox(height: 18), _PayCtaButton( label: _payLabel, onPressed: () => Navigator.pop( context, widget.methods[_selectedIndex], ), ), ], ), ), ), ], ), ), ), ); } static Color _iconAccent(PaymentMethodItem m) { final n = m.displayName.toLowerCase(); if (n.contains('app store')) { return PencilTheme.paymentAppStoreIconBlue; } return PencilTheme.stone900; } } String _formatProductPrice(PaymentProductItem p) { final a = p.actualAmount?.trim(); if (a == null || a.isEmpty) return '\$—'; if (a.startsWith(r'$') || a.startsWith('¥')) return a; return '\$$a'; } /// 支付渠道赠送:仅用 [PaymentMethodItem.bonusCredits],相对档位原始积分 [tierCredits] 换算为 `+N% More Credits`(过小为 `+<1% More Credits`)。 String? _channelBonusCreditsPercentLine( int tierCredits, int? channelBonusCredits, ) { if (channelBonusCredits == null || channelBonusCredits <= 0) return null; if (tierCredits <= 0) return null; final rounded = ((channelBonusCredits * 100.0) / tierCredits).round(); if (rounded > 0) return '+$rounded% More Credits'; return '+<1% More Credits'; } class _PaymentSummaryCard extends StatelessWidget { const _PaymentSummaryCard({ required this.product, required this.tierCredits, required this.tierBonus, }); final PaymentProductItem product; final int? tierCredits; final int? tierBonus; @override Widget build(BuildContext context) { final credits = tierCredits ?? 0; final tierGift = tierBonus; final price = _formatProductPrice(product); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: PencilTheme.genNavBackStroke), boxShadow: PencilTheme.paymentSummaryCardShadow, ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.diamond_outlined, size: 22 * _paymentSheetIconScale, color: PencilTheme.paymentSummaryGem, ), const SizedBox(width: 10), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( '$credits', style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w700, color: PencilTheme.stone900, ), ), if (tierGift != null && tierGift > 0) ...[ const SizedBox(width: 4), Text( '+$tierGift', style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w700, color: PencilTheme.paymentSummaryBonusGreen, ), ), ], const SizedBox(width: 4), Text( 'Credits', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: PencilTheme.paymentSummaryCreditsLabel, ), ), ], ), ), Text( price, style: GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.w800, color: PencilTheme.stone900, ), ), ], ), ); } } class _PayCtaButton extends StatelessWidget { const _PayCtaButton({required this.label, required this.onPressed}); final String label; final VoidCallback onPressed; @override Widget build(BuildContext context) { return Container( height: 52, decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), gradient: PencilTheme.paymentPayButtonGradient, boxShadow: PencilTheme.paymentPayButtonShadow, ), child: Material( color: Colors.transparent, child: InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(999), child: Center( child: Text( label, style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ), ), ); } } class _PaymentMethodOptionCard extends StatelessWidget { const _PaymentMethodOptionCard({ required this.method, required this.tierCreditsForChannelPercent, required this.selected, required this.iconColor, required this.onSelect, }); final PaymentMethodItem method; /// 渠道赠送占比分母:与摘要一致的档位原始积分(非赠送)。 final int tierCreditsForChannelPercent; final bool selected; final Color iconColor; final VoidCallback onSelect; static const double _radioSize = 22; static const double _iconBox = 28 * _paymentSheetIconScale; @override Widget build(BuildContext context) { final title = method.displayName.isNotEmpty ? method.displayName : (method.paymentMethod ?? 'Payment'); final channelBonusLine = _channelBonusCreditsPercentLine( tierCreditsForChannelPercent, method.bonusCredits, ); return Material( color: Colors.white, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: onSelect, borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? PencilTheme.paymentMethodSelectedStroke : PencilTheme.genNavBackStroke, width: selected ? 2 : 1, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: _radioSize, height: _radioSize, child: selected ? Stack( alignment: Alignment.center, children: [ Container( width: _radioSize, height: _radioSize, decoration: BoxDecoration( shape: BoxShape.circle, color: PencilTheme.paymentMethodSelectedStroke, border: Border.all( color: PencilTheme.paymentMethodSelectedStroke, width: 2, ), ), ), Container( width: 8, height: 8, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white, ), ), ], ) : Container( width: _radioSize, height: _radioSize, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: PencilTheme.paymentRadioOffStroke, width: 2, ), ), ), ), const SizedBox(width: 12), SizedBox( width: _iconBox, height: _iconBox, child: _PaymentMethodSheetIconSmall( iconUrl: method.icon, color: iconColor, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( title, style: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w700, color: PencilTheme.stone900, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (channelBonusLine != null) ...[ const SizedBox(height: 4), Text( channelBonusLine, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: PencilTheme.profileCredits, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ), if (method.recommend == true) Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: PencilTheme.cardThumbBg, borderRadius: BorderRadius.circular(8), ), child: Text( 'Recommend', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w600, color: PencilTheme.stone700, ), ), ), ], ), ), ), ); } } class _PaymentMethodSheetIconSmall extends StatelessWidget { const _PaymentMethodSheetIconSmall({required this.color, this.iconUrl}); final String? iconUrl; final Color color; @override Widget build(BuildContext context) { final url = iconUrl?.trim(); final fallback = Icon( Icons.payment_outlined, size: 22 * _paymentSheetIconScale, color: color, ); if (url != null && url.isNotEmpty) { final img = Image.network( url, fit: BoxFit.contain, alignment: Alignment.center, errorBuilder: (_, _, _) => Center(child: fallback), ); if (color == PencilTheme.paymentAppStoreIconBlue) { return ColorFiltered( colorFilter: ColorFilter.mode(color, BlendMode.srcIn), child: img, ); } return img; } return Center(child: fallback); } }