Phase 4 §4: payment-before-pair for returning users + Maestro suite
Stages 5.1, 5.3, 5.4 of the returning-user flow rework. All three §4 entry paths now require payment BEFORE pairing, matching the updated mermaid spec. * Spec (requirement/flow_customer.mermaid.md §4): payment block converges three call-sites (bestie-yang-udah-kenal-online, bestie-baru, offline-popup → cari bestie lain). PairRoute dispatches lama → targeted pair, baru/cari-lain → §3 blast. §3 retains its post-payment-shared contract. * Stage 5.1 (client_app): PaymentDraft carries targetedMitraId + topicSensitivity. bestie_history_list seeds the draft + pushes /payment/entry (was legacy /payment). searching_screen branches on draft.targetedMitraId for blast-vs-targeted dispatch. payment_entry uses resetExceptTarget(); bestie_choice_sheet + home _onCurhatBestieBaruPressed call explicit reset() before push so the keepAlive draft can't leak stale targeting into a blast. * Stage 5.3 (client_app): new BestieOfflineVariant.prePayReturning. Bestie-history-list _BestieRow splits tappable from dim so offline rows render dimmed but route taps into the popup. CTA "cari bestie lain" resets the draft + pushes /payment/entry. * Stage 5.4 (client_app): deleted legacy /payment route, payment_screen.dart, payment_notifier.dart(+.g.dart). router cleaned. * Tests (requirement/phase4-customer-flow.md + client_app/.maestro/): six Maestro flows TS-01..TS-06 covering every §4 branching point, all passing end-to-end. Shared onboarding prelude under .maestro/subflows/. New helper scripts: accept_latest_pending, force_mitra_offline, force_other_mitra_online, reset_all_mitras_online, mitra_accept_latest_internal. New backend _test endpoints to match. /reset-phone now cascade-deletes customer_transactions (FK was blocking). /force-pairing-timeout branches targeted (RETURNING_CHAT_TIMEOUT via expireTargetedPairingRequest, now exported) vs blast (PAIRING_FAILED). seed_history_session also outputs MITRA_NAME_RE (regex-escaped) for reliable selectors against display names containing regex specials. * mitra_app: dispose-during-deactivate guardrail for back-press on the mitra chat screen after the customer's goodbye message. Pending real emulator repro verification (carried over from 2026-05-15). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import '../../core/availability/mitra_availability_notifier.dart';
|
||||
import '../../core/chat/active_session_notifier.dart';
|
||||
import '../../core/notifications/notif_permission.dart';
|
||||
import '../../core/theme/halo_tokens.dart';
|
||||
import '../payment/state/payment_draft_provider.dart';
|
||||
import 'providers/bestie_history_provider.dart';
|
||||
import 'widgets/bestie_choice_sheet.dart';
|
||||
import 'widgets/halo_tab_bar.dart';
|
||||
@@ -90,6 +91,8 @@ class _HomeScreenState extends ConsumerState<HomeScreen> with WidgetsBindingObse
|
||||
await BestieChoiceSheet.show(context);
|
||||
return;
|
||||
}
|
||||
// explicit reset — this branch is blast-only, clear any stale targeted mitra
|
||||
ref.read(paymentDraftNotifierProvider.notifier).reset();
|
||||
context.push('/payment/entry');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import '../../../core/api/api_client_provider.dart';
|
||||
import '../../../core/constants.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
import '../../chat/widgets/bestie_unavailable_dialog.dart';
|
||||
import '../../payment/state/payment_draft_provider.dart';
|
||||
import '../providers/bestie_history_provider.dart';
|
||||
|
||||
/// `BestieHistoryList` — the picker for the returning-user "curhat lagi"
|
||||
@@ -15,14 +17,19 @@ import '../providers/bestie_history_provider.dart';
|
||||
/// lives in the Chat-tab Selesai sub-tab, not here.
|
||||
///
|
||||
/// Row interaction rules:
|
||||
/// - mitra_is_online + status != closing → tap targets `/payment` with
|
||||
/// `targetedMitraId`, which jumps into the Stage-3.x payment flow and,
|
||||
/// once confirmed, the Stage-5 targeted-wait overlay.
|
||||
/// - mitra_is_online + status != closing → tap stamps the picked mitra
|
||||
/// onto `paymentDraftNotifierProvider` (via `setTargetedMitra`) and
|
||||
/// pushes `/payment/entry`. The Phase-4 multi-screen payment flow
|
||||
/// (entry → method-pick → duration-pick → method → waiting → notif-gate
|
||||
/// → searching) reads the targeting back off the draft to fire the
|
||||
/// returning-chat request after the customer pays.
|
||||
/// - status == closing → tap drops into the chat session screen so the
|
||||
/// user can finish the goodbye composer (one-time grace path).
|
||||
/// - mitra_is_online == false → row is dimmed and tap is disabled. Mermaid
|
||||
/// §4 calls for a Bestie Offline Popup variant here, deferred until
|
||||
/// OfflinePopup gets its returning-user copy.
|
||||
/// - mitra_is_online == false → row stays dimmed for the visual cue, but
|
||||
/// tap is enabled and opens [BestieOfflinePopup] with the
|
||||
/// [BestieOfflineVariant.prePayReturning] variant (Stage 5.3). The popup
|
||||
/// offers "cari bestie lain" (resets the draft + pushes `/payment/entry`
|
||||
/// for a fresh blast-payment flow) and `tanya admin`.
|
||||
class BestieHistoryListScreen extends ConsumerWidget {
|
||||
const BestieHistoryListScreen({super.key});
|
||||
|
||||
@@ -116,12 +123,29 @@ class BestieHistoryListScreen extends ConsumerWidget {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Offline mitra — surface the Stage 5.3 pre-payment
|
||||
// popup. The popup's "cari bestie lain" CTA resets the
|
||||
// draft and pushes `/payment/entry` for a blast flow.
|
||||
if (!item.mitraIsOnline) {
|
||||
// ignore: discarded_futures
|
||||
BestieOfflinePopup.show(
|
||||
context,
|
||||
variant: BestieOfflineVariant.prePayReturning,
|
||||
mitraName: item.mitraName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (item.mitraId == null) return;
|
||||
context.push('/payment', extra: <String, dynamic>{
|
||||
'targetedMitraId': item.mitraId,
|
||||
'mitraName': item.mitraName,
|
||||
'topicSensitivity': TopicSensitivity.regular,
|
||||
});
|
||||
// Stamp the targeted mitra onto the payment draft; the
|
||||
// multi-screen payment flow (entry → method → waiting →
|
||||
// notif-gate → searching) reads it back to fire the
|
||||
// returning-chat request after payment confirms.
|
||||
ref.read(paymentDraftNotifierProvider.notifier)
|
||||
.setTargetedMitra(
|
||||
mitraId: item.mitraId!,
|
||||
mitraName: item.mitraName,
|
||||
);
|
||||
context.push('/payment/entry');
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -156,15 +180,22 @@ class _BestieRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPick = item.mitraIsOnline && !isClosing && item.mitraId != null;
|
||||
// Online + has mitraId → full-colour, taps into targeted-pair payment.
|
||||
// Closing → full-colour, taps into the goodbye composer (mitraId may be
|
||||
// absent but `sessionId` is what the chat route needs).
|
||||
// Offline → dim, but still tappable so [BestieOfflinePopup] can fire the
|
||||
// Stage 5.3 pre-payment branch.
|
||||
final isLive = item.mitraIsOnline && item.mitraId != null;
|
||||
final tappable = isLive || isClosing || !item.mitraIsOnline;
|
||||
final dim = !isLive && !isClosing;
|
||||
return Material(
|
||||
color: HaloTokens.surface,
|
||||
borderRadius: HaloRadius.lg,
|
||||
child: InkWell(
|
||||
onTap: canPick ? onPick : null,
|
||||
onTap: tappable ? onPick : null,
|
||||
borderRadius: HaloRadius.lg,
|
||||
child: Opacity(
|
||||
opacity: canPick ? 1.0 : 0.55,
|
||||
opacity: dim ? 0.55 : 1.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(HaloSpacing.s16),
|
||||
child: Row(
|
||||
@@ -239,7 +270,7 @@ class _BestieRow extends StatelessWidget {
|
||||
'→',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: canPick ? HaloTokens.brand : HaloTokens.inkMuted,
|
||||
color: dim ? HaloTokens.inkMuted : HaloTokens.brand,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
import '../../payment/state/payment_draft_provider.dart';
|
||||
|
||||
/// Phase 4 Stage 8 — Bestie Choice Sheet.
|
||||
///
|
||||
/// Triggered from the home `Mulai Curhat` CTA when the user has at least one
|
||||
/// prior session. Two cards: continue with a known bestie (→ history list)
|
||||
/// vs. find a new bestie (→ soft-prompt + blast).
|
||||
class BestieChoiceSheet extends StatelessWidget {
|
||||
class BestieChoiceSheet extends ConsumerWidget {
|
||||
const BestieChoiceSheet({super.key});
|
||||
|
||||
static Future<void> show(BuildContext context) {
|
||||
@@ -19,7 +21,7 @@ class BestieChoiceSheet extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -60,6 +62,8 @@ class BestieChoiceSheet extends StatelessWidget {
|
||||
subtitle: 'cari bestie baru yang siap dengerin sekarang.',
|
||||
icon: Icons.auto_awesome_outlined,
|
||||
onTap: () {
|
||||
// explicit reset — this branch is blast-only, clear any stale targeted mitra
|
||||
ref.read(paymentDraftNotifierProvider.notifier).reset();
|
||||
Navigator.of(context).pop();
|
||||
context.push('/payment/entry');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user