Customer chat screen:
- Voice-call header pill (mode == 'call' renders accent-colored pill;
chat mode renders no pill).
- HaloSnackbar fires once per session at 180s remaining ('sisa 3 menit
lagi ya 🤍'), driven by the backend session_warning WS event.
- Last-2-min danger styling: timer pill flips to HaloTokens.danger +
bold JetBrainsMono when remaining <= 120s.
- Floating ChatExpiredBanner widget injected above the input bar when
remaining hits 0 in closing-grace state. perpanjang -> existing
pricing bottom sheet.
- pricing_bottom_sheet.dart rewritten to the 5-option layout with
chat|call mode toggle (mirrors duration-pick from Stage 3).
Mitra chat screen: voice-call header pill only (no countdown UX per PRD).
Backend:
- session.service.js getSessionById JOINs payment_sessions so mode +
expires_at ship in /api/shared/chat/:id/info.
- session-timer.service.js onThreeMinuteWarning now emits expires_at +
remaining_seconds for client resync.
- Dev-only POST /internal/_test/force-session-expires-at clears the
3-min flag, reschedules the timer, and broadcasts WS resync. Lets
the Maestro flow drive 175s -> 90s -> 0s without waiting live.
New chatRemainingSeconds StreamProvider derived from expiresAt, fed by
session_warning / session_timer / session_expired resync messages
(plan referenced a secondsLeftProvider that didn't actually exist).
Maestro 06_chat_countdown.yaml + force_session_expires_at.js helper.
Out of scope: meet.google.com URL launching - url_launcher isn't a
client_app dependency and message bubbles render plain Text. Defer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
1.7 KiB
Dart
60 lines
1.7 KiB
Dart
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.
|
|
class ChatExpiredBanner extends StatelessWidget {
|
|
final VoidCallback onExtend;
|
|
|
|
const ChatExpiredBanner({super.key, 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,
|
|
),
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
HaloButton(
|
|
label: 'perpanjang',
|
|
size: HaloButtonSize.sm,
|
|
variant: HaloButtonVariant.secondary,
|
|
onPressed: onExtend,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|