import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart' show AuthException; import '../controllers/auth_controller.dart'; /// Which authentication flow is currently executing, so the relevant button /// can render a spinner while the request is in flight. enum _AuthAction { google, apple, microsoft, magicLink } /// Passwordless sign-in surface: federated OAuth (Google, Apple, Microsoft) /// and email magic links. Wired to [authControllerProvider] for execution and /// progress/error feedback. class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final TextEditingController _emailController = TextEditingController(); /// The action currently awaiting completion, or `null` when idle. _AuthAction? _pendingAction; static final RegExp _emailPattern = RegExp( r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$", ); @override void dispose() { _emailController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // The controller records failures in its AsyncValue state instead of // throwing, so a single listener surfaces every flow's error feedback. ref.listen>(authControllerProvider, (previous, next) { final error = next.error; if (error == null) return; _showSnack(_messageFor(error), isError: true); }); final isBusy = ref.watch(authControllerProvider).isLoading; return Scaffold( body: Center( child: SingleChildScrollView( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Icon(Icons.health_and_safety, size: 64, color: Colors.teal), const SizedBox(height: 16), const Text( 'GLP-1 Companion', textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Passwordless & Biometric Sign In', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), const SizedBox(height: 32), _oauthButton(_AuthAction.google, Icons.g_mobiledata, 'Continue with Google'), const SizedBox(height: 12), _oauthButton( _AuthAction.apple, Icons.apple, 'Continue with Apple'), const SizedBox(height: 12), _oauthButton(_AuthAction.microsoft, Icons.window, 'Continue with Microsoft'), const SizedBox(height: 24), const Row( children: [ Expanded(child: Divider()), Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text('OR'), ), Expanded(child: Divider()), ], ), const SizedBox(height: 24), TextField( controller: _emailController, enabled: !isBusy, keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], decoration: const InputDecoration( labelText: 'Email Address', border: OutlineInputBorder(), prefixIcon: Icon(Icons.email_outlined), ), ), const SizedBox(height: 12), FilledButton( onPressed: isBusy ? null : _sendMagicLink, child: _pendingAction == _AuthAction.magicLink ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Send Magic Link'), ), ], ), ), ), ), ), ); } /// Builds an OAuth button bound to [action]; disabled while any flow runs /// and swapped for a spinner while [action] itself is executing. Widget _oauthButton(_AuthAction action, IconData icon, String label) { final isBusy = ref.watch(authControllerProvider).isLoading; return ElevatedButton.icon( onPressed: isBusy ? null : () => _start(action, _runnerFor(action)), icon: _pendingAction == action ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(icon), label: Text(label), ); } /// Resolves the controller method backing [action]. Future Function() _runnerFor(_AuthAction action) { final notifier = ref.read(authControllerProvider.notifier); return switch (action) { _AuthAction.google => notifier.signInWithGoogle, _AuthAction.apple => notifier.signInWithApple, _AuthAction.microsoft => notifier.signInWithMicrosoft, _AuthAction.magicLink => () => notifier.signInWithMagicLink(_emailController.text.trim()), }; } /// Runs [execute] while marking [action] as pending. /// /// A completed flow without a recorded error counts as success; optional /// [successMessage] is surfaced via SnackBar. Errors are reported by the /// listener in [build], so they are not repeated here. Future _start( _AuthAction action, Future Function() execute, { String? successMessage, }) async { if (_pendingAction != null) return; setState(() => _pendingAction = action); try { await execute(); if (!mounted) return; final result = ref.read(authControllerProvider); if (!result.hasError && successMessage != null) { _showSnack(successMessage); } } finally { if (mounted) setState(() => _pendingAction = null); } } Future _sendMagicLink() async { final email = _emailController.text.trim(); if (!_emailPattern.hasMatch(email)) { _showSnack( 'Enter a valid email address to receive a magic link.', isError: true, ); return; } await _start( _AuthAction.magicLink, _runnerFor(_AuthAction.magicLink), successMessage: 'Magic link sent to $email. Check your inbox to finish signing in.', ); } void _showSnack(String message, {bool isError = false}) { if (!mounted) return; final messenger = ScaffoldMessenger.of(context); messenger ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Theme.of(context).colorScheme.error : null, ), ); } String _messageFor(Object error) { if (error is AuthException) return error.message; return 'Authentication failed. Please try again.'; } }