diff --git a/locales/en.toml b/locales/en.toml index bc5d4d6a..35d8e8c0 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -551,6 +551,7 @@ client_id = "Client ID: {{id}}" client_id_missing = "No client ID available." current_status = "Current status: {{status}}" last_auth = "Last signed in: {{value}}" +link_status = "Link status: {{status}}" link_missing = "This app does not have a launch URL configured." link_open_error = "Could not open the app link." render_error = "Dashboard render error: {{error}}" @@ -2084,7 +2085,8 @@ title = "Cancel consent" [ui.userfront.dashboard] last_auth_label = "Last sign-in" -status_history = "Activity history" +link_status_label = "Link status" +status_history = "Link details" [ui.userfront.dashboard.activity] linked = "Linked" @@ -2109,7 +2111,7 @@ confirm_button = "Disconnect" title = "Disconnect app" [ui.userfront.dashboard.scopes] -title = "Permission (Scopes)" +title = "Consent scopes" [ui.userfront.dashboard.status] revoked = "Revoked" diff --git a/locales/ko.toml b/locales/ko.toml index 680b34f8..a89cea92 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -192,6 +192,7 @@ client_id = "Client ID: {{id}}" client_id_missing = "Client ID 없음" current_status = "현재 상태: {{status}}" last_auth = "최근 인증: {{value}}" +link_status = "연동 상태: {{status}}" link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." link_open_error = "해당 링크를 열 수 없습니다." render_error = "대시보드 렌더링 오류: {{error}}" @@ -402,7 +403,8 @@ session = "세션" [ui.userfront.dashboard] last_auth_label = "최근 인증" -status_history = "상태 이력" +link_status_label = "연동 상태" +status_history = "연동 정보" [ui.userfront.device] android = "Mobile(Android)" @@ -2505,7 +2507,7 @@ confirm_button = "해지하기" title = "연동 해지" [ui.userfront.dashboard.scopes] -title = "권한 (Scopes)" +title = "동의 범위" [ui.userfront.dashboard.status] revoked = "해지됨" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index f7eafeb2..acc22ea8 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -86,6 +86,7 @@ client_id = "Client ID: {id}" client_id_missing = "No client ID available." current_status = "Current status: {status}" last_auth = "Last signed in: {value}" +link_status = "Link status: {status}" link_missing = "This app does not have a launch URL configured." link_open_error = "Could not open the app link." render_error = "Dashboard render error: {error}" @@ -464,7 +465,8 @@ title = "Cancel consent" [ui.userfront.dashboard] last_auth_label = "Last sign-in" -status_history = "Activity history" +link_status_label = "Link status" +status_history = "Link details" [ui.userfront.dashboard.activity] linked = "Linked" @@ -489,7 +491,7 @@ confirm_button = "Disconnect" title = "Disconnect app" [ui.userfront.dashboard.scopes] -title = "Permission (Scopes)" +title = "Consent scopes" [ui.userfront.dashboard.status] revoked = "Revoked" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 244414a5..24f7ce3b 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -62,6 +62,7 @@ client_id = "Client ID: {id}" client_id_missing = "Client ID 없음" current_status = "현재 상태: {status}" last_auth = "최근 인증: {value}" +link_status = "연동 상태: {status}" link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." link_open_error = "해당 링크를 열 수 없습니다." render_error = "대시보드 렌더링 오류: {error}" @@ -176,7 +177,8 @@ session = "세션" [ui.userfront.dashboard] last_auth_label = "최근 인증" -status_history = "상태 이력" +link_status_label = "연동 상태" +status_history = "연동 정보" [ui.userfront.device] android = "Mobile(Android)" @@ -694,7 +696,7 @@ confirm_button = "해지하기" title = "연동 해지" [ui.userfront.dashboard.scopes] -title = "권한 (Scopes)" +title = "동의 범위" [ui.userfront.dashboard.status] revoked = "해지됨" diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index d06ef9c2..5d46255c 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -38,6 +38,8 @@ class _DashboardScreenState extends ConsumerState { static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); static const double _dashboardCardSpacing = 12; + static const double _dashboardCardMaxWidth = 228; + static const double _activityDialogMaxWidth = 360; static const double _historySessionMinWidth = 92; static const double _historyOtherColumnsBaselineWidth = 780; static const int _historySessionMinVisibleChars = 8; @@ -235,85 +237,158 @@ class _DashboardScreenState extends ConsumerState { context: context, builder: (context) => Consumer( builder: (context, ref, _) { + final dialogWidth = math.min( + MediaQuery.sizeOf(context).width - 48, + _activityDialogMaxWidth, + ); + final statusLabel = item.status == 'active' + ? tr('ui.userfront.dashboard.activity.linked') + : tr('ui.userfront.dashboard.status.revoked'); + final statusColor = _activityStatusColor(item.status); + return AlertDialog( - title: Text(item.appName), + backgroundColor: _surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 24, + ), + contentPadding: const EdgeInsets.fromLTRB(20, 20, 20, 8), content: SizedBox( - width: double.maxFinite, + width: dialogWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - tr('ui.userfront.dashboard.scopes.title'), - style: const TextStyle(fontWeight: FontWeight.bold), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: _border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.appName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _ink, + ), + ), + const SizedBox(height: 4), + Text( + tr('ui.common.details'), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + ], + ), ), - const SizedBox(height: 8), - if (item.scopes.isEmpty) - Text( - tr('msg.userfront.dashboard.scopes.empty'), - style: const TextStyle(color: Colors.grey), - ) - else - Wrap( - spacing: 8, - runSpacing: 4, - children: item.scopes - .map( - (s) => Chip( - label: Text( - s, - style: const TextStyle(fontSize: 12), - ), - visualDensity: VisualDensity.compact, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, + const SizedBox(height: 16), + _buildActivityDetailSection( + title: tr('ui.userfront.dashboard.status_history'), + child: Row( + children: [ + Expanded( + child: _buildActivityDetailField( + label: tr( + 'ui.userfront.dashboard.link_status_label', + ), + value: statusLabel, + valueColor: statusColor, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildActivityDetailField( + label: tr('ui.userfront.dashboard.last_auth_label'), + value: item.lastAuthAt, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + _buildActivityDetailSection( + title: tr('ui.userfront.dashboard.scopes.title'), + child: item.scopes.isEmpty + ? Text( + tr('msg.userfront.dashboard.scopes.empty'), + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], ), ) - .toList(), - ), - const SizedBox(height: 24), - Text( - tr('ui.userfront.dashboard.status_history'), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr( - 'msg.userfront.dashboard.last_auth', - params: {'value': item.lastAuthAt}, - ), - ), - const SizedBox(height: 4), - Builder( - builder: (context) { - final statusLabel = item.status == 'active' - ? tr('ui.common.status.active') - : tr('ui.userfront.dashboard.status.revoked'); - return Text( - tr( - 'msg.userfront.dashboard.current_status', - params: {'status': statusLabel}, - ), - style: TextStyle( - color: item.status == 'active' - ? Colors.green - : Colors.grey, - ), - ); - }, - ), - ], + : Wrap( + spacing: 8, + runSpacing: 8, + children: item.scopes + .map( + (scope) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _border), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.shield_outlined, + size: 14, + color: _ink, + ), + const SizedBox(width: 6), + Text( + scope, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _ink, + ), + ), + ], + ), + ), + ) + .toList(), + ), ), ], ), ), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(tr('ui.common.close')), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + foregroundColor: _ink, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: _subtle, + ), + child: Text( + tr('ui.common.close'), + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), ), ], ); @@ -322,6 +397,73 @@ class _DashboardScreenState extends ConsumerState { ); } + Widget _buildActivityDetailSection({ + required String title, + required Widget child, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: _ink, + ), + ), + const SizedBox(height: 10), + child, + ], + ), + ); + } + + Widget _buildActivityDetailField({ + required String label, + required String value, + Color? valueColor, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 6), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: valueColor ?? _ink, + ), + ), + ], + ), + ); + } + Widget _buildSideMenu(BuildContext context, {required bool closeOnTap}) { return SafeArea( child: Column( @@ -1319,7 +1461,7 @@ class _DashboardScreenState extends ConsumerState { Widget _buildActivityCard(_ActivityItem item, {double? cardWidth}) { final isActive = item.status == 'active'; - final statusColor = isActive ? Colors.green : Colors.grey; + final statusColor = _activityStatusColor(item.status); final borderColor = isActive ? Colors.green.withValues(alpha: 128) : _border; @@ -1331,10 +1473,10 @@ class _DashboardScreenState extends ConsumerState { // 카드 컨텐츠 final cardContent = Container( width: cardWidth ?? 260, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: _surface, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: borderWidth), boxShadow: isActive ? [ @@ -1355,7 +1497,7 @@ class _DashboardScreenState extends ConsumerState { child: Text( item.appName, style: const TextStyle( - fontSize: 16, + fontSize: 15, fontWeight: FontWeight.w600, color: _ink, ), @@ -1380,7 +1522,7 @@ class _DashboardScreenState extends ConsumerState { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 10), Text( tr('ui.userfront.dashboard.last_auth_label'), style: TextStyle(fontSize: 12, color: Colors.grey[600]), @@ -1389,12 +1531,12 @@ class _DashboardScreenState extends ConsumerState { Text( item.lastAuthAt, style: const TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.w600, color: _ink, ), ), - const SizedBox(height: 16), + const SizedBox(height: 14), Row( children: [ Expanded( @@ -1403,7 +1545,7 @@ class _DashboardScreenState extends ConsumerState { style: OutlinedButton.styleFrom( foregroundColor: _ink, side: const BorderSide(color: _border), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 7), ), child: Text( tr('ui.common.details'), @@ -1425,7 +1567,7 @@ class _DashboardScreenState extends ConsumerState { color: item.isRevoked ? Colors.grey : Colors.redAccent, width: 0.5, ), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 7), ), child: _isRevoking && !item.isRevoked ? const SizedBox( @@ -1783,8 +1925,15 @@ class _DashboardScreenState extends ConsumerState { } double _dashboardCardWidth(double maxWidth, int crossAxisCount) { - return (maxWidth - (_dashboardCardSpacing * (crossAxisCount - 1))) / - crossAxisCount; + return math.min( + (maxWidth - (_dashboardCardSpacing * (crossAxisCount - 1))) / + crossAxisCount, + _dashboardCardMaxWidth, + ); + } + + Color _activityStatusColor(String status) { + return status == 'active' ? Colors.green : Colors.grey; } Widget _buildCenteredHistoryHeader(String label, {double? width}) { diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 7974493c..46f24bf9 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -418,6 +418,7 @@ const Map koStrings = { "msg.userfront.audit.device": "접속환경: {{value}}", "msg.userfront.audit.end": "더 이상 항목이 없습니다.", "msg.userfront.audit.filter.description": "활성화된 세션만 보려면 토글을 켜주세요.", + "msg.userfront.audit.filtered_empty": "활성 세션으로 필터링된 접속 이력이 없습니다.", "msg.userfront.audit.ip": "접속 IP: {{value}}", "msg.userfront.audit.load_more_error": "더 불러오지 못했습니다.", "msg.userfront.audit.result": "인증결과: {{value}}", @@ -460,6 +461,7 @@ const Map koStrings = { "msg.userfront.dashboard.last_auth": "최근 인증: {{value}}", "msg.userfront.dashboard.link_missing": "이동할 페이지 주소(Client URI)가 설정되지 않았습니다.", "msg.userfront.dashboard.link_open_error": "해당 링크를 열 수 없습니다.", + "msg.userfront.dashboard.link_status": "연동 상태: {{status}}", "msg.userfront.dashboard.render_error": "대시보드 렌더링 오류: {{error}}", "msg.userfront.dashboard.revoke.confirm": "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", @@ -1717,9 +1719,10 @@ const Map koStrings = { "ui.userfront.dashboard.approved_session.default": "승인한 세션 ID", "ui.userfront.dashboard.approved_session.userfront": "승인한 Userfront 세션 ID", "ui.userfront.dashboard.last_auth_label": "최근 인증", + "ui.userfront.dashboard.link_status_label": "연동 상태", "ui.userfront.dashboard.revoke.confirm_button": "해지하기", "ui.userfront.dashboard.revoke.title": "연동 해지", - "ui.userfront.dashboard.scopes.title": "권한 (Scopes)", + "ui.userfront.dashboard.scopes.title": "동의 범위", "ui.userfront.dashboard.sessions.active_badge": "활성화", "ui.userfront.dashboard.sessions.current_badge": "접속중", "ui.userfront.dashboard.sessions.current_disabled": "현재 세션", @@ -2324,6 +2327,8 @@ const Map enStrings = { "msg.userfront.audit.end": "No more items to show.", "msg.userfront.audit.filter.description": "Toggle to view only active sessions.", + "msg.userfront.audit.filtered_empty": + "No sign-in history matches the active session filter.", "msg.userfront.audit.ip": "IP address: {{value}}", "msg.userfront.audit.load_more_error": "Could not load more history.", "msg.userfront.audit.result": "Result: {{value}}", @@ -2376,6 +2381,7 @@ const Map enStrings = { "msg.userfront.dashboard.link_missing": "This app does not have a launch URL configured.", "msg.userfront.dashboard.link_open_error": "Could not open the app link.", + "msg.userfront.dashboard.link_status": "Link status: {{status}}", "msg.userfront.dashboard.render_error": "Dashboard render error: {{error}}", "msg.userfront.dashboard.revoke.confirm": "Disconnect {{app}}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in.", @@ -3728,9 +3734,10 @@ const Map enStrings = { "ui.userfront.dashboard.approved_session.userfront": "Approved UserFront session ID", "ui.userfront.dashboard.last_auth_label": "Last sign-in", + "ui.userfront.dashboard.link_status_label": "Link status", "ui.userfront.dashboard.revoke.confirm_button": "Disconnect", "ui.userfront.dashboard.revoke.title": "Disconnect app", - "ui.userfront.dashboard.scopes.title": "Permission (Scopes)", + "ui.userfront.dashboard.scopes.title": "Consent scopes", "ui.userfront.dashboard.sessions.active_badge": "Active", "ui.userfront.dashboard.sessions.current_badge": "Current", "ui.userfront.dashboard.sessions.current_disabled": "Current session", @@ -3739,7 +3746,7 @@ const Map enStrings = { "ui.userfront.dashboard.sessions.unknown_device": "Unknown device", "ui.userfront.dashboard.sessions.unknown_session": "Session", "ui.userfront.dashboard.status.revoked": "Revoked", - "ui.userfront.dashboard.status_history": "Activity history", + "ui.userfront.dashboard.status_history": "Link details", "ui.userfront.device.android": "Mobile(Android)", "ui.userfront.device.ios": "Mobile(iOS)", "ui.userfront.device.linux": "Desktop(Linux)",