Phase 4 checkpoint: chat-screen perf refactor + retryable blast-failure + repo-wide dispose-ref guardrail
Chat-screen performance (customer + mitra): - Parent screens have zero `ref.watch` — only `ref.listen` for side effects - Body extracted into its own `ConsumerStatefulWidget`; AppBar parts split into narrow `.select` consumers (mode, sensitivity, timer) - Per-second timer ticks routed to dedicated providers (`chatRemainingSecondsProvider` + new `mitraChatRemainingSecondsProvider`) so WS `session_tick` frames don't invalidate the rest of the chat state Dispose-in-ref bug fix: - `home_screen.dart`, `payment_screen.dart`, `mitra_chat_screen.dart` — ref-using cleanup moved from `dispose()` to `deactivate()`. Modern Riverpod invalidates `ref` the moment `dispose()` runs; the resulting silent error corrupts the widget-tree finalize and the next screen appears frozen - `halo_lints` package added at repo root with `no_ref_in_dispose` rule to catch this pattern in CI / IDE analysis - `custom_lint` activated in both apps' `analysis_options.yaml` (was installed but never wired in — also brings `riverpod_lint`'s `avoid_ref_inside_state_dispose` online) - CLAUDE.md Pitfalls section added to client_app + mitra_app Phase 4 §3 retryable blast-failure (Option A): - Backend `expirePairingRequest` + all-rejected use `recordIntermediateFailure` instead of `failPaymentSession` so the payment session stays `confirmed` for re-blast - WS `pairing_failed` payload carries `is_terminal: false` on the retryable paths; client parses the flag and exposes `retryBlast()` - "Coba cari lagi" CTA on S7 Timeout now re-blasts on the same payment - Pairing service test updated to reflect the new semantics Customer waiting-payment screen navigation patch: - `_navigateTerminal` uses `Future.microtask` + `addPostFrameCallback` redundancy after a release-mode bug where polling stopped but `context.go` never fired, leaving the screen visually stuck on "menunggu pembayaran" See requirement/resume-2026-05-15.md for next-day pickup checklist (mitra release rebuild + S21 Ultra install + retest is the gating item). Bundles unrelated in-flight Phase 4 §2.x work that was already on disk (ESP screen removal, USP one-time gate scaffolding, bestie-availability public route, OTP service edits, Maestro flow tweaks) — kept together to avoid a partial-rebase mess. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -191,8 +191,14 @@ class _SearchingBody extends ConsumerWidget {
|
||||
fullWidth: true,
|
||||
size: HaloButtonSize.lg,
|
||||
onPressed: () {
|
||||
ref.read(pairingProvider.notifier).reset();
|
||||
context.go('/payment/entry');
|
||||
final notifier = ref.read(pairingProvider.notifier);
|
||||
final s = state;
|
||||
if (s is PairingFailedData && s.isRetryable) {
|
||||
notifier.retryBlast();
|
||||
} else {
|
||||
notifier.reset();
|
||||
context.go('/payment/entry');
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
|
||||
@@ -1,56 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/halo_button.dart';
|
||||
|
||||
/// Floating banner injected above the chat input bar when the session timer
|
||||
/// has hit zero but the session is still in `closing` grace. Phase 4 Stage 6:
|
||||
/// gives the customer a soft, in-place way to extend instead of the modal-only
|
||||
/// flow from Phase 3.
|
||||
/// Floating expired banner shown above the chat input when the session
|
||||
/// timer has hit zero but the session is still in `closing` grace.
|
||||
///
|
||||
/// Mirrors Figma `v3.jsx::HBChatExpiredBanner` (line 423): brand-pink
|
||||
/// background, white text, `⏰` icon, "habis nih... mau lanjutin curhat
|
||||
/// sama {name}?" copy, white `perpanjang` button.
|
||||
class ChatExpiredBanner extends StatelessWidget {
|
||||
final String mitraName;
|
||||
final VoidCallback onExtend;
|
||||
|
||||
const ChatExpiredBanner({super.key, required this.onExtend});
|
||||
const ChatExpiredBanner({
|
||||
super.key,
|
||||
required this.mitraName,
|
||||
required this.onExtend,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(
|
||||
HaloSpacing.s12,
|
||||
HaloSpacing.s8,
|
||||
HaloSpacing.s12,
|
||||
HaloSpacing.s8,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
HaloSpacing.s16,
|
||||
HaloSpacing.s12,
|
||||
HaloSpacing.s12,
|
||||
HaloSpacing.s12,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: HaloTokens.danger,
|
||||
borderRadius: HaloRadius.lg,
|
||||
boxShadow: HaloShadows.card,
|
||||
margin: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: HaloTokens.brand,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: HaloTokens.brand.withValues(alpha: 0.31),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('⏰', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: HaloSpacing.s12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'waktu curhat habis',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
const Text('⏰', style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 12.5,
|
||||
height: 1.4,
|
||||
color: Colors.white,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'habis nih...',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(text: ' mau lanjutin curhat sama $mitraName?'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
HaloButton(
|
||||
label: 'perpanjang',
|
||||
size: HaloButtonSize.sm,
|
||||
variant: HaloButtonVariant.secondary,
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: onExtend,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: HaloTokens.brand,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: const Size(0, 32),
|
||||
elevation: 0,
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
child: const Text('perpanjang'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user