// ignore_for_file: use_build_context_synchronously, deprecated_member_use, prefer_const_constructors import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import '../../app/injection_container.dart'; import '../../core/network/api_client.dart'; import '../../core/storage/secure_storage.dart'; // --------------------------------------------------------------------------- // UserPairingScreen // --------------------------------------------------------------------------- // // Ditampilkan ke akun ROLE_USER. // - Tampilkan uniqueUserId mereka (besar, 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? _uniqueId; @override void initState() { super.initState(); _loadUniqueId(); } Future _loadUniqueId() async { var value = await sl().getUniqueUserId(); if (value == null || value.isEmpty) { try { final res = await sl() .dio .get('/user/profile') .timeout(const Duration(seconds: 5)); final data = res.data['data']; if (data is Map) value = data['uniqueUserId']?.toString(); } catch (_) {} } if (mounted) setState(() => _uniqueId = value); } @override Widget build(BuildContext context) { return _Page( title: 'Pairing', subtitle: 'Bagikan Unique ID ini ke Guardian untuk terhubung.', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (_uniqueId == null || _uniqueId!.isEmpty) _InfoCard( title: 'Your Unique ID', value: 'Login sebagai User untuk melihat ID', icon: Icons.qr_code_2) else _InfoCard( title: 'Your Unique ID', value: _uniqueId!, icon: Icons.qr_code_2), const SizedBox(height: 16), _PairingStatusCard(allowUserResponse: true), ], ), ); } } // --------------------------------------------------------------------------- // GuardianPairingScreen // --------------------------------------------------------------------------- // // Ditampilkan ke akun ROLE_GUARDIAN. // - Input field 12-char User ID. // - 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 uniqueId = _id.text.trim().toUpperCase(); if (uniqueId.isEmpty || uniqueId.length != 12) { _snack(context, 'Unique ID harus 12 karakter dari akun User.'); return; } setState(() => _loading = true); try { final res = await sl().dio.post('/shared/pairing/invite', data: {'uniqueUserId': uniqueId}).timeout(const Duration(seconds: 8)); _snack( context, res.data['message']?.toString() ?? 'Invite terkirim. Minta User buka menu Pairing lalu Accept.'); setState(() => _statusReload++); } on DioException catch (e) { _snack( context, _friendlyDioMessage(e, fallback: 'Invite gagal. Pastikan kamu login sebagai Guardian dan ID User benar.')); } on TimeoutException { _snack(context, 'Server terlalu lama merespons invite. Coba Refresh status, jangan klik berkali-kali.'); } catch (e) { _snack(context, 'Invite gagal: $e'); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return _Page( title: 'Pair User', subtitle: 'Masukkan 12 karakter Unique ID milik User.', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _id, textCapitalization: TextCapitalization.characters, maxLength: 12, decoration: const InputDecoration( labelText: 'Unique User ID', hintText: 'Contoh: AB1C2D3E4F5G', 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); try { 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 Unique ID kamu ke Guardian.'; } } on DioException catch (e) { _active = false; _data = null; _status = _friendlyDioMessage(e, fallback: 'Belum bisa mengecek server, tapi Unique ID tetap bisa dibagikan.'); } on TimeoutException { _active = false; _data = null; _status = 'Server terlalu lama merespons status pairing. Cek backend masih running dan URL server benar.'; } catch (e) { _active = false; _data = null; _status = 'Status pairing belum bisa dicek: $e'; } finally { 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); try { 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(); } on DioException catch (e) { _snack(context, _friendlyDioMessage(e, fallback: 'Gagal merespons pairing.')); } on TimeoutException { _snack(context, 'Server terlalu lama merespons pairing.'); } finally { 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); try { await sl() .dio .delete('/shared/pairing/unpair') .timeout(const Duration(seconds: 8)); _snack(context, 'Pairing telah diputus.'); await _load(); } on DioException catch (e) { _snack( context, _friendlyDioMessage(e, fallback: 'Gagal memutus pairing.')); } finally { if (mounted) setState(() => _responding = false); } } @override Widget build(BuildContext context) { final pending = _data?['status'] == 'PENDING'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: _active ? const Color(0xFFF0FDF4) : const Color(0xFFFFFBEB), borderRadius: BorderRadius.circular(12), border: Border.all( color: _active ? const Color(0xFFBBF7D0) : const Color(0xFFFDE68A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(_active ? Icons.link : Icons.info_outline, color: _active ? const Color(0xFF16A34A) : const Color(0xFFD97706)), const SizedBox(width: 12), Expanded(child: Text(_status)), 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: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith(fontWeight: FontWeight.w800)), if (subtitle != null) Text(subtitle!, style: const TextStyle(color: Color(0xFF64748B))), ], ), ), ], ), const SizedBox(height: 16), Expanded(child: child), ], ), ), ); } } class _InfoCard extends StatelessWidget { final String title; final String value; final IconData icon; const _InfoCard( {required this.title, required this.value, required this.icon}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(12)), child: Row( children: [ Icon(icon, color: const Color(0xFF1A56DB)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title), SelectableText(value, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800)) ])), ], ), ); } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- void _snack(BuildContext context, String message) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } } String _friendlyDioMessage(DioException e, {required String fallback}) { final data = e.response?.data; if (data is Map && data['message'] != null) return data['message'].toString(); if (e.response?.statusCode == 401) { return 'Sesi login habis. Logout lalu login ulang.'; } if (e.response?.statusCode == 403) { return 'Role akun tidak cocok untuk fitur ini. Pastikan User/Guardian benar.'; } if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout) { return 'Server terlalu lama merespons. Pastikan backend masih running dan URL server benar.'; } if (e.type == DioExceptionType.connectionError) { return 'Tidak bisa ke server. Di Chrome pakai http://localhost:8080. Di HP pakai IP laptop/server, bukan localhost.'; } return fallback; }