import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../app/app_state.dart'; import '../services/api.dart'; import '../services/auth_storage.dart'; /// A dialog that handles phone number + OTP sign-in inline. /// Returns true if sign-in was successful, false if cancelled. class SignInDialog extends StatefulWidget { const SignInDialog({super.key}); /// Shows the sign-in dialog and returns true if authenticated successfully static Future show(BuildContext context) async { final result = await showDialog( context: context, barrierDismissible: false, builder: (context) => const SignInDialog(), ); return result ?? false; } @override State createState() => _SignInDialogState(); } enum _SignInStep { phone, otp } class _SignInDialogState extends State { _SignInStep _currentStep = _SignInStep.phone; final _phoneController = TextEditingController(); final _otpController = TextEditingController(); final _phoneFocus = FocusNode(); final _otpFocus = FocusNode(); String _uuid = ''; String _phone = ''; bool _isLoading = false; String? _errorMessage; @override void initState() { super.initState(); // Auto-focus the phone field when dialog opens WidgetsBinding.instance.addPostFrameCallback((_) { _phoneFocus.requestFocus(); }); } @override void dispose() { _phoneController.dispose(); _otpController.dispose(); _phoneFocus.dispose(); _otpFocus.dispose(); super.dispose(); } String _formatPhoneNumber(String input) { final digits = input.replaceAll(RegExp(r'[^\d]'), ''); if (digits.length == 11 && digits.startsWith('1')) { return digits.substring(1); } return digits; } String _formatPhoneDisplay(String phone) { if (phone.length == 10) { return '(${phone.substring(0, 3)}) ${phone.substring(3, 6)}-${phone.substring(6)}'; } return phone; } Future _handleSendOtp() async { final phone = _formatPhoneNumber(_phoneController.text); if (phone.length != 10) { setState(() { _errorMessage = 'Please enter a valid 10-digit phone number'; }); return; } setState(() { _isLoading = true; _errorMessage = null; }); try { final response = await Api.sendLoginOtp(phone: phone); if (!mounted) return; if (response.uuid.isEmpty) { setState(() { _errorMessage = 'Server error - please try again'; _isLoading = false; }); return; } setState(() { _uuid = response.uuid; _phone = phone; _currentStep = _SignInStep.otp; _isLoading = false; }); // Auto-focus the OTP field WidgetsBinding.instance.addPostFrameCallback((_) { _otpFocus.requestFocus(); }); } catch (e) { if (!mounted) return; setState(() { _errorMessage = e.toString().replaceFirst('StateError: ', ''); _isLoading = false; }); } } Future _handleVerifyOtp() async { if (_uuid.isEmpty) { setState(() { _errorMessage = 'Session expired. Please go back and try again.'; }); return; } final otp = _otpController.text.trim(); if (otp.length != 6) { setState(() { _errorMessage = 'Please enter the 6-digit code'; }); return; } setState(() { _isLoading = true; _errorMessage = null; }); try { final response = await Api.verifyLoginOtp(uuid: _uuid, otp: otp); if (!mounted) return; // Save credentials for persistent login await AuthStorage.saveAuth( userId: response.userId, token: response.token, ); // Update app state final appState = context.read(); appState.setUserId(response.userId); // Close dialog with success Navigator.of(context).pop(true); // Show welcome message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Welcome${response.userFirstName.isNotEmpty ? ', ${response.userFirstName}' : ''}!', style: const TextStyle(color: Colors.black), ), backgroundColor: const Color(0xFF90EE90), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16), ), ); } catch (e) { if (!mounted) return; setState(() { _errorMessage = e.toString().replaceFirst('StateError: ', ''); _isLoading = false; }); } } Future _handleResendOtp() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final response = await Api.sendLoginOtp(phone: _phone); if (!mounted) return; setState(() { _uuid = response.uuid; _isLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('New code sent!', style: TextStyle(color: Colors.black)), backgroundColor: Color(0xFF90EE90), behavior: SnackBarBehavior.floating, ), ); } catch (e) { if (!mounted) return; setState(() { _errorMessage = e.toString().replaceFirst('StateError: ', ''); _isLoading = false; }); } } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Row( children: [ Expanded( child: Text( _currentStep == _SignInStep.phone ? 'Sign In to Continue' : 'Enter Code', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: _isLoading ? null : () => Navigator.of(context).pop(false), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), const SizedBox(height: 8), Text( _currentStep == _SignInStep.phone ? 'Enter your phone number to add items to your cart' : 'We sent a code to ${_formatPhoneDisplay(_phone)}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey.shade600, ), ), const SizedBox(height: 24), // Form content if (_currentStep == _SignInStep.phone) _buildPhoneStep(), if (_currentStep == _SignInStep.otp) _buildOtpStep(), // Error message if (_errorMessage != null) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red.shade600, size: 20), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: TextStyle(color: Colors.red.shade800, fontSize: 13), ), ), ], ), ), ], ], ), ), ); } Widget _buildPhoneStep() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _phoneController, focusNode: _phoneFocus, decoration: InputDecoration( labelText: 'Phone Number', hintText: '(555) 123-4567', hintStyle: TextStyle(color: Colors.grey.shade400), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.phone), prefixText: '+1 ', ), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], enabled: !_isLoading, onFieldSubmitted: (_) => _handleSendOtp(), ), const SizedBox(height: 20), FilledButton( onPressed: _isLoading ? null : _handleSendOtp, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Text('Send Code'), ), ], ); } Widget _buildOtpStep() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _otpController, focusNode: _otpFocus, decoration: InputDecoration( labelText: 'Verification Code', hintText: '123456', hintStyle: TextStyle(color: Colors.grey.shade400), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.lock), ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6), ], textAlign: TextAlign.center, style: const TextStyle( fontSize: 20, letterSpacing: 6, fontWeight: FontWeight.bold, ), enabled: !_isLoading, onFieldSubmitted: (_) => _handleVerifyOtp(), ), const SizedBox(height: 20), FilledButton( onPressed: _isLoading ? null : _handleVerifyOtp, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Text('Verify & Continue'), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: _isLoading ? null : _handleResendOtp, child: const Text('Resend Code'), ), const SizedBox(width: 8), TextButton( onPressed: _isLoading ? null : () { setState(() { _currentStep = _SignInStep.phone; _otpController.clear(); _errorMessage = null; }); WidgetsBinding.instance.addPostFrameCallback((_) { _phoneFocus.requestFocus(); }); }, child: const Text('Change Number'), ), ], ), ], ); } }