From c28c6ca2e804712ef15fb5ecb6a5210674f4e15a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 05:18:13 +0000 Subject: [PATCH] fix(ui): show session expiry in device local time Parse API UTC expires_at and render yyyy-MM-dd HH:mm via DateTime.toLocal() instead of raw RFC3339 Z strings on the sessions screen. Co-authored-by: okuma --- mobile/lib/screens/sessions_screen.dart | 16 ++++++++++++++- mobile/test/sessions_local_time_test.dart | 24 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 mobile/test/sessions_local_time_test.dart diff --git a/mobile/lib/screens/sessions_screen.dart b/mobile/lib/screens/sessions_screen.dart index ab57c69..15379fe 100644 --- a/mobile/lib/screens/sessions_screen.dart +++ b/mobile/lib/screens/sessions_screen.dart @@ -8,6 +8,17 @@ import '../state/session_state.dart'; class SessionsScreen extends StatefulWidget { const SessionsScreen({super.key}); + /// Formats API `expires_at` (UTC RFC3339) for the device local timezone. + static String formatExpiresAtLocal(Object? raw) { + if (raw == null) return ''; + final parsed = DateTime.tryParse(raw.toString()); + if (parsed == null) return raw.toString(); + final local = parsed.toLocal(); + String two(int n) => n.toString().padLeft(2, '0'); + return '${local.year}-${two(local.month)}-${two(local.day)} ' + '${two(local.hour)}:${two(local.minute)}'; + } + @override State createState() => _SessionsScreenState(); } @@ -104,7 +115,10 @@ class _SessionsScreenState extends State { s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}', style: theme.textTheme.titleSmall, ), - subtitle: Text('만료: ${s['expires_at'] ?? ''}', style: theme.textTheme.bodySmall), + subtitle: Text( + '만료: ${SessionsScreen.formatExpiresAtLocal(s['expires_at'])}', + style: theme.textTheme.bodySmall, + ), trailing: IconButton( tooltip: '세션 종료', icon: const Icon(Icons.logout), diff --git a/mobile/test/sessions_local_time_test.dart b/mobile/test/sessions_local_time_test.dart new file mode 100644 index 0000000..332e38e --- /dev/null +++ b/mobile/test/sessions_local_time_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ykavu_mobile/screens/sessions_screen.dart'; + +void main() { + test('formatExpiresAtLocal converts UTC Z to local wall clock', () { + // Fixed offset via DateTime.parse keeps instant; toLocal() uses test TZ. + final raw = '2026-09-02T09:06:45.137172Z'; + final out = SessionsScreen.formatExpiresAtLocal(raw); + final expected = DateTime.parse(raw).toLocal(); + final two = (int n) => n.toString().padLeft(2, '0'); + expect( + out, + '${expected.year}-${two(expected.month)}-${two(expected.day)} ' + '${two(expected.hour)}:${two(expected.minute)}', + ); + expect(out.contains('T'), isFalse); + expect(out.endsWith('Z'), isFalse); + }); + + test('formatExpiresAtLocal handles null and garbage', () { + expect(SessionsScreen.formatExpiresAtLocal(null), ''); + expect(SessionsScreen.formatExpiresAtLocal('not-a-date'), 'not-a-date'); + }); +}