// ignore_for_file: use_build_context_synchronously, deprecated_member_use, prefer_const_constructors import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/injection_container.dart'; import '../../core/errors/friendly_error.dart'; import '../../core/network/api_client.dart'; import '../../core/storage/secure_storage.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_decorations.dart'; import '../../core/theme/app_text_styles.dart'; import '../../shared/widgets/animations/animations.dart'; // --------------------------------------------------------------------------- // UserPairingScreen // --------------------------------------------------------------------------- // // Ditampilkan ke akun ROLE_USER. // - Tampilkan pairing code sementara yang bisa di-copy/share. // - Jika ada pending invite → tampilkan nama Guardian + tombol Accept / Reject. // - Jika sudah paired → tampilkan info Guardian + tombol Unpair. // --------------------------------------------------------------------------- class UserPairingScreen extends StatefulWidget { const UserPairingScreen({super.key}); @override State createState() => _UserPairingScreenState(); } class _UserPairingScreenState extends State { String? _pairingCode; DateTime? _pairingCodeExpiresAt; int? _pairingCodeSeconds; bool _regenerating = false; Future _regeneratePairingCode() async { setState(() => _regenerating = true); await runFriendlyAction( () async { final res = await sl() .dio .post('/shared/pairing/code/regenerate') .timeout(const Duration(seconds: 8)); _applyPairingCode(res.data['data']); _snack(context, 'Pairing code baru sudah dibuat.'); }, onError: (message) => _snack(context, message), fallback: 'Gagal membuat pairing code baru.', ); if (mounted) setState(() => _regenerating = false); } void _applyPairingCode(dynamic raw) { if (raw is! Map) return; final expires = DateTime.tryParse(raw['expiresAt']?.toString() ?? ''); setState(() { _pairingCode = raw['pairingCode']?.toString(); _pairingCodeExpiresAt = expires; _pairingCodeSeconds = int.tryParse(raw['expiresInSeconds'].toString()); }); } @override Widget build(BuildContext context) { return _Page( title: 'Pairing', subtitle: 'Bagikan pairing code sementara ini ke Guardian.', child: StaggerWrapper( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (_pairingCode == null || _pairingCode!.isEmpty) _InfoCard( title: 'Pairing Code', value: 'Tap Generate', icon: Icons.qr_code_2, helper: 'Kode dibuat saat dibutuhkan, berlaku sementara, dan bisa dibuat ulang kapan saja.') else _InfoCard( title: 'Pairing Code', value: _pairingCode!, icon: Icons.qr_code_2, helper: 'Valid ${_formatRemaining(_pairingCodeSeconds, _pairingCodeExpiresAt)}. Kode ini akan berubah dan kadaluarsa otomatis.'), const SizedBox(height: 10), OutlinedButton.icon( onPressed: _regenerating ? null : _regeneratePairingCode, icon: _regenerating ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.autorenew), label: Text(_regenerating ? 'Generating...' : 'Generate New Code'), ), const SizedBox(height: 16), _PairingStatusCard(allowUserResponse: true), ], ), ); } } // --------------------------------------------------------------------------- // GuardianPairingScreen // --------------------------------------------------------------------------- // // Ditampilkan ke akun ROLE_GUARDIAN. // - Input field 8-char temporary pairing code. // - Tombol "Send Invite". // - Status card: jika sudah paired → info User + tombol Unpair. // Jika pending → waiting state. // --------------------------------------------------------------------------- class GuardianPairingScreen extends StatefulWidget { const GuardianPairingScreen({super.key}); @override State createState() => _GuardianPairingScreenState(); } class _GuardianPairingScreenState extends State { final _id = TextEditingController(); bool _loading = false; int _statusReload = 0; Future _invite() async { final pairingCode = _id.text.trim().toUpperCase(); if (pairingCode.isEmpty || pairingCode.length != 8) { _snack(context, 'Pairing code harus 8 karakter dari akun User.'); return; } setState(() => _loading = true); await runFriendlyAction( () async { final res = await sl().dio.post('/shared/pairing/invite', data: { 'pairingCode': pairingCode }).timeout(const Duration(seconds: 8)); _snack( context, res.data['message']?.toString() ?? 'Invite terkirim. Minta User buka menu Pairing lalu Accept.'); setState(() => _statusReload++); }, onError: (message) => _snack(context, message), fallback: 'Invite gagal. Pastikan kamu login sebagai Guardian dan pairing code benar.', ); if (mounted) setState(() => _loading = false); } @override Widget build(BuildContext context) { return _Page( title: 'Pair User', subtitle: 'Masukkan 8 karakter pairing code aktif dari User.', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _id, textCapitalization: TextCapitalization.characters, maxLength: 8, decoration: const InputDecoration( labelText: 'Pairing Code', hintText: 'Contoh: A7K9Q2M4', prefixIcon: Icon(Icons.link), )), FilledButton.icon( onPressed: _loading ? null : _invite, icon: _loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.link), label: Text(_loading ? 'Sending...' : 'Send Invite'), ), const SizedBox(height: 20), _PairingStatusCard(key: ValueKey(_statusReload)), ], ), ); } } // --------------------------------------------------------------------------- // _PairingStatusCard (shared between both screens) // --------------------------------------------------------------------------- class _PairingStatusCard extends StatefulWidget { final bool allowUserResponse; const _PairingStatusCard({super.key, this.allowUserResponse = false}); @override State<_PairingStatusCard> createState() => _PairingStatusCardState(); } class _PairingStatusCardState extends State<_PairingStatusCard> { String _status = 'Mengecek status pairing...'; bool _active = false; bool _loading = false; bool _responding = false; Map? _data; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() => _loading = true); await runFriendlyAction( () async { final token = await sl().getAccessToken(); if (token == null || token.isEmpty) { _active = false; _data = null; _status = 'Belum login. Login dulu supaya status pairing bisa dicek.'; return; } final res = await sl() .dio .get('/shared/pairing/status') .timeout(const Duration(seconds: 5)); final data = res.data['data']; _data = data is Map ? Map.from(data) : null; _active = data is Map && data['status'] == 'ACTIVE'; if (data is Map && data['status'] == 'ACTIVE') { _active = true; _status = 'Sudah pairing dengan ${data['pairedWithName'] ?? data['pairedWithEmail'] ?? 'akun lain'}.'; } else if (data is Map && data['status'] == 'PENDING') { _status = widget.allowUserResponse ? 'Ada undangan pairing dari ${data['pairedWithName'] ?? data['pairedWithEmail'] ?? 'Guardian'}.' : 'Invite sudah terkirim. Tunggu User membuka menu Pairing lalu Accept.'; } else { _status = 'Belum pairing. Bagikan pairing code aktif ke Guardian.'; } }, onError: (message) { _active = false; _data = null; _status = message; }, fallback: 'Status pairing belum bisa dicek. Coba refresh lagi.', ); if (mounted) setState(() => _loading = false); } Future _respond(bool accept) async { final pairingId = _data?['pairingId']; if (pairingId == null) { _snack(context, 'Tidak ada invite yang bisa direspons.'); return; } setState(() => _responding = true); await runFriendlyAction( () async { final res = await sl().dio.post('/shared/pairing/respond', data: { 'pairingId': pairingId, 'accept': accept, }).timeout(const Duration(seconds: 8)); _snack( context, res.data['message']?.toString() ?? (accept ? 'Pairing diterima.' : 'Pairing ditolak.')); await _load(); }, onError: (message) => _snack(context, message), fallback: 'Gagal merespons pairing.', ); if (mounted) setState(() => _responding = false); } Future _unpair() async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Putus Pairing?'), content: const Text( 'Semua konfigurasi voice command, shortcut, dan AI config akan dihapus.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Batal')), FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFDC2626)), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Putus')), ], ), ); if (confirmed != true) return; setState(() => _responding = true); await runFriendlyAction( () async { await sl() .dio .delete('/shared/pairing/unpair') .timeout(const Duration(seconds: 8)); _snack(context, 'Pairing telah diputus.'); await _load(); }, onError: (message) => _snack(context, message), fallback: 'Gagal memutus pairing.', ); if (mounted) setState(() => _responding = false); } @override Widget build(BuildContext context) { final pending = _data?['status'] == 'PENDING'; final cardColor = _active ? const Color(0xFFF0FDF4) : pending ? AppColors.softBlueBg : const Color(0xFFFFFBEB); final accent = _active ? const Color(0xFF059669) : pending ? AppColors.primaryBlue : const Color(0xFFD97706); return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: cardColor, borderRadius: BorderRadius.circular(20), border: Border.all(color: accent.withValues(alpha: 0.28)), boxShadow: AppDecorations.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon( _active ? Icons.verified_user_outlined : pending ? Icons.mark_email_unread_outlined : Icons.info_outline, color: accent), ), const SizedBox(width: 12), Expanded( child: Text(_status, style: const TextStyle( color: AppColors.textDark, fontWeight: FontWeight.w700, height: 1.25)), ), IconButton( onPressed: _loading ? null : _load, icon: _loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.refresh)), ], ), if (widget.allowUserResponse && pending) ...[ const SizedBox(height: 12), Row( children: [ Expanded( child: FilledButton.icon( onPressed: _responding ? null : () => _respond(true), icon: const Icon(Icons.check), label: const Text('Accept'), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton.icon( onPressed: _responding ? null : () => _respond(false), icon: const Icon(Icons.close), label: const Text('Reject'), ), ), ], ), ], if (_active) ...[ const SizedBox(height: 12), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFDC2626), side: const BorderSide(color: Color(0xFFDC2626))), onPressed: _responding ? null : _unpair, icon: const Icon(Icons.link_off), label: const Text('Putus Pairing'), ), ], ], ), ); } } // --------------------------------------------------------------------------- // Shared private widgets // --------------------------------------------------------------------------- class _Page extends StatelessWidget { final String title; final String? subtitle; final Widget child; const _Page({required this.title, required this.child, this.subtitle}); @override Widget build(BuildContext context) { return SafeArea( child: DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.softBlueBg, Colors.white], ), ), child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TweenAnimationBuilder( tween: Tween(begin: 14, end: 0), duration: const Duration(milliseconds: 360), curve: Curves.easeOutCubic, builder: (_, offset, child) => Opacity( opacity: (1 - offset / 14).clamp(0.0, 1.0), child: Transform.translate( offset: Offset(0, offset), child: child), ), child: Container( width: double.infinity, padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: AppDecorations.blueGradient, borderRadius: BorderRadius.circular(24), boxShadow: AppDecorations.cardShadow, ), child: Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: const Color(0xFF38BDF8).withValues(alpha: 0.16), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF38BDF8)), ), child: const Icon(Icons.hub_outlined, color: Color(0xFFBAE6FD), size: 28), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: AppTextStyles.heading.copyWith( fontSize: 24, fontWeight: FontWeight.w900, color: Colors.white, )), if (subtitle != null) Text(subtitle!, style: const TextStyle( color: Color(0xFFCBD5E1), height: 1.25)), ], ), ), ], ), ), ), const SizedBox(height: 16), Expanded(child: FadeSlideWrapper(child: child)), ], ), ), ), ); } } class _InfoCard extends StatelessWidget { final String title; final String value; final IconData icon; final String? helper; const _InfoCard( {required this.title, required this.value, required this.icon, this.helper}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: AppDecorations.cardShadow, ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.softBlueBg, borderRadius: BorderRadius.circular(50), ), child: Icon(icon, color: AppColors.primaryBlue), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( color: AppColors.muted, fontWeight: FontWeight.w700)), SelectableText(value, style: const TextStyle( fontSize: 25, height: 1.1, letterSpacing: 1.2, fontWeight: FontWeight.w900, color: AppColors.textDark)), if (helper != null) ...[ const SizedBox(height: 6), Text(helper!, style: const TextStyle( color: AppColors.muted, fontSize: 12)), ], ])), ], ), ); } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- void _snack(BuildContext context, String message) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } } String _formatRemaining(int? seconds, DateTime? expiresAt) { final value = seconds ?? expiresAt?.difference(DateTime.now()).inSeconds; if (value == null || value <= 0) return 'sudah kadaluarsa'; final minutes = value ~/ 60; final secs = value % 60; if (minutes <= 0) return '$secs detik'; return '$minutes menit ${secs.toString().padLeft(2, '0')} detik'; }