Mitra chat: real customer name + ticking timer + goodbye pill

Four small fixes on the mitra chat screen, all surfacing through the
chat connected-state.

1. AppBar customer name. The hardcoded "Customer" only ever came from
   the FCM-tap navigation fallback (notification_service:
   `extra: {'customerName': 'Customer'}`); the popup-overlay path passes
   the real name but FCM had no way to know it. /chat/:sessionId/info
   already returns `customer_display_name` — propagate it into
   MitraChatConnectedData and read in the AppBar via .select. Falls
   back to the route arg for the brief connecting window.

2. SISA WAKTU stuck at --:--. The pill watches a remaining-seconds
   provider that's only updated by backend WS frames. Backend only
   fires session_timer at 3-min + 1-min warnings + expiry, so the pill
   sat at --:-- for the first ~7 minutes of a 10-min chat. Added a
   local 1s ticker in the notifier that drives the provider against
   expires_at (also pulled from /info). WS warning frames still
   overwrite normally on top.

3. Pesan Penutup textbox. Replaced the rounded-rect OutlineInputBorder
   field with a fixed-height Container pill whose border matches the
   previous "shadow" tone (HaloTokens.border). Pill borderRadius is
   the full 100 (was 12).

4. Goodbye textbox text was top-aligned because maxLines: 3 +
   OutlineInputBorder left vertical alignment to InputDecoration's
   built-in padding. Switched to maxLines: 1 + textAlignVertical:
   center + isCollapsed: true inside the fixed-height container —
   text now sits on the vertical center.

Bonus: the goodbye subhead "Tuliskan pesan terakhirmu untuk Customer"
also picked up the real name ("…untuk Andi Pratama").

Verified end-to-end on emulator-5556 (TestMitra-1501 + customer
"Andi Pratama"): AppBar shows Andi Pratama, SISA WAKTU ticks (04:57 →
04:35 across screenshots), goodbye pill renders with centered hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:09:00 +08:00
parent a8c20d929e
commit 82c9b1eee8
2 changed files with 105 additions and 19 deletions

View File

@@ -147,16 +147,7 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.customerName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: HaloTokens.ink,
height: 1.2,
),
),
_MitraChatTitleName(fallback: widget.customerName),
const _MitraChatSubtitle(),
],
),
@@ -186,6 +177,35 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
/// rebuilds this widget — only an actual mode flip or expiry transition does.
/// Mirrors `BestieChatV5` (figma-bestie/.../v5.jsx:239): "sesi aktif · Chat",
/// "sesi aktif · Voice", or "sesi berakhir" (color #A8410E when ended).
/// AppBar customer-name line. Reads customerDisplayName from the chat
/// connected-state via `.select` so the parent AppBar doesn't rebuild on
/// every chat-state change. Falls back to the route-arg `fallback` value
/// (which is "Customer" when reached via FCM tap — see notification_service
/// `_navigateFromMessage`).
class _MitraChatTitleName extends ConsumerWidget {
final String fallback;
const _MitraChatTitleName({required this.fallback});
@override
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(mitraChatProvider.select((s) {
if (s is MitraChatConnectedData) return s.customerDisplayName;
return null;
}));
final displayed = (name != null && name.isNotEmpty) ? name : fallback;
return Text(
displayed,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: HaloTokens.ink,
height: 1.2,
),
);
}
}
class _MitraChatSubtitle extends ConsumerWidget {
const _MitraChatSubtitle();
@@ -426,7 +446,7 @@ class _MitraChatBodyContentState extends ConsumerState<_MitraChatBodyContent> {
final showGoodbye = !state.goodbyeSubmitted &&
(state.sessionClosing || extState is ExtensionShowGoodbyeData || extState is ExtensionSubmittingData);
if (showGoodbye) {
return _buildGoodbyeView(extState);
return _buildGoodbyeView(extState, state.customerDisplayName);
}
if (state.sessionClosing && state.goodbyeSubmitted) {
@@ -851,7 +871,10 @@ class _MitraChatBodyContentState extends ConsumerState<_MitraChatBodyContent> {
);
}
Widget _buildGoodbyeView(ExtensionData extState) {
Widget _buildGoodbyeView(ExtensionData extState, String? customerDisplayName) {
final name = (customerDisplayName != null && customerDisplayName.isNotEmpty)
? customerDisplayName
: widget.customerName;
return SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Column(
@@ -861,14 +884,33 @@ class _MitraChatBodyContentState extends ConsumerState<_MitraChatBodyContent> {
const SizedBox(height: 16),
const Text('Pesan Penutup', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Tuliskan pesan terakhirmu untuk Customer', textAlign: TextAlign.center),
Text('Tuliskan pesan terakhirmu untuk $name', textAlign: TextAlign.center),
const SizedBox(height: 24),
TextField(
controller: widget.goodbyeController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Terima kasih sudah curhat...',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
// Pill-shaped, single-line text field with the border color picked
// up from the previous "shadow" tone (HaloTokens.border). Container
// fixes the height so textAlignVertical can center the entry.
Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: HaloTokens.surface,
borderRadius: BorderRadius.circular(100),
border: Border.all(color: HaloTokens.border, width: 1),
),
child: TextField(
controller: widget.goodbyeController,
maxLines: 1,
textAlignVertical: TextAlignVertical.center,
style: const TextStyle(fontSize: 14, color: HaloTokens.ink),
decoration: const InputDecoration(
hintText: 'Terima kasih sudah curhat...',
hintStyle: TextStyle(color: HaloTokens.inkMuted, fontSize: 14),
isCollapsed: true,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
),
),
),
const SizedBox(height: 16),