Phase 3.3: topic sensitivity + Phase 3.4: auth foundation
Phase 3.3 — Session Topic Sensitivity (complete): - Backend: topic_sensitivity column + session_sensitivity_log, sensitivity service (flip with one-way-latch + audit), PATCH /api/shared/chat/sessions/:id/topic, topic carried in pairing + extension WS payloads, CC filter + sensitive stats + per-mitra sensitive columns on activity page - client_app: TopicSelectionBottomSheet before pricing, topic flows through pairing request, silent WS handler for session_topic_updated - mitra_app: SensitivityBadge + SensitivityTheme + sensitivityConfigProvider, overlay badge + yellow accent, chat screen app-bar toggle with configurable confirmation + latch, extension card shows current flag, history + transcript yellow theme - control_center: Sensitivitas Topik settings section, topic filter + column with inline audit log, sensitive stats dashboard card, mitra activity sensitive columns with QC flag Phase 3.4 — Self-Managed Auth (foundation only): - Migration: auth_sessions + otp_requests tables, social identity columns on customers, password_hash + lockout on control_center_users, OTP + CC lockout app_config keys - New services: password (bcrypt + complexity), token (JWT HS256 + refresh rotation, session_id claim pre-wires future Valkey revocation), social-identity (Google + Apple JWKS), OTP (Fazpass stub — real API TBD) - Constants: AuthProvider + OtpChannel - Middleware, auth route rewrites, WS auth update, Firebase → FCM isolation still pending (next chunk); Fazpass docs + Apple Developer setup still required before E2E testing Docs: - requirement/phase3.3.md, phase3.3-plan.md, phase3.3-testing.md - requirement/phase3.4.md, phase3.4-plan.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/chat/mitra_chat_notifier.dart';
|
||||
import '../../../core/chat/extension_notifier.dart';
|
||||
import '../../../core/chat/sensitivity_config_provider.dart';
|
||||
import '../../../core/chat/widgets/sensitivity_badge.dart';
|
||||
import '../../../core/chat/widgets/sensitivity_theme.dart';
|
||||
import '../../../core/constants.dart';
|
||||
|
||||
// Chat theme colors
|
||||
const _kUserBubbleColor = Color(0xFFD4929A);
|
||||
const _kBgTint = Color(0xFFF5D0D6);
|
||||
const _kBannerColor = Color(0xFFC4868F);
|
||||
const _kAccentPink = Color(0xFFBE7C8A);
|
||||
|
||||
@@ -99,6 +101,10 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
}
|
||||
});
|
||||
|
||||
final currentSensitivity = chatState is MitraChatConnectedData
|
||||
? chatState.topicSensitivity
|
||||
: TopicSensitivity.regular;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
@@ -111,6 +117,7 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
),
|
||||
title: Text(widget.customerName),
|
||||
actions: [
|
||||
if (chatState is MitraChatConnectedData) _buildTopicToggle(chatState),
|
||||
if (chatState is MitraChatConnectedData && chatState.remainingSeconds != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
@@ -126,10 +133,120 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody(chatState, extState),
|
||||
body: Column(
|
||||
children: [
|
||||
if (currentSensitivity == TopicSensitivity.sensitive)
|
||||
_buildSensitivityHeader(),
|
||||
Expanded(child: _buildBody(chatState, extState)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSensitivityHeader() {
|
||||
const theme = SensitivityTheme.sensitive;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
color: theme.badgeBg,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 16, color: theme.badgeFg),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Topik sensitif',
|
||||
style: TextStyle(
|
||||
color: theme.badgeFg,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopicToggle(MitraChatConnectedData state) {
|
||||
final configAsync = ref.watch(sensitivityConfigProvider);
|
||||
final config = configAsync.value ?? SensitivityConfig.defaults;
|
||||
final isSensitive = state.topicSensitivity == TopicSensitivity.sensitive;
|
||||
final locked = config.oneWayLatch && isSensitive;
|
||||
|
||||
return Tooltip(
|
||||
message: locked
|
||||
? 'Sesi sudah terkunci sebagai topik sensitif'
|
||||
: isSensitive
|
||||
? 'Tandai sebagai topik umum'
|
||||
: 'Tandai sebagai topik sensitif',
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isSensitive ? Icons.flag : Icons.outlined_flag,
|
||||
color: isSensitive ? SensitivityTheme.sensitive.badgeBg : Colors.grey.shade600,
|
||||
),
|
||||
onPressed: locked ? null : () => _onTopicTogglePressed(state, config),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTopicTogglePressed(
|
||||
MitraChatConnectedData state,
|
||||
SensitivityConfig config,
|
||||
) async {
|
||||
final toValue = state.topicSensitivity == TopicSensitivity.sensitive
|
||||
? TopicSensitivity.regular
|
||||
: TopicSensitivity.sensitive;
|
||||
|
||||
if (config.flipConfirmationEnabled) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(
|
||||
toValue == TopicSensitivity.sensitive
|
||||
? 'Tandai sesi ini sebagai sensitif?'
|
||||
: 'Tandai sesi ini sebagai topik umum?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Tandai'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
}
|
||||
|
||||
final err = await ref
|
||||
.read(mitraChatProvider.notifier)
|
||||
.flipTopic(widget.sessionId, toValue);
|
||||
|
||||
if (!mounted) return;
|
||||
if (err != null) {
|
||||
final msg = err == 'SENSITIVITY_LATCHED'
|
||||
? 'Sesi sudah ditandai sensitif dan tidak bisa dikembalikan.'
|
||||
: err == 'SESSION_NOT_ACTIVE'
|
||||
? 'Sesi sudah berakhir.'
|
||||
: 'Gagal mengubah topik. Coba lagi.';
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
toValue == TopicSensitivity.sensitive
|
||||
? 'Sesi ditandai sensitif'
|
||||
: 'Sesi ditandai topik umum',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBody(MitraChatData chatState, ExtensionData extState) {
|
||||
if (chatState is MitraChatConnectingData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -154,12 +271,14 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
return _buildGoodbyeView(extState);
|
||||
}
|
||||
|
||||
final bgTint = SensitivityTheme.of(state.topicSensitivity).bgTint;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Background pattern
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: _kBgTint,
|
||||
color: bgTint,
|
||||
child: Image.asset(
|
||||
'assets/images/chat_pattern.png',
|
||||
repeat: ImageRepeat.repeat,
|
||||
@@ -328,8 +447,12 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
final duration = request['duration_minutes'] as int?;
|
||||
final extensionId = request['extension_id'] as String?;
|
||||
final isResponding = extState is ExtensionRespondingData;
|
||||
final topic = TopicSensitivity.fromString(request['topic_sensitivity'] as String?);
|
||||
final isSensitive = topic == TopicSensitivity.sensitive;
|
||||
|
||||
return Center(
|
||||
return Container(
|
||||
color: isSensitive ? SensitivityTheme.sensitive.bgTint : null,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
@@ -338,6 +461,10 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
const Icon(Icons.timer, size: 64, color: Colors.orange),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Permintaan Perpanjangan', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
if (isSensitive) ...[
|
||||
const SizedBox(height: 8),
|
||||
SensitivityBadge(sensitivity: topic),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text('Customer ingin perpanjang $duration menit', textAlign: TextAlign.center),
|
||||
const SizedBox(height: 24),
|
||||
@@ -371,6 +498,7 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user