import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/api_client.dart'; import '../state/session_state.dart'; /// Multi-device awareness: list / revoke active sessions (roadmap B). 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(); } class _SessionsScreenState extends State { List> _sessions = []; bool _loading = true; String? _error; @override void initState() { super.initState(); _load(); } Future _load() async { final session = context.read(); if (session.user == null) return; setState(() { _loading = true; _error = null; }); try { final list = await session.api.listSessions(session.user!.id); setState(() => _sessions = list); } on ApiException catch (e) { setState(() => _error = '세션 목록 실패 (${e.statusCode})'); } finally { setState(() => _loading = false); } } Future _revoke(Map s) async { final session = context.read(); if (session.user == null) return; final id = s['id']; if (id is! num) return; final isCurrent = s['is_current'] == true; final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(isCurrent ? '이 기기에서 로그아웃?' : '다른 세션 종료?'), content: Text(isCurrent ? '현재 기기 세션이 삭제됩니다.' : '선택한 기기의 로그인이 해제됩니다.'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('취소')), FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('종료')), ], ), ); if (ok != true || !mounted) return; if (isCurrent) { // Q8c — clear local token and return to entry via AuthGate. await session.logout(); if (!mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); return; } try { await session.api.revokeSession(session.user!.id, id.toInt()); await _load(); } on ApiException catch (e) { setState(() => _error = '세션 종료 실패 (${e.statusCode})'); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar(title: const Text('로그인 세션')), body: RefreshIndicator( onRefresh: _load, child: _loading ? ListView(children: const [SizedBox(height: 160), Center(child: CircularProgressIndicator())]) : ListView( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Text( '이 계정에 연결된 활성 세션입니다. 다른 기기를 종료하면 해당 토큰이 즉시 무효화됩니다.', style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), ), if (_error != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)), ), for (final s in _sessions) ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), title: Text( s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}', style: theme.textTheme.titleSmall, ), subtitle: Text( '만료: ${SessionsScreen.formatExpiresAtLocal(s['expires_at'])}', style: theme.textTheme.bodySmall, ), trailing: IconButton( tooltip: '세션 종료', icon: const Icon(Icons.logout), onPressed: () => _revoke(s), ), ), ], ), ), ); } }