import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/pairing/pairing_notifier.dart'; import '../../../core/theme/halo_tokens.dart'; import '../../../core/theme/widgets/widgets.dart'; /// Phase 4 Stage 5 — `SWaitingBestie` overlay. /// /// Entry route: `/chat/waiting-targeted/:mitraId` — pushed from the chat /// history "Curhat lagi" CTA after the targeted payment session is confirmed. /// /// Three sub-states mapped from `pairingProvider`: /// /// - `waiting` (PairingTargetedWaitingData) — orb + 20s countdown + cancel. /// The countdown is purely cosmetic; the server owns the auto-reject timer. /// - `accepted` (PairingBestieFoundData / PairingActiveData) — routes into /// the chat screen immediately. /// - `declined` (PairingTargetedUnavailableData) — shows the bestie-offline /// popup. TODO(stage8): swap this stub for the proper BestieOfflinePopup /// component once Stage 8 lands. class TargetedWaitingScreen extends ConsumerStatefulWidget { final String mitraId; const TargetedWaitingScreen({super.key, required this.mitraId}); @override ConsumerState createState() => _TargetedWaitingScreenState(); } class _TargetedWaitingScreenState extends ConsumerState { bool _popupShown = false; @override void initState() { super.initState(); ref.listenManual(pairingProvider, _onPairingState); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _onPairingState(null, ref.read(pairingProvider)); }); } void _onPairingState(PairingData? prev, PairingData next) { if (!mounted) return; if (next is PairingBestieFoundData) { context.go('/chat/session/${next.sessionId}', extra: next.mitraName); return; } if (next is PairingActiveData) { context.go('/chat/session/${next.sessionId}', extra: next.mitraName); return; } if (next is PairingCancelledData) { context.go('/home'); return; } if (next is PairingTargetedUnavailableData && !_popupShown) { _popupShown = true; // TODO(stage8): replace stub with the production BestieOfflinePopup // (Stage 8 owns the proper variant + fallback-to-blast surface). // ignore: discarded_futures HaloPopup.show( context, title: '${next.mitraName} lagi nggak online', body: 'bestie kamu belum bisa nerima chat sekarang. coba bestie lain atau balik ke beranda dulu ya.', primary: HaloPopupAction( label: 'kembali ke home', onPressed: () { ref.read(pairingProvider.notifier).reset(); if (mounted) context.go('/home'); }, ), ).then((_) { if (mounted) _popupShown = false; }); } } @override Widget build(BuildContext context) { final state = ref.watch(pairingProvider); final waiting = state is PairingTargetedWaitingData ? state : null; final mitraName = waiting?.mitraName ?? 'bestie'; final secondsRemaining = waiting?.secondsRemaining ?? 0; return PopScope( // Targeted-wait is reachable directly from chat history; per the // deep-link pop-fallback rule (project memory), we drop the user // back to home if they swipe back rather than into a stale stack. canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; ref.read(pairingProvider.notifier).cancelSearch(); }, child: Scaffold( backgroundColor: HaloTokens.bg, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB( HaloSpacing.s24, HaloSpacing.s24, HaloSpacing.s24, HaloSpacing.s24, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ HaloOrb( size: 120, seed: mitraName.hashCode, label: mitraName, ), const SizedBox(height: HaloSpacing.s20), const Text( '◦ MENUNGGU JAWABAN ◦', style: TextStyle( fontFamily: HaloTokens.fontBody, fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 1.6, color: HaloTokens.brand, ), ), const SizedBox(height: HaloSpacing.s8), Text( 'lagi nungguin $mitraName', textAlign: TextAlign.center, style: const TextStyle( fontFamily: HaloTokens.fontDisplay, fontSize: 24, height: 30 / 24, fontWeight: FontWeight.w700, color: HaloTokens.brandDark, ), ), const SizedBox(height: HaloSpacing.s12), Container( padding: const EdgeInsets.symmetric( horizontal: HaloSpacing.s16, vertical: HaloSpacing.s8, ), decoration: BoxDecoration( color: HaloTokens.brandSofter, borderRadius: BorderRadius.circular(999), border: Border.all(color: HaloTokens.brandSoft), ), child: Text( '${secondsRemaining}d', style: const TextStyle( fontFamily: HaloTokens.fontDisplay, fontSize: 22, fontWeight: FontWeight.w700, color: HaloTokens.brandDark, ), ), ), const SizedBox(height: HaloSpacing.s12), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 280), child: const Text( 'kalau bestie nggak respon dalam 20 detik, kami bantu cariin yang lain.', textAlign: TextAlign.center, style: TextStyle( fontFamily: HaloTokens.fontBody, fontSize: 13, height: 20 / 13, color: HaloTokens.inkSoft, ), ), ), ], ), ), ), HaloButton( label: 'batalkan', variant: HaloButtonVariant.ghost, fullWidth: true, onPressed: () => ref.read(pairingProvider.notifier).cancelSearch(), ), ], ), ), ), ), ); } }