import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/chat/chat_opening_provider.dart'; import '../../../core/constants.dart'; import '../../../core/pairing/pairing_notifier.dart'; import '../payment_notifier.dart'; /// Payment screen. /// /// Reuses the mock pricing service (tiers + free trial). The customer picks a /// duration (or auto-selects the free trial); on tap the screen creates a /// `pending` payment session, then on "Bayar" / "Mulai" confirms it and routes /// to the searching screen carrying `paymentSessionId` (and `targetedMitraId` /// if this is a "Curhat lagi" flow). /// /// Reachable from: /// - Home "Mulai Curhat" CTA → no targeted mitra, normal blast follows. /// - Chat history "Curhat lagi" CTA → targetedMitraId set, returning-chat /// flow follows. class PaymentScreen extends ConsumerStatefulWidget { /// "Curhat lagi" only — when set, the eventual chat-request goes through /// the returning-chat endpoint targeting this mitra. final String? targetedMitraId; /// Optional display name for the targeted mitra, surfaced in the screen /// header so the customer knows who they're paying to chat with again. final String? mitraName; /// The topic-sensitivity choice the customer made in the topic-selection /// bottom sheet on the home screen. Carried through here to be passed into /// the chat-request API after confirm. Defaults to regular. final TopicSensitivity topicSensitivity; const PaymentScreen({ super.key, this.targetedMitraId, this.mitraName, this.topicSensitivity = TopicSensitivity.regular, }); @override ConsumerState createState() => _PaymentScreenState(); } class _PaymentScreenState extends ConsumerState { /// Local UI selection (not in the notifier) — the duration the customer is /// previewing before they tap to lock it in via createSession. int? _selectedDurationMinutes; /// True once we've kicked off `createSession()` for the current selection; /// used to suppress double-taps while the round-trip is in flight. bool _creatingSession = false; @override void initState() { super.initState(); // Make sure no stale state leaks in from a previous payment attempt. Future.microtask(() => ref.read(paymentProvider.notifier).reset()); } @override void dispose() { // Best-effort cancel on back/dispose if we still have a `pending` row. // The notifier checks state before calling the API, so this is safe to // call unconditionally. // ignore: discarded_futures ref.read(paymentProvider.notifier).cancelIfPending(); super.dispose(); } Future _onTierTapped({ required int durationMinutes, required int price, }) async { if (_creatingSession) return; // `price` is informational (already shown in the tier card) — the source // of truth for the amount comes back from the backend. setState(() { _selectedDurationMinutes = durationMinutes; _creatingSession = true; }); await ref.read(paymentProvider.notifier).createSession( durationMinutes: durationMinutes, targetedMitraId: widget.targetedMitraId, ); if (mounted) setState(() => _creatingSession = false); } Future _onConfirmTapped() async { final notifier = ref.read(paymentProvider.notifier); await notifier.confirm(); } Future _routeToSearchOnConfirmed(PaymentConfirmedData payment) async { // Kick off the right pairing flow against the freshly-confirmed payment. final pairing = ref.read(pairingProvider.notifier); if (payment.targetedMitraId != null) { await pairing.startTargetedSearch( paymentSessionId: payment.paymentSessionId, mitraId: payment.targetedMitraId!, mitraName: widget.mitraName ?? 'Bestie', topicSensitivity: widget.topicSensitivity, ); } else { await pairing.startSearch( paymentSessionId: payment.paymentSessionId, topicSensitivity: widget.topicSensitivity, ); } if (!mounted) return; // Reset our local notifier so a future payment attempt starts clean. ref.read(paymentProvider.notifier).reset(); // Phase 4 Stage 5: targeted "Curhat lagi" lands on the dedicated // SWaitingBestie overlay screen; general blast still uses the searching // shell (which renders inline soft-prompt + timeout panels). if (payment.targetedMitraId != null) { context.go('/chat/waiting-targeted/${payment.targetedMitraId}'); } else { context.go('/chat/searching'); } } @override Widget build(BuildContext context) { // One-shot side-effect listener: when the payment lands in `confirmed`, // route to the searching screen. ref.listen(paymentProvider, (prev, next) { if (next is PaymentConfirmedData) { // ignore: discarded_futures _routeToSearchOnConfirmed(next); } }); final paymentState = ref.watch(paymentProvider); final pricingAsync = ref.watch(chatPricingProvider); final isReturning = widget.targetedMitraId != null; return PopScope( canPop: true, child: Scaffold( appBar: AppBar( title: Text(isReturning ? 'Chat lagi dengan ${widget.mitraName ?? 'Bestie'}' : 'Pilih Sesi & Bayar'), leading: IconButton( icon: const Icon(Icons.chevron_left), onPressed: () { // PopScope above lets canPop fire dispose() which cancels the // pending session. If there's no back-stack, fall back to home. if (context.canPop()) { context.pop(); } else { context.go('/home'); } }, ), ), body: pricingAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => const Center( child: Padding( padding: EdgeInsets.all(24), child: Text('Gagal memuat harga. Coba lagi.', textAlign: TextAlign.center), ), ), data: (pricing) => _buildBody(pricing, paymentState), ), ), ); } Widget _buildBody(PricingData pricing, PaymentSessionData paymentState) { // Inline error widget per project memory ("Avoid SnackBars for provider errors"). final errorBanner = paymentState is PaymentErrorData ? Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red.shade700), const SizedBox(width: 8), Expanded( child: Text( paymentState.message, style: TextStyle(color: Colors.red.shade900), ), ), ], ), ) : const SizedBox.shrink(); return Column( children: [ errorBanner, Expanded( child: ListView( padding: const EdgeInsets.all(24), children: [ const Text( 'Pilih Durasi Curhat', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), if (pricing.freeTrialEligible) ...[ _FreeTrialCard( durationMinutes: pricing.freeTrialDurationMinutes, selected: paymentState is PaymentPendingData && paymentState.isFreeTrial, onTap: () => _onTierTapped( // For free trial: backend still wants a duration_minutes — // pass the trial duration. The backend overrides amount→0 // when the customer is eligible. durationMinutes: pricing.freeTrialDurationMinutes, price: 0, ), ), const Divider(height: 24), ], ...pricing.tiers.map((tier) { final selected = _selectedDurationMinutes == tier.durationMinutes && paymentState is PaymentPendingData && !paymentState.isFreeTrial; return _TierCard( label: tier.label, priceLabel: formatRupiah(tier.price), selected: selected, onTap: () => _onTierTapped( durationMinutes: tier.durationMinutes, price: tier.price, ), ); }), ], ), ), if (paymentState is PaymentPendingData || paymentState is PaymentConfirmingData || paymentState is PaymentCreatingData) _ConfirmBar( paymentState: paymentState, onConfirm: _onConfirmTapped, formatPrice: formatRupiah, ), ], ); } } class _FreeTrialCard extends StatelessWidget { final int durationMinutes; final bool selected; final VoidCallback onTap; const _FreeTrialCard({ required this.durationMinutes, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return Card( color: selected ? Colors.green.shade100 : Colors.green.shade50, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: selected ? BorderSide(color: Colors.green.shade700, width: 1.5) : BorderSide.none, ), child: ListTile( leading: const Icon(Icons.card_giftcard, color: Colors.green), title: Text('Free Trial ($durationMinutes Menit)'), subtitle: const Text('Gratis untuk pertama kali!'), trailing: Text( 'Gratis', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green.shade800), ), onTap: onTap, ), ); } } class _TierCard extends StatelessWidget { final String label; final String priceLabel; final bool selected; final VoidCallback onTap; const _TierCard({ required this.label, required this.priceLabel, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: selected ? const BorderSide(color: Colors.pink, width: 1.5) : BorderSide.none, ), child: ListTile( title: Text(label), trailing: Text( priceLabel, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onTap: onTap, ), ); } } class _ConfirmBar extends StatelessWidget { final PaymentSessionData paymentState; final Future Function() onConfirm; final String Function(int) formatPrice; const _ConfirmBar({ required this.paymentState, required this.onConfirm, required this.formatPrice, }); @override Widget build(BuildContext context) { final isCreating = paymentState is PaymentCreatingData; final isConfirming = paymentState is PaymentConfirmingData; final pending = paymentState is PaymentPendingData ? paymentState as PaymentPendingData : null; final totalLabel = pending == null ? '...' : pending.isFreeTrial ? 'Gratis' : formatPrice(pending.amount); final ctaLabel = pending != null && pending.isFreeTrial ? 'Mulai' : 'Bayar'; final disabled = isCreating || isConfirming || pending == null; return SafeArea( top: false, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, -2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total', style: TextStyle(fontSize: 16)), Text( totalLabel, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: disabled ? null : onConfirm, child: isConfirming || isCreating ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text(ctaLabel, style: const TextStyle(fontSize: 16)), ), ), ], ), ), ); } }