diff --git a/hesabixAPI/adapters/api/v1/auth.py b/hesabixAPI/adapters/api/v1/auth.py index 7104ef8..b46050d 100644 --- a/hesabixAPI/adapters/api/v1/auth.py +++ b/hesabixAPI/adapters/api/v1/auth.py @@ -26,7 +26,7 @@ def generate_captcha(db: Session = Depends(get_db)) -> dict: @router.post("/register", summary="Register new user") -def register(payload: RegisterRequest, db: Session = Depends(get_db)) -> dict: +def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)) -> dict: user_id = register_user( db=db, first_name=payload.first_name, @@ -37,7 +37,16 @@ def register(payload: RegisterRequest, db: Session = Depends(get_db)) -> dict: captcha_id=payload.captcha_id, captcha_code=payload.captcha_code, ) - return success_response({"user_id": user_id}) + # Create a session api key similar to login + user_agent = request.headers.get("User-Agent") + ip = request.client.host if request.client else None + from app.core.security import generate_api_key + from adapters.db.repositories.api_key_repo import ApiKeyRepository + api_key, key_hash = generate_api_key() + api_repo = ApiKeyRepository(db) + api_repo.create_session_key(user_id=user_id, key_hash=key_hash, device_id=payload.device_id, user_agent=user_agent, ip=ip, expires_at=None) + user = {"id": user_id, "first_name": payload.first_name, "last_name": payload.last_name, "email": payload.email, "mobile": payload.mobile} + return success_response({"api_key": api_key, "expires_at": None, "user": user}) @router.post("/login", summary="Login with email or mobile") diff --git a/hesabixAPI/adapters/api/v1/schemas.py b/hesabixAPI/adapters/api/v1/schemas.py index a3a1471..ed2e980 100644 --- a/hesabixAPI/adapters/api/v1/schemas.py +++ b/hesabixAPI/adapters/api/v1/schemas.py @@ -14,6 +14,7 @@ class RegisterRequest(CaptchaSolve): email: EmailStr | None = None mobile: str | None = Field(default=None, max_length=32) password: str = Field(..., min_length=8, max_length=128) + device_id: str | None = Field(default=None, max_length=100) class LoginRequest(CaptchaSolve): diff --git a/hesabixAPI/app/core/error_handlers.py b/hesabixAPI/app/core/error_handlers.py index 6f12dc0..e10ba57 100644 --- a/hesabixAPI/app/core/error_handlers.py +++ b/hesabixAPI/app/core/error_handlers.py @@ -16,6 +16,7 @@ def _translate_validation_error(request: Request, exc: RequestValidationError) - content={"success": False, "error": {"code": "VALIDATION_ERROR", "message": "Validation error", "details": exc.errors()}}, ) + # translated details details: list[dict[str, Any]] = [] for err in exc.errors(): type_ = err.get("type") @@ -23,6 +24,13 @@ def _translate_validation_error(request: Request, exc: RequestValidationError) - ctx = err.get("ctx", {}) or {} msg = err.get("msg", "") + # extract field name (skip body/query/path) + field_name = None + if isinstance(loc, (list, tuple)): + for part in loc: + if str(part) not in ("body", "query", "path"): + field_name = str(part) + if type_ == "string_too_short": msg = translator.t("STRING_TOO_SHORT") min_len = ctx.get("min_length") @@ -35,7 +43,12 @@ def _translate_validation_error(request: Request, exc: RequestValidationError) - msg = f"{msg} (حداکثر {max_len})" elif type_ in {"missing", "value_error.missing"}: msg = translator.t("FIELD_REQUIRED") - elif type_ in {"value_error.email", "email", "value_error.email"}: + # broader email detection + elif ( + type_ in {"value_error.email", "email"} + or (field_name == "email" and isinstance(type_, str) and type_.startswith("value_error")) + or (isinstance(msg, str) and "email address" in msg.lower()) + ): msg = translator.t("INVALID_EMAIL") details.append({"loc": loc, "msg": msg, "type": type_}) diff --git a/hesabixUI/hesabix_ui/lib/core/auth_store.dart b/hesabixUI/hesabix_ui/lib/core/auth_store.dart index 8dd2dcb..32fc696 100644 --- a/hesabixUI/hesabix_ui/lib/core/auth_store.dart +++ b/hesabixUI/hesabix_ui/lib/core/auth_store.dart @@ -35,13 +35,21 @@ class AuthStore with ChangeNotifier { final prefs = await SharedPreferences.getInstance(); _apiKey = key; if (key == null) { - await _secure.delete(key: _kApiKey); - await prefs.remove(_kApiKey); + if (kIsWeb) { + await prefs.remove(_kApiKey); + } else { + try { + await _secure.delete(key: _kApiKey); + } catch (_) {} + await prefs.remove(_kApiKey); + } } else { if (kIsWeb) { await prefs.setString(_kApiKey, key); } else { - await _secure.write(key: _kApiKey, value: key); + try { + await _secure.write(key: _kApiKey, value: key); + } catch (_) {} await prefs.setString(_kApiKey, key); } } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index ff9a166..bbbe85a 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -36,5 +36,17 @@ "resetFailed": "Request failed. Please try again." , "fixFormErrors": "Please fix the form errors." + , + "dashboard": "Dashboard", + "profile": "Profile", + "settings": "Settings", + "logout": "Logout", + "logoutDone": "Signed out.", + "logoutConfirmTitle": "Sign out", + "logoutConfirmMessage": "Are you sure you want to sign out?", + "menu": "Menu" + , + "ok": "OK", + "cancel": "Cancel" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index 90da8fb..f2301fe 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -36,5 +36,17 @@ "resetFailed": "ارسال کد بازیابی ناموفق بود. لطفاً دوباره تلاش کنید." , "fixFormErrors": "لطفاً خطاهای فرم را برطرف کنید." + , + "dashboard": "داشبورد", + "profile": "پروفایل", + "settings": "تنظیمات", + "logout": "خروج", + "logoutDone": "خروج انجام شد", + "logoutConfirmTitle": "تایید خروج", + "logoutConfirmMessage": "آیا برای خروج مطمئن هستید؟", + "menu": "منو" + , + "ok": "تایید", + "cancel": "انصراف" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index 1e1f526..dab86a5 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -289,6 +289,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Please fix the form errors.'** String get fixFormErrors; + + /// No description provided for @dashboard. + /// + /// In en, this message translates to: + /// **'Dashboard'** + String get dashboard; + + /// No description provided for @profile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get profile; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @logoutDone. + /// + /// In en, this message translates to: + /// **'Signed out.'** + String get logoutDone; + + /// No description provided for @logoutConfirmTitle. + /// + /// In en, this message translates to: + /// **'Sign out'** + String get logoutConfirmTitle; + + /// No description provided for @logoutConfirmMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to sign out?'** + String get logoutConfirmMessage; + + /// No description provided for @menu. + /// + /// In en, this message translates to: + /// **'Menu'** + String get menu; + + /// No description provided for @ok. + String get ok; + + /// No description provided for @cancel. + String get cancel; } class _AppLocalizationsDelegate diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index ad4d8c0..73baac5 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -105,4 +105,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get fixFormErrors => 'Please fix the form errors.'; + + @override + String get dashboard => 'Dashboard'; + + @override + String get profile => 'Profile'; + + @override + String get settings => 'Settings'; + + @override + String get logout => 'Logout'; + + @override + String get logoutDone => 'Signed out.'; + + @override + String get logoutConfirmTitle => 'Sign out'; + + @override + String get logoutConfirmMessage => 'Are you sure you want to sign out?'; + + @override + String get menu => 'Menu'; + + @override + String get ok => 'OK'; + + @override + String get cancel => 'Cancel'; } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index 38bcb2c..39e4351 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -105,4 +105,34 @@ class AppLocalizationsFa extends AppLocalizations { @override String get fixFormErrors => 'لطفاً خطاهای فرم را برطرف کنید.'; + + @override + String get dashboard => 'داشبورد'; + + @override + String get profile => 'پروفایل'; + + @override + String get settings => 'تنظیمات'; + + @override + String get logout => 'خروج'; + + @override + String get logoutDone => 'خروج انجام شد'; + + @override + String get logoutConfirmTitle => 'تایید خروج'; + + @override + String get logoutConfirmMessage => 'آیا برای خروج مطمئن هستید؟'; + + @override + String get menu => 'منو'; + + @override + String get ok => 'تایید'; + + @override + String get cancel => 'انصراف'; } diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart index 6411a6c..2b8f697 100644 --- a/hesabixUI/hesabix_ui/lib/main.dart +++ b/hesabixUI/hesabix_ui/lib/main.dart @@ -80,6 +80,13 @@ class _MyAppState extends State { final router = GoRouter( initialLocation: '/login', + redirect: (context, state) { + final hasKey = _authStore!.apiKey != null && _authStore!.apiKey!.isNotEmpty; + final loggingIn = state.matchedLocation == '/login'; + if (!hasKey && !loggingIn) return '/login'; + if (hasKey && loggingIn) return '/user/profile/dashboard'; + return null; + }, routes: [ GoRoute( path: '/login', @@ -99,7 +106,7 @@ class _MyAppState extends State { ), ), ShellRoute( - builder: (context, state, child) => ProfileShell(child: child), + builder: (context, state, child) => ProfileShell(child: child, authStore: _authStore!, localeController: controller, themeController: themeController), routes: [ GoRoute( path: '/user/profile/dashboard', diff --git a/hesabixUI/hesabix_ui/lib/pages/login_page.dart b/hesabixUI/hesabix_ui/lib/pages/login_page.dart index c5b9394..a3ea5d3 100644 --- a/hesabixUI/hesabix_ui/lib/pages/login_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/login_page.dart @@ -76,33 +76,47 @@ class _LoginPageState extends State with SingleTickerProviderStateMix } Future _refreshCaptcha(String scope) async { - final api = ApiClient(); - final res = await api.post>('/api/v1/auth/captcha'); - final data = res.data!['data'] as Map; - final id = data['captcha_id'] as String; - final imgB64 = data['image_base64'] as String; - final bytes = base64Decode(imgB64); - final ttl = (data['ttl_seconds'] as num?)?.toInt(); - setState(() { - if (scope == 'login') _loginCaptchaId = id; - if (scope == 'register') _registerCaptchaId = id; - if (scope == 'forgot') _forgotCaptchaId = id; - if (scope == 'login') _loginCaptchaImage = bytes; - if (scope == 'register') _registerCaptchaImage = bytes; - if (scope == 'forgot') _forgotCaptchaImage = bytes; - }); - if (ttl != null && ttl > 0) { - final delay = Duration(seconds: ttl); - if (scope == 'login') { - _loginCaptchaTimer?.cancel(); - _loginCaptchaTimer = Timer(delay, () => _refreshCaptcha('login')); - } else if (scope == 'register') { - _registerCaptchaTimer?.cancel(); - _registerCaptchaTimer = Timer(delay, () => _refreshCaptcha('register')); - } else if (scope == 'forgot') { - _forgotCaptchaTimer?.cancel(); - _forgotCaptchaTimer = Timer(delay, () => _refreshCaptcha('forgot')); + try { + final api = ApiClient(); + final res = await api.post>('/api/v1/auth/captcha'); + final body = res.data; + if (body is! Map) return; + final data = body['data']; + if (data is! Map) return; + final String? id = data['captcha_id'] as String?; + final String? imgB64 = data['image_base64'] as String?; + final int? ttl = (data['ttl_seconds'] as num?)?.toInt(); + if (id == null || imgB64 == null) return; + Uint8List bytes; + try { + bytes = base64Decode(imgB64); + } catch (_) { + return; } + if (!mounted) return; + setState(() { + if (scope == 'login') _loginCaptchaId = id; + if (scope == 'register') _registerCaptchaId = id; + if (scope == 'forgot') _forgotCaptchaId = id; + if (scope == 'login') _loginCaptchaImage = bytes; + if (scope == 'register') _registerCaptchaImage = bytes; + if (scope == 'forgot') _forgotCaptchaImage = bytes; + }); + if (ttl != null && ttl > 0) { + final delay = Duration(seconds: ttl); + if (scope == 'login') { + _loginCaptchaTimer?.cancel(); + _loginCaptchaTimer = Timer(delay, () => _refreshCaptcha('login')); + } else if (scope == 'register') { + _registerCaptchaTimer?.cancel(); + _registerCaptchaTimer = Timer(delay, () => _refreshCaptcha('register')); + } else if (scope == 'forgot') { + _forgotCaptchaTimer?.cancel(); + _forgotCaptchaTimer = Timer(delay, () => _refreshCaptcha('forgot')); + } + } + } catch (_) { + // سکوت: خطای شبکه/شکل پاسخ نباید باعث کرش شود } } @@ -119,18 +133,63 @@ class _LoginPageState extends State with SingleTickerProviderStateMix try { if (e is DioException) { final data = e.response?.data; - if (data is Map && data['error'] is Map && data['error']['message'] is String) { - return data['error']['message'] as String; - } - if (data is Map && data['detail'] is List) { - final details = data['detail'] as List; - if (details.isNotEmpty && details.first is Map && (details.first as Map)['msg'] is String) { - return (details.first as Map)['msg'] as String; + if (data is Map) { + final err = data['error'] is Map ? data['error'] as Map : null; + List? details; + if (err != null && err['details'] is List) { + details = err['details'] as List; + } else if (data['detail'] is List) { + details = data['detail'] as List; + } + if (details != null && details.isNotEmpty) { + final parts = []; + for (final item in details) { + if (item is Map) { + final fieldRaw = (item['field'] ?? (item['loc'] is List ? (item['loc'] as List).isNotEmpty ? (item['loc'] as List).last?.toString() : null : null))?.toString(); + final String? message = (item['message'] ?? item['msg'])?.toString(); + String label = ''; + switch (fieldRaw) { + case 'password': + label = t.password; + break; + case 'email': + label = t.email; + break; + case 'mobile': + label = t.mobile; + break; + case 'first_name': + label = t.firstName; + break; + case 'last_name': + label = t.lastName; + break; + case 'captcha': + case 'captcha_code': + label = t.captcha; + break; + case 'identifier': + label = t.identifier; + break; + default: + label = fieldRaw ?? ''; + } + if (message != null && message.isNotEmpty) { + parts.add(label.isNotEmpty ? '$label: $message' : message); + } + } + } + if (parts.isNotEmpty) { + return parts.join('\n'); + } + } + if (err != null && err['message'] is String) { + return err['message'] as String; } } } } catch (_) {} - return t.loginFailed; + return ''; } void _showSnack(String message) { @@ -165,15 +224,20 @@ class _LoginPageState extends State with SingleTickerProviderStateMix 'device_id': widget.authStore.deviceId, }, ); - final data = res.data?['data'] as Map?; - final apiKey = data?['api_key'] as String?; + Map? data; + final body = res.data; + if (body is Map) { + final inner = body['data']; + if (inner is Map) data = inner; + } + final apiKey = data != null ? data['api_key'] as String? : null; if (apiKey != null && apiKey.isNotEmpty) { await widget.authStore.saveApiKey(apiKey); } if (!mounted) return; _showSnack(t.homeWelcome); - context.go('/'); + context.go('/user/profile/dashboard'); } catch (e) { final msg = _extractErrorMessage(e, AppLocalizations.of(context)); _showSnack(msg); @@ -219,7 +283,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix setState(() => _loadingRegister = true); try { final api = ApiClient(); - await api.post>( + final res = await api.post>( '/api/v1/auth/register', data: { 'first_name': _firstNameCtrl.text.trim(), @@ -229,12 +293,26 @@ class _LoginPageState extends State with SingleTickerProviderStateMix 'password': _registerPasswordCtrl.text, 'captcha_id': _registerCaptchaId, 'captcha_code': _registerCaptchaCtrl.text.trim(), + 'device_id': widget.authStore.deviceId, }, ); if (!mounted) return; - _showSnack(t.registerSuccess); - DefaultTabController.of(context).animateTo(0); + Map? data; + final body = res.data; + if (body is Map) { + final inner = body['data']; + if (inner is Map) data = inner; + } + final apiKey = data != null ? data['api_key'] as String? : null; + if (apiKey != null && apiKey.isNotEmpty) { + await widget.authStore.saveApiKey(apiKey); + _showSnack(t.registerSuccess); + context.go('/user/profile/dashboard'); + } else { + _showSnack(t.registerSuccess); + context.go('/user/profile/dashboard'); + } } catch (e) { if (!mounted) return; final msg = _extractErrorMessage(e, AppLocalizations.of(context)); diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart index 84e93a0..2630b15 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart @@ -1,27 +1,189 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../core/auth_store.dart'; +import '../../core/locale_controller.dart'; +import '../../theme/theme_controller.dart'; +import '../../widgets/language_switcher.dart'; +import '../../widgets/theme_mode_switcher.dart'; +import '../../widgets/logout_button.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; class ProfileShell extends StatelessWidget { final Widget child; - const ProfileShell({super.key, required this.child}); + final AuthStore authStore; + final LocaleController? localeController; + final ThemeController? themeController; + const ProfileShell({super.key, required this.child, required this.authStore, this.localeController, this.themeController}); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Profile')), - body: Row( + final width = MediaQuery.of(context).size.width; + final bool useRail = width >= 700; + final bool railExtended = width >= 1100; + final ColorScheme scheme = Theme.of(context).colorScheme; + final String location = GoRouterState.of(context).uri.toString(); + + final t = AppLocalizations.of(context); + final destinations = <_Dest>[ + _Dest(t.dashboard, Icons.dashboard_outlined, Icons.dashboard, '/user/profile/dashboard'), + ]; + + int selectedIndex = 0; + for (int i = 0; i < destinations.length; i++) { + if (location.startsWith(destinations[i].path)) { + selectedIndex = i; + break; + } + } + + Future onSelect(int index) async { + final path = destinations[index].path; + if (GoRouterState.of(context).uri.toString() != path) { + context.go(path); + } + } + + Future onLogout() async { + await authStore.saveApiKey(null); + if (!context.mounted) return; + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(const SnackBar(content: Text('خروج انجام شد'))); + context.go('/login'); + } + + // Brand top bar with contrast color + final Color appBarBg = Theme.of(context).brightness == Brightness.dark + ? scheme.surfaceVariant + : scheme.primary; + final Color appBarFg = Theme.of(context).brightness == Brightness.dark + ? scheme.onSurfaceVariant + : scheme.onPrimary; + + final appBar = AppBar( + backgroundColor: appBarBg, + foregroundColor: appBarFg, + titleSpacing: 0, + title: Row( children: [ - NavigationRail( - selectedIndex: 0, - destinations: const [ - NavigationRailDestination(icon: Icon(Icons.dashboard_outlined), label: Text('Dashboard')), - ], - ), - const VerticalDivider(width: 1), - Expanded(child: child), + const SizedBox(width: 12), + // Logo placeholder (can replace with AssetImage) + CircleAvatar(backgroundColor: appBarFg.withOpacity(0.15), child: Icon(Icons.account_balance, color: appBarFg)), + const SizedBox(width: 12), + Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)), + const SizedBox(width: 8), + Text(destinations[selectedIndex].label, style: TextStyle(color: appBarFg.withOpacity(0.85))), ], ), + leading: useRail + ? null + : Builder( + builder: (ctx) => IconButton( + icon: Icon(Icons.menu, color: appBarFg), + onPressed: () => Scaffold.of(ctx).openDrawer(), + tooltip: t.menu, + ), + ), + actions: [ + if (themeController != null) ...[ + ThemeModeSwitcher(controller: themeController!), + const SizedBox(width: 8), + ], + if (localeController != null) ...[ + LanguageSwitcher(controller: localeController!), + const SizedBox(width: 8), + ], + LogoutButton(authStore: authStore), + ], + ); + + final content = Container( + color: scheme.surface, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + + if (useRail) { + return Scaffold( + appBar: appBar, + body: Row( + children: [ + NavigationRail( + selectedIndex: selectedIndex, + extended: railExtended, + leading: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: CircleAvatar( + backgroundColor: scheme.primary, + child: const Icon(Icons.person, color: Colors.white), + ), + ), + destinations: [ + for (final d in destinations) + NavigationRailDestination( + icon: Icon(d.icon), + selectedIcon: Icon(d.selectedIcon), + label: Text(d.label), + ), + ], + onDestinationSelected: onSelect, + ), + const VerticalDivider(width: 1), + Expanded(child: content), + ], + ), + ); + } + + return Scaffold( + appBar: appBar, + drawer: Drawer( + child: SafeArea( + child: Column( + children: [ + UserAccountsDrawerHeader( + currentAccountPicture: CircleAvatar( + backgroundColor: scheme.primary, + child: const Icon(Icons.person, color: Colors.white), + ), + accountName: const Text(''), + accountEmail: const Text(''), + ), + for (int i = 0; i < destinations.length; i++) + ListTile( + leading: Icon(destinations[i].selectedIcon), + title: Text(destinations[i].label), + selected: i == selectedIndex, + onTap: () { + Navigator.of(context).pop(); + onSelect(i); + }, + ), + const Spacer(), + ListTile( + leading: const Icon(Icons.logout), + title: const Text('خروج'), + onTap: onLogout, + ), + ], + ), + ), + ), + body: content, ); } } +class _Dest { + final String label; + final IconData icon; + final IconData selectedIcon; + final String path; + const _Dest(this.label, this.icon, this.selectedIcon, this.path); +} + diff --git a/hesabixUI/hesabix_ui/lib/widgets/logout_button.dart b/hesabixUI/hesabix_ui/lib/widgets/logout_button.dart new file mode 100644 index 0000000..9eb9e1f --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/logout_button.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../core/auth_store.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; + +class LogoutButton extends StatelessWidget { + final AuthStore authStore; + const LogoutButton({super.key, required this.authStore}); + + Future _confirmAndLogout(BuildContext context) async { + final t = AppLocalizations.of(context); + final bool? ok = await showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + title: Text(t.logoutConfirmTitle), + content: Text(t.logoutConfirmMessage), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: Text(t.cancel)), + FilledButton(onPressed: () => Navigator.of(ctx).pop(true), child: Text(t.logout)), + ], + ); + }, + ); + + if (ok != true) return; + + await authStore.saveApiKey(null); + if (!context.mounted) return; + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(t.logoutDone))); + context.go('/login'); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final t = AppLocalizations.of(context); + return Tooltip( + message: t.logout, + child: InkWell( + onTap: () => _confirmAndLogout(context), + customBorder: const CircleBorder(), + child: CircleAvatar( + radius: 14, + backgroundColor: cs.surfaceContainerHighest, + foregroundColor: cs.onSurface, + child: const Icon(Icons.logout, size: 16), + ), + ), + ); + } +} + +