Phase 4 Stage 5: pairing UX upgrades (searching + match + targeted-wait)
Searching screen: soft-prompt card reskin, pulsing-dots panel replaces
the spinner, inline 5-min timeout panel with `coba cari lagi` (resets
pairing notifier + routes to /payment/entry for a fresh funnel — the
server-side payment is failed_pairing at that point so a stale retry
isn't valid) and `kembali ke home` ghost CTA.
Bestie-found screen: S9 Match-V4 reskin — HaloOrb + status dot +
'halo, aku bestie {name}' + `mulai sesi {N} menit →` with N pulled from
the active session's duration_minutes.
Targeted-wait overlay (new) at /chat/waiting-targeted/:mitraId. Three
sub-states from pairingProvider's PairingTargetedWaitingData:
waiting (20s countdown) / accepted (routes to chat) / declined (stubbed
BestieOfflinePopup with a TODO pointing to Stage 8). Reached via
payment_screen._routeToSearchOnConfirmed when the confirm carried a
targetedMitraId — keeps the mandatory payment-before-pairing invariant.
Dev-only POST /internal/_test/force-pairing-timeout drives the 5-min
timeout shortcut for the Maestro flow without waiting live.
Maestro 05_searching_timeout.yaml + force_pairing_timeout.js helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/chat/active_session_notifier.dart';
|
||||
import '../../../core/pairing/pairing_notifier.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
|
||||
class BestieFoundScreen extends ConsumerWidget {
|
||||
/// Phase 4 Stage 5 — S9 Match-found screen.
|
||||
///
|
||||
/// Reskinned from the v4 mock (`v4.jsx::S9MatchV4`). Shows the matched
|
||||
/// bestie's orb + a small online status dot, the matched-line copy, and a
|
||||
/// primary CTA `mulai sesi {N} menit →`. The duration is read from the active
|
||||
/// session payload (which the pairing notifier kicks via
|
||||
/// `activeSessionProvider.refresh()` on the WS `paired` event).
|
||||
///
|
||||
/// `PairingActiveData` is the auto-advance signal — fired by the notifier
|
||||
/// ~2s after WS `paired` lands. The same advance is also reachable manually
|
||||
/// via the CTA in case the user is faster than the auto-advance timer.
|
||||
class BestieFoundScreen extends ConsumerStatefulWidget {
|
||||
final String sessionId;
|
||||
final String mitraName;
|
||||
|
||||
@@ -14,34 +28,127 @@ class BestieFoundScreen extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.listen(pairingProvider, (prev, next) {
|
||||
ConsumerState<BestieFoundScreen> createState() => _BestieFoundScreenState();
|
||||
}
|
||||
|
||||
class _BestieFoundScreenState extends ConsumerState<BestieFoundScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listenManual<PairingData>(pairingProvider, (prev, next) {
|
||||
if (!mounted) return;
|
||||
if (next is PairingActiveData) {
|
||||
context.go('/chat/session/${next.sessionId}', extra: next.mitraName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _enterChat() {
|
||||
context.go('/chat/session/${widget.sessionId}', extra: widget.mitraName);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activeSession = ref.watch(activeSessionProvider).valueOrNull;
|
||||
final durationMinutes =
|
||||
activeSession?.session?['duration_minutes'] as int?;
|
||||
final ctaLabel = durationMinutes != null
|
||||
? 'mulai sesi $durationMinutes menit →'
|
||||
: 'mulai sesi →';
|
||||
final subtitle = durationMinutes != null
|
||||
? 'siap nemenin kamu $durationMinutes menit ke depan. cerita aja pelan-pelan ya 🤍'
|
||||
: 'siap nemenin kamu. cerita aja pelan-pelan ya 🤍';
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
backgroundColor: HaloTokens.bg,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
HaloSpacing.s24,
|
||||
HaloSpacing.s24,
|
||||
HaloSpacing.s24,
|
||||
HaloSpacing.s24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.check_circle, size: 80, color: Colors.green),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Bestie ditemukan!',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
HaloOrb(
|
||||
size: 140,
|
||||
seed: widget.mitraName.hashCode,
|
||||
label: widget.mitraName,
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: HaloTokens.success,
|
||||
border: Border.all(
|
||||
color: HaloTokens.bg,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s20),
|
||||
const Text(
|
||||
'◦ MATCHED ◦',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.6,
|
||||
color: HaloTokens.brand,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
Text(
|
||||
'halo, aku bestie ${widget.mitraName}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 26,
|
||||
height: 32 / 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.brandDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
height: 22 / 14,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Menghubungkan kamu ke $mitraName',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
HaloButton(
|
||||
label: ctaLabel,
|
||||
fullWidth: true,
|
||||
size: HaloButtonSize.lg,
|
||||
onPressed: _enterChat,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user