import 'dart:io' as io; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; import '../../core/exceptions.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/settings_provider.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { bool _loadingStatus = true; bool _oauthEnabled = false; bool _localAuthEnabled = false; final _formKey = GlobalKey(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); bool _loading = false; bool _obscure = true; String? _error; @override void initState() { super.initState(); _loadStatus(); } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); super.dispose(); } Future _loadStatus() async { try { final status = await ref.read(authRepositoryProvider).getStatus(); if (!mounted) return; setState(() { _oauthEnabled = status['oauth_enabled'] as bool? ?? false; _localAuthEnabled = status['local_auth_enabled'] as bool? ?? true; _loadingStatus = false; }); } catch (_) { if (!mounted) return; setState(() { _localAuthEnabled = true; _loadingStatus = false; }); } } Future _localLogin() async { if (!_formKey.currentState!.validate()) return; setState(() { _loading = true; _error = null; }); try { await ref.read(authProvider.notifier).login( _usernameController.text.trim(), _passwordController.text, ); if (mounted) context.go(Routes.notes); } on AuthException catch (e) { setState(() => _error = e.message); } on AppException catch (e) { setState(() => _error = e.message); } finally { if (mounted) setState(() => _loading = false); } } void _openOAuth() { final serverUrl = ref.read(serverUrlProvider) ?? ''; Navigator.of(context).push(MaterialPageRoute( builder: (_) => _OAuthWebView( serverUrl: serverUrl, cookieJar: ref.read(cookieJarProvider), onSuccess: () async { await ref.read(authProvider.notifier).verify(); if (mounted) context.go(Routes.notes); }, ), )); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.all(24), child: _loadingStatus ? const Center(child: CircularProgressIndicator()) : Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Sign In', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 32), if (_oauthEnabled) ...[ FilledButton.icon( icon: const Icon(Icons.login), label: const Text('Sign in with SSO'), onPressed: _openOAuth, ), ], if (_oauthEnabled && _localAuthEnabled) ...[ const SizedBox(height: 16), const Row(children: [ Expanded(child: Divider()), Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: Text('or'), ), Expanded(child: Divider()), ]), const SizedBox(height: 16), ], if (_localAuthEnabled) ...[ TextFormField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', border: OutlineInputBorder()), textInputAction: TextInputAction.next, autocorrect: false, validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: 'Password', border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off), onPressed: () => setState(() => _obscure = !_obscure), ), ), obscureText: _obscure, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _localLogin(), validator: (v) => (v == null || v.isEmpty) ? 'Required' : null, ), if (_error != null) ...[ const SizedBox(height: 8), Text(_error!, style: TextStyle( color: Theme.of(context).colorScheme.error)), ], const SizedBox(height: 24), FilledButton( onPressed: _loading ? null : _localLogin, child: _loading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2), ) : const Text('Sign In'), ), ], if (!_oauthEnabled && !_localAuthEnabled) Text( 'No sign-in method is available. Check server configuration.', style: TextStyle( color: Theme.of(context).colorScheme.error), textAlign: TextAlign.center, ), const SizedBox(height: 12), TextButton( onPressed: () => context.go(Routes.setup), child: const Text('Change server'), ), ], ), ), ), ), ); } } // --------------------------------------------------------------------------- // In-app WebView that handles the server-side OAuth redirect flow. // --------------------------------------------------------------------------- class _OAuthWebView extends StatefulWidget { final String serverUrl; final dynamic cookieJar; // PersistCookieJar — avoid importing cookie_jar here final Future Function() onSuccess; const _OAuthWebView({ required this.serverUrl, required this.cookieJar, required this.onSuccess, }); @override State<_OAuthWebView> createState() => _OAuthWebViewState(); } class _OAuthWebViewState extends State<_OAuthWebView> { bool _completing = false; Future _handleUrl(String url) async { if (_completing) return; // The server redirects to "/" on successful OAuth completion. final root = widget.serverUrl.endsWith('/') ? widget.serverUrl : '${widget.serverUrl}/'; if (url == widget.serverUrl || url == root) { _completing = true; // Copy cookies from Android WebView store → Dio PersistCookieJar. final cookieManager = CookieManager.instance(); final wvCookies = await cookieManager.getCookies(url: WebUri(widget.serverUrl)); final serverUri = Uri.parse(widget.serverUrl); final ioCookies = wvCookies.map((c) { final dc = io.Cookie(c.name, c.value.toString()); var domain = c.domain ?? serverUri.host; if (domain.startsWith('.')) domain = domain.substring(1); dc.domain = domain; dc.path = c.path ?? '/'; return dc; }).toList(); await widget.cookieJar.saveFromResponse(serverUri, ioCookies); if (mounted) { Navigator.of(context).pop(); await widget.onSuccess(); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Sign in'), leading: const CloseButton(), ), body: InAppWebView( initialUrlRequest: URLRequest( url: WebUri('${widget.serverUrl}/api/auth/oauth/login'), ), onLoadStop: (controller, url) async { if (url != null) await _handleUrl(url.toString()); }, ), ); } }