import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/chat/chat_opening_provider.dart'; import '../../../core/chat/session_closure_notifier.dart'; import '../../../core/constants.dart'; import '../../../core/theme/halo_tokens.dart'; import '../../../core/theme/widgets/halo_button.dart'; import '../../payment/state/payment_draft_provider.dart'; /// Extension-only pricing sheet — Phase 4 Stage 6 layout. /// /// Used solely for in-session extension requests; the initial pairing flow /// goes through `/payment` instead. Free-trial is never offered for extensions. /// /// Mirrors the Stage 3 duration-pick screen: chat/call toggle on top, /// 5-option tier list below, single CTA at the bottom. The `perpanjang` /// behavior is unchanged from Phase 3.7 — submit calls /// [SessionClosure.requestExtension], which runs the payment-session /// create+confirm and then the extend POST. class PricingBottomSheet extends ConsumerStatefulWidget { /// Required — the in-progress chat session id this extension targets. final String extensionSessionId; /// Optional — fired when the customer chooses the "cukup, akhiri sesi" /// ghost CTA. The sheet pops itself before invoking the callback so the /// downstream confirm-end popup chain can stack on a clean route. final VoidCallback? onEndSession; const PricingBottomSheet({ super.key, required this.extensionSessionId, this.onEndSession, }); /// Show for session extension (from chat screen). static Future showForExtension( BuildContext context, { required String sessionId, VoidCallback? onEndSession, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: HaloTokens.surface, // Material 3 auto-injects a drag-handle widget above the sheet when // the theme has one configured. We render our own pill inside // `_Body` to control its exact tint/size — explicitly disable the // auto one so the user doesn't see two stacked horizontal lines. showDragHandle: false, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), builder: (_) => PricingBottomSheet( extensionSessionId: sessionId, onEndSession: onEndSession, ), ); } @override ConsumerState createState() => _PricingBottomSheetState(); } class _PricingBottomSheetState extends ConsumerState { PaymentMode _mode = PaymentMode.chat; String? _selectedDurationId; List _tiersForMode(PricingData pricing) { // Phase 4 — chat/call tier groups. Falls back to legacy `tiers` when the // backend hasn't been cut over yet (so the sheet still works locally // against an old backend). if (_mode == PaymentMode.call) { return pricing.callTiers.isNotEmpty ? pricing.callTiers : pricing.tiers; } return pricing.chatTiers.isNotEmpty ? pricing.chatTiers : pricing.tiers; } void _onTierTap(PriceTier tier) { setState(() { _selectedDurationId = tier.id ?? tier.durationMinutes.toString(); }); } void _onConfirm(PriceTier tier) { Navigator.of(context).pop(); ref.read(sessionClosureProvider.notifier).requestExtension( widget.extensionSessionId, durationMinutes: tier.durationMinutes, price: tier.price, ); } void _onEndSession() { Navigator.of(context).pop(); widget.onEndSession?.call(); } @override Widget build(BuildContext context) { final pricingAsync = ref.watch(chatPricingProvider); return DraggableScrollableSheet( initialChildSize: 0.65, minChildSize: 0.5, maxChildSize: 0.92, expand: false, builder: (_, scrollController) { return SafeArea( top: false, child: pricingAsync.when( loading: () => const SizedBox( height: 240, child: Center(child: CircularProgressIndicator()), ), error: (_, __) => const SizedBox( height: 240, child: Center(child: Text('Gagal memuat harga. Coba lagi.')), ), data: (pricing) => _Body( pricing: pricing, mode: _mode, selectedDurationId: _selectedDurationId, tiers: _tiersForMode(pricing), scrollController: scrollController, onModeChanged: (m) => setState(() { _mode = m; _selectedDurationId = null; }), onTierTap: _onTierTap, onConfirm: _onConfirm, onEndSession: widget.onEndSession != null ? _onEndSession : null, ), ), ); }, ); } } class _Body extends StatelessWidget { final PricingData pricing; final PaymentMode mode; final String? selectedDurationId; final List tiers; final ScrollController scrollController; final ValueChanged onModeChanged; final ValueChanged onTierTap; final ValueChanged onConfirm; final VoidCallback? onEndSession; const _Body({ required this.pricing, required this.mode, required this.selectedDurationId, required this.tiers, required this.scrollController, required this.onModeChanged, required this.onTierTap, required this.onConfirm, required this.onEndSession, }); @override Widget build(BuildContext context) { final selectedTier = tiers.firstWhere( (t) => (t.id ?? t.durationMinutes.toString()) == selectedDurationId, orElse: () => const PriceTier(durationMinutes: 0, price: 0, label: ''), ); final hasSelection = selectedTier.durationMinutes > 0; final ctaLabel = hasSelection ? '${mode == PaymentMode.call ? '📞' : '💬'} perpanjang ${formatRupiah(selectedTier.price)}' : 'pilih durasi dulu'; return Column( children: [ const SizedBox(height: HaloSpacing.s8), Container( width: 40, height: 4, decoration: const BoxDecoration( color: HaloTokens.border, borderRadius: HaloRadius.pill, ), ), const SizedBox(height: HaloSpacing.s12), const Text( 'waktu curhat habis', style: TextStyle( fontFamily: HaloTokens.fontDisplay, fontSize: 18, fontWeight: FontWeight.w700, color: HaloTokens.brandDark, ), ), const SizedBox(height: HaloSpacing.s4), const Text( 'mau tambah waktu?', style: TextStyle( fontFamily: HaloTokens.fontBody, fontSize: 13, color: HaloTokens.inkSoft, ), ), const SizedBox(height: HaloSpacing.s16), Padding( padding: const EdgeInsets.symmetric(horizontal: HaloSpacing.s24), child: _ModeToggle(mode: mode, onChanged: onModeChanged), ), const SizedBox(height: HaloSpacing.s12), Expanded( child: tiers.isEmpty ? const _EmptyState() : ListView.separated( controller: scrollController, padding: const EdgeInsets.fromLTRB( HaloSpacing.s24, HaloSpacing.s4, HaloSpacing.s24, HaloSpacing.s16, ), itemCount: tiers.length, separatorBuilder: (_, __) => const SizedBox(height: HaloSpacing.s8), itemBuilder: (context, i) { final tier = tiers[i]; final id = tier.id ?? tier.durationMinutes.toString(); final selected = id == selectedDurationId; return _TierCard( tier: tier, selected: selected, onTap: () => onTierTap(tier), ); }, ), ), Padding( // No top border here — the drag handle is the only top "divider" // the figma shows. A second border above the action bar reads as a // duplicate line stacked under the list separator visuals. padding: const EdgeInsets.fromLTRB( HaloSpacing.s24, HaloSpacing.s12, HaloSpacing.s24, HaloSpacing.s16, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ HaloButton( label: ctaLabel, size: HaloButtonSize.lg, fullWidth: true, onPressed: hasSelection ? () => onConfirm(selectedTier) : null, ), if (onEndSession != null) ...[ const SizedBox(height: HaloSpacing.s8), HaloButton( label: 'cukup, akhiri sesi', variant: HaloButtonVariant.ghost, fullWidth: true, onPressed: onEndSession, ), ], ], ), ), ], ); } } class _ModeToggle extends StatelessWidget { final PaymentMode mode; final ValueChanged onChanged; const _ModeToggle({required this.mode, required this.onChanged}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: HaloTokens.brandSofter, borderRadius: HaloRadius.pill, ), child: Row( children: [ Expanded(child: _Pill(label: 'chat', selected: mode == PaymentMode.chat, onTap: () => onChanged(PaymentMode.chat))), Expanded(child: _Pill(label: 'call', selected: mode == PaymentMode.call, onTap: () => onChanged(PaymentMode.call))), ], ), ); } } class _Pill extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; const _Pill({required this.label, required this.selected, required this.onTap}); @override Widget build(BuildContext context) { return Material( color: selected ? HaloTokens.surface : Colors.transparent, borderRadius: HaloRadius.pill, child: InkWell( borderRadius: HaloRadius.pill, onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: HaloSpacing.s8), alignment: Alignment.center, child: Text( label, style: TextStyle( fontFamily: HaloTokens.fontBody, fontSize: 13.5, fontWeight: FontWeight.w600, color: selected ? HaloTokens.brandDark : HaloTokens.inkSoft, ), ), ), ), ); } } class _TierCard extends StatelessWidget { final PriceTier tier; final bool selected; final VoidCallback onTap; const _TierCard({required this.tier, required this.selected, required this.onTap}); @override Widget build(BuildContext context) { return Material( color: selected ? HaloTokens.brandSofter : HaloTokens.surface, borderRadius: HaloRadius.lg, child: InkWell( borderRadius: HaloRadius.lg, onTap: onTap, child: AnimatedContainer( duration: HaloMotion.fast, padding: const EdgeInsets.all(HaloSpacing.s16), decoration: BoxDecoration( border: Border.all( color: selected ? HaloTokens.brand : HaloTokens.border, width: selected ? 2 : 1, ), borderRadius: HaloRadius.lg, ), child: Row( children: [ Container( width: 44, height: 44, decoration: const BoxDecoration( color: HaloTokens.surface, borderRadius: HaloRadius.md, ), alignment: Alignment.center, child: Text( '${tier.durationMinutes}', style: const TextStyle( fontFamily: HaloTokens.fontDisplay, fontSize: 16, fontWeight: FontWeight.w700, color: HaloTokens.brandDark, ), ), ), const SizedBox(width: HaloSpacing.s12), Expanded( child: Row( children: [ Text( '${tier.durationMinutes} menit', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: HaloTokens.ink, ), ), if (tier.tag != null) ...[ const SizedBox(width: HaloSpacing.s8), Container( padding: const EdgeInsets.symmetric( horizontal: HaloSpacing.s8, vertical: 2, ), decoration: const BoxDecoration( color: HaloTokens.mint, borderRadius: HaloRadius.pill, ), child: Text( tier.tag!, style: const TextStyle( fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFF1F4D34), letterSpacing: 0.4, ), ), ), ], ], ), ), Text( formatRupiah(tier.price), style: const TextStyle( fontFamily: HaloTokens.fontDisplay, fontSize: 16, fontWeight: FontWeight.w700, color: HaloTokens.brandDark, ), ), ], ), ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return const Center( child: Padding( padding: EdgeInsets.all(HaloSpacing.s24), child: Text( 'Belum ada paket untuk mode ini.', style: TextStyle(color: HaloTokens.inkSoft), ), ), ); } }