From 5c995a5b4d81839398acf77f011bf5d8f6ab7fbb Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 20 Mar 2026 10:50:16 +0900 Subject: [PATCH] =?UTF-8?q?uf=20=EC=95=A1=EC=85=98=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B3=B5=ED=86=B5=ED=99=94=20=EB=B0=8F=20SnackBar?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/lib/core/ui/toast_service.dart | 234 ++++++++++++++++++ .../presentation/create_user_screen.dart | 19 +- .../presentation/user_management_screen.dart | 16 +- .../auth/presentation/consent_screen.dart | 18 +- .../presentation/forgot_password_screen.dart | 12 +- .../auth/presentation/login_screen.dart | 9 +- .../presentation/qr_scan_screen_stub.dart | 12 +- .../presentation/reset_password_screen.dart | 12 +- .../presentation/dashboard_screen.dart | 53 +--- .../presentation/pages/profile_page.dart | 58 +++-- userfront/lib/main.dart | 6 + 11 files changed, 308 insertions(+), 141 deletions(-) create mode 100644 userfront/lib/core/ui/toast_service.dart diff --git a/userfront/lib/core/ui/toast_service.dart b/userfront/lib/core/ui/toast_service.dart new file mode 100644 index 00000000..560f6e94 --- /dev/null +++ b/userfront/lib/core/ui/toast_service.dart @@ -0,0 +1,234 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +enum ToastType { success, error, info } + +class _ToastItem { + const _ToastItem({ + required this.id, + required this.message, + required this.type, + }); + + final String id; + final String message; + final ToastType type; +} + +class ToastService { + static const Duration _displayDuration = Duration(milliseconds: 3000); + static final ValueNotifier> _toasts = + ValueNotifier>(<_ToastItem>[]); + + static void success(String message) { + show(message, type: ToastType.success); + } + + static void error(String message) { + show(message, type: ToastType.error); + } + + static void info(String message) { + show(message, type: ToastType.info); + } + + static void show(String message, {ToastType type = ToastType.success}) { + final trimmed = message.trim(); + if (trimmed.isEmpty) { + return; + } + + final item = _ToastItem( + id: '${DateTime.now().microsecondsSinceEpoch}-${_toasts.value.length}', + message: trimmed, + type: type, + ); + + _toasts.value = [..._toasts.value, item]; + + unawaited( + Future.delayed(_displayDuration, () { + _remove(item.id); + }), + ); + } + + static void _remove(String id) { + final next = _toasts.value.where((toast) => toast.id != id).toList(); + if (next.length == _toasts.value.length) { + return; + } + _toasts.value = next; + } +} + +class ToastViewport extends StatelessWidget { + const ToastViewport({super.key}); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: true, + child: SafeArea( + child: ValueListenableBuilder>( + valueListenable: ToastService._toasts, + builder: (context, toasts, _) { + if (toasts.isEmpty) { + return const SizedBox.shrink(); + } + + final media = MediaQuery.of(context); + final width = math.min(320.0, media.size.width - 32); + + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(right: 16, bottom: 16), + child: SizedBox( + width: width > 0 ? width : 320, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + for (final toast in toasts) + Padding( + padding: const EdgeInsets.only(top: 8), + child: _ToastCard(item: toast), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } +} + +class _ToastCard extends StatefulWidget { + const _ToastCard({required this.item}); + + final _ToastItem item; + + @override + State<_ToastCard> createState() => _ToastCardState(); +} + +class _ToastCardState extends State<_ToastCard> { + bool _visible = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + setState(() { + _visible = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + final scheme = _toastColorScheme(widget.item.type); + final icon = _toastIcon(widget.item.type); + + return AnimatedSlide( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + offset: _visible ? Offset.zero : const Offset(1, 0), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 220), + opacity: _visible ? 1 : 0, + child: DecoratedBox( + decoration: BoxDecoration( + color: scheme.background, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: scheme.border), + boxShadow: const [ + BoxShadow( + color: Color(0x26000000), + blurRadius: 16, + offset: Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, size: 20, color: scheme.foreground), + const SizedBox(width: 12), + Expanded( + child: Text( + widget.item.message, + style: TextStyle( + color: scheme.foreground, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.2, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + _ToastColorScheme _toastColorScheme(ToastType type) { + switch (type) { + case ToastType.success: + return const _ToastColorScheme( + background: Color(0xFFECFDF5), + border: Color(0xFFA7F3D0), + foreground: Color(0xFF065F46), + ); + case ToastType.error: + return const _ToastColorScheme( + background: Color(0xFFFFF1F2), + border: Color(0xFFFDA4AF), + foreground: Color(0xFF9F1239), + ); + case ToastType.info: + return const _ToastColorScheme( + background: Color(0xFFEFF6FF), + border: Color(0xFFBFDBFE), + foreground: Color(0xFF1E40AF), + ); + } + } + + IconData _toastIcon(ToastType type) { + switch (type) { + case ToastType.success: + return Icons.check_circle_outline; + case ToastType.error: + return Icons.error_outline; + case ToastType.info: + return Icons.info_outline; + } + } +} + +class _ToastColorScheme { + const _ToastColorScheme({ + required this.background, + required this.border, + required this.foreground, + }); + + final Color background; + final Color border; + final Color foreground; +} diff --git a/userfront/lib/features/admin/presentation/create_user_screen.dart b/userfront/lib/features/admin/presentation/create_user_screen.dart index 368cc681..e08e94d5 100644 --- a/userfront/lib/features/admin/presentation/create_user_screen.dart +++ b/userfront/lib/features/admin/presentation/create_user_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/i18n/locale_utils.dart'; +import '../../../../core/ui/toast_service.dart'; class CreateUserScreen extends StatefulWidget { const CreateUserScreen({super.key}); @@ -86,12 +87,7 @@ class _CreateUserScreenState extends State { } } else { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid Password. Access Denied.'), - backgroundColor: Colors.red, - ), - ); + ToastService.error('Invalid Password. Access Denied.'); context.go(buildLocalizedHomePath(Uri.base)); // Kick out } } @@ -144,12 +140,7 @@ class _CreateUserScreenState extends State { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('User created successfully!'), - backgroundColor: Colors.green, - ), - ); + ToastService.success('User created successfully!'); _formKey.currentState!.reset(); _loginIdController.clear(); _emailController.clear(); @@ -158,9 +149,7 @@ class _CreateUserScreenState extends State { } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), - ); + ToastService.error('Error: $e'); } } finally { if (mounted) setState(() => _isLoading = false); diff --git a/userfront/lib/features/admin/presentation/user_management_screen.dart b/userfront/lib/features/admin/presentation/user_management_screen.dart index 50274f13..d1d4168f 100644 --- a/userfront/lib/features/admin/presentation/user_management_screen.dart +++ b/userfront/lib/features/admin/presentation/user_management_screen.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'dart:async'; import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/i18n/locale_utils.dart'; +import '../../../../core/ui/toast_service.dart'; class UserManagementScreen extends StatefulWidget { const UserManagementScreen({super.key}); @@ -108,12 +109,7 @@ class _UserManagementScreenState extends State } } else { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid Password'), - backgroundColor: Colors.red, - ), - ); + ToastService.error('Invalid Password'); context.go(buildLocalizedHomePath(Uri.base)); } } @@ -343,16 +339,12 @@ class _UserManagementScreenState extends State // --- UI Helpers --- void _showError(String msg) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.red)); + ToastService.error(msg); } void _showSuccess(String msg) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.green)); + ToastService.success(msg); } @override diff --git a/userfront/lib/features/auth/presentation/consent_screen.dart b/userfront/lib/features/auth/presentation/consent_screen.dart index 9d7d8003..327b63ff 100644 --- a/userfront/lib/features/auth/presentation/consent_screen.dart +++ b/userfront/lib/features/auth/presentation/consent_screen.dart @@ -4,6 +4,7 @@ import 'package:userfront/i18n.dart'; import 'package:userfront/core/i18n/locale_utils.dart'; import 'package:userfront/core/services/auth_proxy_service.dart'; import 'package:userfront/core/services/web_window.dart'; +import 'package:userfront/core/ui/toast_service.dart'; class ConsentScreen extends StatefulWidget { final String consentChallenge; @@ -187,16 +188,11 @@ class _ConsentScreenState extends State { } catch (e) { setState(() => _isSubmitting = false); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.consent.cancel.error', - fallback: - 'An error occurred while cancelling consent: {{error}}', - params: {'error': '$e'}, - ), - ), + ToastService.error( + tr( + 'msg.userfront.consent.cancel.error', + fallback: 'An error occurred while cancelling consent: {{error}}', + params: {'error': '$e'}, ), ); } @@ -419,7 +415,7 @@ class _ConsentScreenState extends State { ) : Text( tr('ui.userfront.consent.accept'), - style: TextStyle( + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), diff --git a/userfront/lib/features/auth/presentation/forgot_password_screen.dart b/userfront/lib/features/auth/presentation/forgot_password_screen.dart index b3fe1d96..8c01e4cd 100644 --- a/userfront/lib/features/auth/presentation/forgot_password_screen.dart +++ b/userfront/lib/features/auth/presentation/forgot_password_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../core/services/auth_proxy_service.dart'; +import '../../../core/ui/toast_service.dart'; import 'package:userfront/i18n.dart'; class ForgotPasswordScreen extends StatefulWidget { @@ -46,12 +47,7 @@ class _ForgotPasswordScreenState extends State { drySend: _drySendEnabled, ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('msg.userfront.forgot.sent')), - backgroundColor: Colors.green, - ), - ); + ToastService.success(tr('msg.userfront.forgot.sent')); Navigator.of(context).pop(); } } catch (e) { @@ -68,9 +64,7 @@ class _ForgotPasswordScreenState extends State { } void _showError(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); + ToastService.error(message); } bool _parseBoolParam(String? value) { diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 906b7ba5..89365e12 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -18,6 +18,7 @@ import '../domain/cookie_session_policy.dart'; import '../domain/login_link_route_policy.dart'; import '../../profile/domain/notifiers/profile_notifier.dart'; import '../../../core/services/web_window.dart'; +import '../../../core/ui/toast_service.dart'; class LoginScreen extends ConsumerStatefulWidget { final String? verificationToken; @@ -1153,9 +1154,7 @@ class _LoginScreenState extends ConsumerState void _showError(String message) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); + ToastService.error(message); try { AuthProxyService.logError(message); } catch (e) { @@ -1165,9 +1164,7 @@ class _LoginScreenState extends ConsumerState void _showInfo(String message) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.green), - ); + ToastService.success(message); } void _logTokenDetails(String jwt) { diff --git a/userfront/lib/features/auth/presentation/qr_scan_screen_stub.dart b/userfront/lib/features/auth/presentation/qr_scan_screen_stub.dart index cd524661..75962f23 100644 --- a/userfront/lib/features/auth/presentation/qr_scan_screen_stub.dart +++ b/userfront/lib/features/auth/presentation/qr_scan_screen_stub.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:userfront/i18n.dart'; +import 'package:userfront/core/ui/toast_service.dart'; import 'qr_scan_route.dart'; @@ -23,15 +24,8 @@ class _QRScanScreenState extends State { void _submit() { final raw = _controller.text.trim(); if (raw.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.qr.permission_required', - fallback: '카메라 권한이 필요합니다.', - ), - ), - ), + ToastService.info( + tr('msg.userfront.qr.permission_required', fallback: '카메라 권한이 필요합니다.'), ); return; } diff --git a/userfront/lib/features/auth/presentation/reset_password_screen.dart b/userfront/lib/features/auth/presentation/reset_password_screen.dart index d9ebcb39..938831a2 100644 --- a/userfront/lib/features/auth/presentation/reset_password_screen.dart +++ b/userfront/lib/features/auth/presentation/reset_password_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../core/i18n/locale_utils.dart'; import '../../../core/services/auth_proxy_service.dart'; +import '../../../core/ui/toast_service.dart'; import 'package:userfront/i18n.dart'; class ResetPasswordScreen extends StatefulWidget { @@ -84,12 +85,7 @@ class _ResetPasswordScreenState extends State { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('msg.userfront.reset.success')), - backgroundColor: Colors.green, - ), - ); + ToastService.success(tr('msg.userfront.reset.success')); context.go(buildLocalizedSigninPath(Uri.base)); } } catch (e) { @@ -109,9 +105,7 @@ class _ResetPasswordScreenState extends State { } void _showError(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); + ToastService.error(message); } String _buildPolicyDescription() { diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 3e30856f..342d0940 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -15,6 +15,7 @@ import '../../../../core/services/http_client.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/widgets/language_selector.dart'; import '../../../../core/ui/layout_breakpoints.dart'; +import '../../../../core/ui/toast_service.dart'; import '../../profile/domain/notifiers/profile_notifier.dart'; import '../domain/dashboard_providers.dart'; import '../domain/models.dart' hide LinkedRp; @@ -104,14 +105,10 @@ class _DashboardScreenState extends ConsumerState { try { await ref.read(linkedRpsProvider.notifier).revokeRp(clientId); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.dashboard.revoke.success', - params: {'app': appName}, - ), - ), + ToastService.success( + tr( + 'msg.userfront.dashboard.revoke.success', + params: {'app': appName}, ), ); setState(() { @@ -121,15 +118,8 @@ class _DashboardScreenState extends ConsumerState { } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.dashboard.revoke.error', - params: {'error': '$e'}, - ), - ), - ), + ToastService.error( + tr('msg.userfront.dashboard.revoke.error', params: {'error': '$e'}), ); } } finally { @@ -547,12 +537,8 @@ class _DashboardScreenState extends ConsumerState { : () async { await Clipboard.setData(ClipboardData(text: approvedSessionId)); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr('msg.userfront.dashboard.session_id_copied'), - ), - ), + ToastService.info( + tr('msg.userfront.dashboard.session_id_copied'), ); } }, @@ -626,12 +612,8 @@ class _DashboardScreenState extends ConsumerState { : () async { await Clipboard.setData(ClipboardData(text: approvedSessionId)); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr('msg.userfront.dashboard.session_id_copied'), - ), - ), + ToastService.info( + tr('msg.userfront.dashboard.session_id_copied'), ); } }, @@ -1280,7 +1262,6 @@ class _DashboardScreenState extends ConsumerState { cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () async { - final messenger = ScaffoldMessenger.of(context); final itemUrl = item.url; if (itemUrl != null && itemUrl.isNotEmpty) { final uri = Uri.parse(itemUrl); @@ -1290,18 +1271,10 @@ class _DashboardScreenState extends ConsumerState { await launchUrl(uri); return; } - messenger.showSnackBar( - SnackBar( - content: Text(tr('msg.userfront.dashboard.link_open_error')), - ), - ); + ToastService.error(tr('msg.userfront.dashboard.link_open_error')); } else { if (!mounted) return; - messenger.showSnackBar( - SnackBar( - content: Text(tr('msg.userfront.dashboard.link_missing')), - ), - ); + ToastService.info(tr('msg.userfront.dashboard.link_missing')); } }, child: opaqueCard, diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 705b9752..c489e2a8 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -7,6 +7,7 @@ import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/ui/layout_breakpoints.dart'; +import '../../../../core/ui/toast_service.dart'; import '../../../../core/widgets/language_selector.dart'; import '../../data/models/user_profile_model.dart'; import '../../domain/notifiers/profile_notifier.dart'; @@ -202,21 +203,15 @@ class _ProfilePageState extends ConsumerState { _isVerifying = false; }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('msg.userfront.profile.phone.code_sent'))), - ); + ToastService.info(tr('msg.userfront.profile.phone.code_sent')); } } catch (e) { setState(() => _isVerifying = false); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.profile.phone.send_failed', - params: {'error': e.toString()}, - ), - ), + ToastService.error( + tr( + 'msg.userfront.profile.phone.send_failed', + params: {'error': e.toString()}, ), ); } @@ -236,21 +231,15 @@ class _ProfilePageState extends ConsumerState { _isVerifying = false; }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('msg.userfront.profile.phone.verified'))), - ); + ToastService.success(tr('msg.userfront.profile.phone.verified')); } } catch (e) { setState(() => _isVerifying = false); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr( - 'msg.userfront.profile.phone.verify_failed', - params: {'error': e.toString()}, - ), - ), + ToastService.error( + tr( + 'msg.userfront.profile.phone.verify_failed', + params: {'error': e.toString()}, ), ); } @@ -302,8 +291,9 @@ class _ProfilePageState extends ConsumerState { _newPasswordController?.clear(); _confirmPasswordController?.clear(); setState(() { - _passwordSuccess = tr('msg.userfront.profile.password.changed'); + _passwordSuccess = null; }); + ToastService.success(tr('msg.userfront.profile.password.changed')); } catch (e) { final message = e.toString().replaceFirst('Exception: ', ''); setState(() { @@ -312,6 +302,12 @@ class _ProfilePageState extends ConsumerState { params: {'error': message}, ); }); + ToastService.error( + tr( + 'msg.userfront.profile.password.change_failed', + params: {'error': message}, + ), + ); } finally { if (mounted) { setState(() => _isPasswordSaving = false); @@ -338,7 +334,7 @@ class _ProfilePageState extends ConsumerState { _debugLog('save_skip', reason: 'saving_in_flight'); return; } - + setState(() { _fieldSaveError = null; }); @@ -411,7 +407,7 @@ class _ProfilePageState extends ConsumerState { setState(() { _isSavingField = true; }); - + _debugLog('save_dispatch', field: currentField, changed: true); try { @@ -431,9 +427,7 @@ class _ProfilePageState extends ConsumerState { _editingField = null; }); _debugLog('save_success', field: currentField); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('msg.userfront.profile.update_success'))), - ); + ToastService.success(tr('msg.userfront.profile.update_success')); } } catch (e) { _debugLog('save_failed', field: currentField, reason: e.toString()); @@ -711,7 +705,9 @@ class _ProfilePageState extends ConsumerState { const SizedBox(width: 8), OutlinedButton( key: Key('profile-$field-cancel-button'), - onPressed: isUpdating || _isSavingField ? null : () => _cancelEditing(profile), + onPressed: isUpdating || _isSavingField + ? null + : () => _cancelEditing(profile), child: Text(tr('ui.common.cancel')), ), ], @@ -798,7 +794,9 @@ class _ProfilePageState extends ConsumerState { ), const SizedBox(width: 8), OutlinedButton( - onPressed: isUpdating || _isSavingField ? null : () => _cancelEditing(profile), + onPressed: isUpdating || _isSavingField + ? null + : () => _cancelEditing(profile), child: Text(tr('ui.common.cancel')), ), ], diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index 89dd0a20..8fbabee1 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -28,6 +28,7 @@ import 'core/i18n/locale_gate.dart'; import 'core/i18n/locale_registry.dart'; import 'core/i18n/locale_utils.dart'; import 'core/i18n/toml_asset_loader.dart'; +import 'core/ui/toast_service.dart'; import 'package:logging/logging.dart'; import 'features/auth/presentation/consent_screen.dart'; import 'i18n.dart'; @@ -370,6 +371,11 @@ class BaronSSOApp extends StatelessWidget { localizationsDelegates: delegates, supportedLocales: supportedLocales, locale: locale, + builder: (context, child) { + return Stack( + children: [if (child != null) child, const ToastViewport()], + ); + }, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base