Customer end-of-session (figma §6):
- PricingBottomSheet: ghost "cukup, akhiri sesi" CTA + dedup divider
- chat_screen._runEndSessionFlow chains ConfirmEndStep1 → ConfirmEndStep2
→ ClosingMessageSheet (or "lewati saja" → close + /home). The four
popup/sheet widgets already existed; this commit just wires them
- showModalBottomSheet: showDragHandle=false to suppress the Material 3
auto-injected handle that was stacking with our own pill
Notification sound on API 33+:
- Bump channel halobestie_chat_v1 → halobestie_chat_v2, created from
native Kotlin in MainActivity.kt with AudioAttributes contentType
CONTENT_TYPE_SONIFICATION. flutter_local_notifications' default of
CONTENT_TYPE_UNKNOWN was causing Android 13 to silently drop audio
focus while the notification still posted (isNoisy=true). Both apps
- Backend FCM payload channelId updated to v2
- AndroidManifest meta-data: default_notification_icon + color → brand
silhouette tinted pink instead of generic Android bell. Both apps
Customer pairing reliability:
- pairing_notifier: applyPairedFromPush({sessionId, mitraName}) unsticks
searching screen when WS push failed and FCM/active-session-poll is
the first signal. Idempotent across PairingSearchingData,
PairingTargetedWaitingData, PairingErrorData (covers ALREADY_ACTIVE)
- notification_service: dispatches every FCM data payload to an
onDataMessage callback (foreground + tap + cold-start). main.dart
wires that to applyPairedFromPush on type=='paired'. Foreground
'paired' no longer renders a local banner — screen self-advances
- main.dart activeSession listener also calls applyPairedFromPush when
a session appears server-side while pairing is in a waiting state.
Covers stale ALREADY_ACTIVE recovery without a full page refresh
Auth refresh token race:
- auth_notifier._refreshFromStorage shares a single in-flight Future
across all callers (Auth.build + 401-retry path). Backend rotates
refresh tokens, so concurrent callers using the same stored token
would race → loser 401s → catch wipes flutter_secure_storage → user
appears logged out after kill+reopen
Polish:
- method_pick_screen: resizeToAvoidBottomInset=false — prevents the
one-frame overflow when entering with the previous screen's keyboard
still animating out
- bestie_history: BestieHistoryItem now carries `status` (backend
already returns it). Removed _rawHistoryProvider that fetched the
same endpoint just to read status; the two providers could go out
of sync mid-rebuild and throw RangeError(length) on indexing
Xendit Stage 8 (carried from WIP):
- xendit_checkout_screen: embedded webview hosting Xendit's invoice
page (intercepts halobestie:// deeplink + return-page URLs for
deterministic pop)
- waiting_payment_screen: auto-pushes the webview when the backend
payload includes xendit_invoice_url; spinner card + "Buka ulang
halaman pembayaran" CTA for the QR-fallback path
- pubspec: webview_flutter ^4.13.0
Maestro infra:
- subflows/onboarding_returning_user: drop the "Mulai" carousel wait
(splash auto-advances since 2026-05-26); tap phone-field hint
instead of point; drop hideKeyboard (sends BACK → /home when the
IME isn't actually up)
- New flow ts-customer-06-01-end_session_via_timeup_sheet: drives
the full path to the chat-expired banner. Last step blocked by a
Maestro+Flutter gesture quirk on the perpanjang ElevatedButton
(raw `adb input tap` works at the same coords). Documented in
memory; deeplink fixture or manual verify recommended
- ChatExpiredBanner button wrapped with Semantics(identifier:
'chat_extend_button', button: true, onTap: …) — good hygiene for
future tests even though it doesn't fix the dadb tap issue
.dev/: tracked wsl_emulator_bridge.ps1 + wsl_tcp_relay.py for
Maestro-on-WSL setup (Windows-side netsh portproxy + WSL-side
loopback relays). Both referenced from existing CLAUDE.md notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.4 KiB
Dart
95 lines
3.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../core/theme/halo_tokens.dart';
|
|
|
|
/// 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.mitraName,
|
|
required this.onExtend,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
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: 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?'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Semantics wrapper exposes the button via Android's resource-id so
|
|
// maestro can find it via `id:` selector AND triggers onExtend
|
|
// directly on a11y CLICK action (instead of synthesized coord-tap
|
|
// which doesn't reliably fire this ElevatedButton's onPressed —
|
|
// confirmed: raw `adb input tap` works, maestro's tap does not).
|
|
// `onTap: onExtend` makes the Semantics node itself clickable so
|
|
// Android's UiAutomator routes node.performAction(ACTION_CLICK)
|
|
// → onExtend without needing the child button's gesture detector.
|
|
Semantics(
|
|
identifier: 'chat_extend_button',
|
|
button: true,
|
|
onTap: onExtend,
|
|
child: 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'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|