Add account UI and secure session storage

This commit is contained in:
Hamza Ayed
2026-10-03 00:54:16 +03:00
parent b0cd825838
commit f745bed0f7
14 changed files with 456 additions and 13 deletions
@@ -423,6 +423,28 @@ class ChatCubit extends Cubit<ChatState> {
}
}
Future<void> reloadForAccountChange() async {
if (state.isSending) return;
emit(
state.copyWith(
messages: const [],
conversations: const [],
clearActiveConversation: true,
isLoadingHistory: true,
isWorkspaceMode: false,
clearSelectedWorkspacePath: true,
availableWorkspaceFiles: const [],
selectedWorkspaceFiles: const [],
clearSelectedSkill: true,
requestStatus: RequestStatus.idle,
clearAgentProgress: true,
clearPendingFileChangeProposal: true,
clearError: true,
),
);
await Future.wait([_loadHistory(), _loadAgentSkills()]);
}
Future<void> _saveCurrent(List<ChatMessage> messages) async {
if (messages.isEmpty) return;
final firstQuestion = messages.firstWhere(
@@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../../audio/presentation/cubit/voice_cubit.dart';
import '../../../audio/presentation/cubit/voice_state.dart';
import '../../../../core/notifications/completion_notification_service.dart';
import '../../../../core/network/api_repository.dart';
import '../cubit/chat_cubit.dart';
import '../cubit/chat_state.dart';
@@ -423,6 +424,232 @@ class _ChatPageState extends State<ChatPage> {
controller.dispose();
}
Future<void> _manageAccount() async {
final cubit = context.read<ChatCubit>();
final api = context.read<ApiRepository>();
if (cubit.state.isSending) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('انتظر حتى ينتهي الطلب قبل تبديل الحساب.'),
),
);
return;
}
if (api.accountEmail != null) {
final confirmed = await showDialog<bool>(
context: context,
builder:
(context) => Directionality(
textDirection: TextDirection.rtl,
child: AlertDialog(
title: const Text('تسجيل الخروج'),
content: Text('هل تريد تسجيل الخروج من ${api.accountEmail}؟'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('إلغاء'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('تسجيل الخروج'),
),
],
),
),
);
if (confirmed != true) return;
try {
await api.logout();
await cubit.reloadForAccountChange();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('تم تسجيل الخروج والعودة إلى الحساب المحلي.'),
),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString().replaceFirst('Exception: ', '')),
),
);
}
return;
}
final emailController = TextEditingController();
final passwordController = TextEditingController();
var register = false;
var validationError = '';
final credentials = await showDialog<(bool, String, String)>(
context: context,
builder:
(dialogContext) => StatefulBuilder(
builder:
(context, setDialogState) => Directionality(
textDirection: TextDirection.rtl,
child: AlertDialog(
title: Text(register ? 'إنشاء حساب' : 'تسجيل الدخول'),
content: SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: emailController,
autofocus: true,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'البريد الإلكتروني',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: passwordController,
obscureText: true,
autofillHints: [
register
? AutofillHints.newPassword
: AutofillHints.password,
],
onSubmitted: (_) {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
setDialogState(
() =>
validationError =
'أدخل البريد وكلمة المرور.',
);
} else if (register && password.length < 12) {
setDialogState(
() =>
validationError =
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
);
} else {
Navigator.pop(dialogContext, (
register,
email,
password,
));
}
},
decoration: const InputDecoration(
labelText: 'كلمة المرور',
border: OutlineInputBorder(),
),
),
if (register) ...[
const SizedBox(height: 8),
const Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
'استخدم 12 محرفًا على الأقل.',
style: TextStyle(fontSize: 12),
),
),
],
if (validationError.isNotEmpty) ...[
const SizedBox(height: 8),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
validationError,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
TextButton(
onPressed:
() => setDialogState(() {
register = !register;
validationError = '';
}),
child: Text(
register
? 'لديك حساب؟ سجّل الدخول'
: 'ليس لديك حساب؟ أنشئ حسابًا',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء'),
),
FilledButton(
onPressed: () {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
setDialogState(
() =>
validationError = 'أدخل البريد وكلمة المرور.',
);
} else if (register && password.length < 12) {
setDialogState(
() =>
validationError =
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
);
} else {
Navigator.pop(dialogContext, (
register,
email,
password,
));
}
},
child: Text(register ? 'إنشاء الحساب' : 'دخول'),
),
],
),
),
),
);
emailController.dispose();
passwordController.dispose();
if (credentials == null) return;
try {
if (credentials.$1) {
await api.registerAccount(
email: credentials.$2,
password: credentials.$3,
);
} else {
await api.login(email: credentials.$2, password: credentials.$3);
}
await cubit.reloadForAccountChange();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
credentials.$1
? 'تم إنشاء الحساب وتخزين الجلسة بأمان.'
: 'تم تسجيل الدخول وتخزين الجلسة بأمان.',
),
),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString().replaceFirst('Exception: ', '')),
),
);
}
}
Future<void> _toggleRecording(VoiceState voice) async {
final cubit = context.read<VoiceCubit>();
if (voice.status == VoiceStatus.recording) {
@@ -1081,6 +1308,18 @@ class _ChatPageState extends State<ChatPage> {
),
),
SizedBox(width: compact ? 2 : 8),
IconButton(
onPressed: _manageAccount,
tooltip:
context.read<ApiRepository>().accountEmail == null
? 'تسجيل الدخول أو إنشاء حساب'
: 'الحساب: ${context.read<ApiRepository>().accountEmail} · تسجيل الخروج',
color:
context.read<ApiRepository>().accountEmail == null
? _ink
: _green,
icon: const Icon(Icons.account_circle_outlined),
),
IconButton(
onPressed: _settings,
tooltip: 'إعداد الاتصال',