import 'package:flutter/material.dart'; import '../../data/session/session_service.dart'; import '../../data/firebase/firebase_service.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/nostr/models/nostr_keypair.dart'; /// Screen for user session management (login/logout). class SessionScreen extends StatefulWidget { final SessionService? sessionService; final FirebaseService? firebaseService; final NostrService? nostrService; final VoidCallback? onSessionChanged; const SessionScreen({ super.key, this.sessionService, this.firebaseService, this.nostrService, this.onSessionChanged, }); @override State createState() => _SessionScreenState(); } class _SessionScreenState extends State { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _userIdController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _nostrKeyController = TextEditingController(); bool _isLoading = false; bool _useFirebaseAuth = false; bool _useNostrLogin = false; NostrKeyPair? _generatedKeyPair; @override void initState() { super.initState(); // Check if Firebase Auth is available _useFirebaseAuth = widget.firebaseService?.isEnabled == true && widget.firebaseService?.config.authEnabled == true; } @override void dispose() { _usernameController.dispose(); _userIdController.dispose(); _emailController.dispose(); _passwordController.dispose(); _nostrKeyController.dispose(); super.dispose(); } Future _handleLogin() async { if (widget.sessionService == null) return; setState(() { _isLoading = true; }); try { // Handle Nostr login if (_useNostrLogin) { final nostrKey = _nostrKeyController.text.trim(); if (nostrKey.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter nsec or npub key'), ), ); } return; } // Validate format if (!nostrKey.startsWith('nsec') && !nostrKey.startsWith('npub')) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Invalid key format. Expected nsec or npub.'), backgroundColor: Colors.red, ), ); } return; } // Login with Nostr await widget.sessionService!.loginWithNostr(nostrKey); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Nostr login successful'), ), ); setState(() {}); widget.onSessionChanged?.call(); } return; } // Handle Firebase or regular login if (_useFirebaseAuth && widget.firebaseService != null) { // Use Firebase Auth for authentication final email = _emailController.text.trim(); final password = _passwordController.text.trim(); if (email.isEmpty || password.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter email and password'), ), ); } return; } // Authenticate with Firebase final firebaseUser = await widget.firebaseService!.loginWithEmailPassword( email: email, password: password, ); // Create session with Firebase user info await widget.sessionService!.login( id: firebaseUser.uid, username: firebaseUser.email?.split('@').first ?? firebaseUser.uid, token: await firebaseUser.getIdToken(), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Login successful'), ), ); setState(() {}); // Notify parent that session state changed widget.onSessionChanged?.call(); } } else { // Simple validation mode (no Firebase Auth) final username = _usernameController.text.trim(); final userId = _userIdController.text.trim(); if (username.isEmpty || userId.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter username and user ID'), ), ); } return; } // Basic validation: require minimum length if (userId.length < 3) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('User ID must be at least 3 characters'), ), ); } return; } if (username.length < 2) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Username must be at least 2 characters'), ), ); } return; } // Create session (demo mode - no real authentication) await widget.sessionService!.login( id: userId, username: username, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Login successful (demo mode)'), backgroundColor: Colors.orange, ), ); setState(() {}); // Notify parent that session state changed widget.onSessionChanged?.call(); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Login failed: ${e.toString().replaceAll('FirebaseException: ', '').replaceAll('SessionException: ', '')}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } Future _handleLogout() async { if (widget.sessionService == null) return; setState(() { _isLoading = true; }); try { // Logout from session service first await widget.sessionService!.logout(); // Also logout from Firebase Auth if enabled if (_useFirebaseAuth && widget.firebaseService != null) { try { await widget.firebaseService!.logout(); } catch (e) { // Log error but don't fail logout - session is already cleared debugPrint('Warning: Firebase logout failed: $e'); } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Logout successful'), ), ); setState(() {}); // Notify parent that session state changed widget.onSessionChanged?.call(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Logout failed: ${e.toString().replaceAll('SessionException: ', '')}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } @override Widget build(BuildContext context) { final isLoggedIn = widget.sessionService?.isLoggedIn ?? false; final currentUser = widget.sessionService?.currentUser; return Scaffold( appBar: AppBar( title: const Text('Session'), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (isLoggedIn && currentUser != null) ...[ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Current Session', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), // Display Nostr profile if available if (currentUser.nostrProfile != null) ...[ Row( children: [ if (currentUser.nostrProfile!.picture != null) CircleAvatar( radius: 30, backgroundImage: NetworkImage( currentUser.nostrProfile!.picture!, ), onBackgroundImageError: (_, __) {}, ) else const CircleAvatar( radius: 30, child: Icon(Icons.person), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( currentUser.nostrProfile!.name ?? currentUser.nostrProfile!.displayName, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), if (currentUser.nostrProfile!.about != null) Text( currentUser.nostrProfile!.about!, style: TextStyle( fontSize: 14, color: Colors.grey[600], ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), const SizedBox(height: 12), const Divider(), const SizedBox(height: 12), ], // NIP-05 section if (currentUser.nostrProfile?.nip05 != null && currentUser.nostrProfile!.nip05!.isNotEmpty) _Nip05Section( nip05: currentUser.nostrProfile!.nip05!, publicKey: currentUser.id, nostrService: widget.nostrService, ), if (currentUser.nostrProfile?.nip05 != null && currentUser.nostrProfile!.nip05!.isNotEmpty) const SizedBox(height: 12), Text('User ID: ${currentUser.id.substring(0, currentUser.id.length > 32 ? 32 : currentUser.id.length)}${currentUser.id.length > 32 ? '...' : ''}'), Text('Username: ${currentUser.username}'), Text( 'Created: ${DateTime.fromMillisecondsSinceEpoch(currentUser.createdAt).toString().split('.')[0]}', ), ], ), ), ), const SizedBox(height: 16), ElevatedButton( onPressed: _handleLogout, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Logout'), ), ] else ...[ const Icon( Icons.person_outline, size: 64, color: Colors.grey, ), const SizedBox(height: 16), const Text( 'Login', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), if (!_useFirebaseAuth) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), child: Row( children: [ Icon(Icons.info_outline, color: Colors.orange.shade700, size: 20), const SizedBox(width: 8), Expanded( child: Text( 'Demo mode: No authentication required. Enter any valid user ID and username.', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, ), ), ), ], ), ), ], const SizedBox(height: 24), // Login method selector SegmentedButton( segments: const [ ButtonSegment( value: false, label: Text('Regular'), ), ButtonSegment( value: true, label: Text('Nostr'), ), ], selected: {_useNostrLogin}, onSelectionChanged: (Set newSelection) { setState(() { _useNostrLogin = newSelection.first; }); }, ), const SizedBox(height: 24), if (_useNostrLogin) ...[ // Key pair generation section if (widget.nostrService != null) ...[ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Generate Key Pair', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ElevatedButton.icon( onPressed: () { setState(() { _generatedKeyPair = widget.nostrService!.generateKeyPair(); }); }, icon: const Icon(Icons.refresh, size: 18), label: const Text('Generate'), ), ], ), if (_generatedKeyPair != null) ...[ const SizedBox(height: 16), // npub display Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'npub (Public Key):', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), SelectableText( _generatedKeyPair!.toNpub(), style: const TextStyle( fontSize: 11, fontFamily: 'monospace', ), ), ], ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.content_copy), tooltip: 'Copy to field', onPressed: () { _nostrKeyController.text = _generatedKeyPair!.toNpub(); }, ), ], ), const SizedBox(height: 12), // nsec display Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'nsec (Private Key):', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), SelectableText( _generatedKeyPair!.toNsec(), style: const TextStyle( fontSize: 11, fontFamily: 'monospace', ), ), ], ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.content_copy), tooltip: 'Copy to field', onPressed: () { _nostrKeyController.text = _generatedKeyPair!.toNsec(); }, ), ], ), ], ], ), ), ), const SizedBox(height: 16), ], TextField( controller: _nostrKeyController, decoration: const InputDecoration( labelText: 'Nostr Key (nsec or npub)', hintText: 'Enter your nsec or npub key', border: OutlineInputBorder(), helperText: 'Enter your Nostr private key (nsec) or public key (npub)', ), maxLines: 3, minLines: 1, ), ] else if (_useFirebaseAuth) ...[ TextField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', hintText: 'Enter your email', border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], ), const SizedBox(height: 16), TextField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', hintText: 'Enter your password', border: OutlineInputBorder(), ), obscureText: true, autofillHints: const [AutofillHints.password], ), ] else ...[ TextField( controller: _userIdController, decoration: const InputDecoration( labelText: 'User ID', hintText: 'Enter user ID (min 3 characters)', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', hintText: 'Enter username (min 2 characters)', border: OutlineInputBorder(), ), ), ], const SizedBox(height: 24), ElevatedButton( onPressed: _isLoading ? null : _handleLogin, child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Login'), ), ], ], ), ), ); } } /// Widget for displaying NIP-05 information including domain and preferred relays. class _Nip05Section extends StatefulWidget { final String nip05; final String publicKey; final NostrService? nostrService; const _Nip05Section({ required this.nip05, required this.publicKey, this.nostrService, }); @override State<_Nip05Section> createState() => _Nip05SectionState(); } class _Nip05SectionState extends State<_Nip05Section> { List _preferredRelays = []; bool _isLoading = false; String? _error; @override void initState() { super.initState(); _loadPreferredRelays(); } Future _loadPreferredRelays() async { if (widget.nostrService == null) { setState(() { _error = 'Nostr service not available'; }); return; } setState(() { _isLoading = true; _error = null; }); try { final relays = await widget.nostrService!.fetchPreferredRelaysFromNip05( widget.nip05, widget.publicKey, ); setState(() { _preferredRelays = relays; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString().replaceAll('NostrException: ', ''); _isLoading = false; }); } } String _getDomain() { final parts = widget.nip05.split('@'); if (parts.length == 2) { return parts[1]; } return widget.nip05; } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'NIP-05', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Row( children: [ const Icon(Icons.verified, size: 16, color: Colors.blue), const SizedBox(width: 8), Expanded( child: Text( widget.nip05, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 8), Text( 'Domain: ${_getDomain()}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 12), const Text( 'Preferred Relays', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), if (_isLoading) const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) else if (_error != null) Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ Icon(Icons.error_outline, size: 16, color: Colors.red.shade700), const SizedBox(width: 8), Expanded( child: Text( _error!, style: TextStyle( fontSize: 12, color: Colors.red.shade700, ), ), ), ], ), ) else if (_preferredRelays.isEmpty) Text( 'No preferred relays found', style: TextStyle( fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ) else ..._preferredRelays.map((relay) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Icon( Icons.link, size: 14, color: Colors.grey[600], ), const SizedBox(width: 8), Expanded( child: Text( relay, style: TextStyle( fontSize: 12, color: Colors.grey[700], ), ), ), ], ), )), ], ); } }