mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-04 20:12:56 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../data/repositories/auth_repository.dart';
|
||||
import '../../../data/models/user_model.dart';
|
||||
import '../../../data/services/biometric_service.dart';
|
||||
import '../../../core/services/analytics_service.dart';
|
||||
import '../../../bootstrap/supabase_client.dart';
|
||||
import '../../../core/utils/unit_conversion_utils.dart';
|
||||
import 'package:local_auth/local_auth.dart' as local_auth;
|
||||
|
||||
final authControllerProvider = StateNotifierProvider<AuthController, User?>((ref) {
|
||||
return AuthController(ref.read(authRepositoryProvider));
|
||||
});
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
return AuthRepository();
|
||||
return AuthRepository(supabaseClient);
|
||||
});
|
||||
|
||||
class AuthController extends StateNotifier<User?> {
|
||||
final AuthRepository _authRepository;
|
||||
final BiometricService _biometricService = BiometricService();
|
||||
final AnalyticsService _analytics = AnalyticsService();
|
||||
|
||||
AuthController(this._authRepository) : super(null) {
|
||||
@@ -46,8 +51,8 @@ class AuthController extends StateNotifier<User?> {
|
||||
_analytics.logSignIn(method: 'email');
|
||||
}
|
||||
|
||||
Future<void> signUpWithEmail(String email, String password, String username) async {
|
||||
await _authRepository.signUpWithEmail(email, password, username);
|
||||
Future<void> signUpWithEmail(String email, String password, String username, {double? heightCm, double? weightKg, int? age, Gender? gender, HeightUnit? heightUnit, WeightUnit? weightUnit}) async {
|
||||
await _authRepository.signUpWithEmail(email, password, username, heightCm: heightCm, weightKg: weightKg, age: age, gender: gender, heightUnit: heightUnit, weightUnit: weightUnit);
|
||||
_analytics.logSignUp(method: 'email');
|
||||
}
|
||||
|
||||
@@ -77,6 +82,12 @@ class AuthController extends StateNotifier<User?> {
|
||||
String? bio,
|
||||
String? avatarUrl,
|
||||
bool? isPublicProfile,
|
||||
double? heightCm,
|
||||
double? weightKg,
|
||||
int? age,
|
||||
Gender? gender,
|
||||
HeightUnit? heightUnit,
|
||||
WeightUnit? weightUnit,
|
||||
}) async {
|
||||
final updatedFields = <String>[];
|
||||
if (username != null) updatedFields.add('username');
|
||||
@@ -86,12 +97,24 @@ class AuthController extends StateNotifier<User?> {
|
||||
updatedFields.add('visibility');
|
||||
_analytics.logProfileVisibilityChanged(isPublic: isPublicProfile);
|
||||
}
|
||||
if (heightCm != null) updatedFields.add('height');
|
||||
if (weightKg != null) updatedFields.add('weight');
|
||||
if (age != null) updatedFields.add('age');
|
||||
if (gender != null) updatedFields.add('gender');
|
||||
if (heightUnit != null) updatedFields.add('height_unit');
|
||||
if (weightUnit != null) updatedFields.add('weight_unit');
|
||||
|
||||
await _authRepository.updateProfile(
|
||||
username: username,
|
||||
bio: bio,
|
||||
avatarUrl: avatarUrl,
|
||||
isPublicProfile: isPublicProfile,
|
||||
heightCm: heightCm,
|
||||
weightKg: weightKg,
|
||||
age: age,
|
||||
gender: gender,
|
||||
heightUnit: heightUnit,
|
||||
weightUnit: weightUnit,
|
||||
);
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
@@ -99,6 +122,98 @@ class AuthController extends StateNotifier<User?> {
|
||||
}
|
||||
}
|
||||
|
||||
// Biometric Authentication Methods
|
||||
|
||||
/// Check if biometric authentication is available
|
||||
Future<BiometricAvailability> checkBiometricAvailability() async {
|
||||
return await _biometricService.checkAvailability();
|
||||
}
|
||||
|
||||
/// Check if biometric login is enabled
|
||||
Future<bool> isBiometricEnabled() async {
|
||||
return await _biometricService.isBiometricEnabled();
|
||||
}
|
||||
|
||||
/// Enable biometric login for current user
|
||||
Future<bool> enableBiometric() async {
|
||||
final userId = currentUserId;
|
||||
if (userId == null) return false;
|
||||
|
||||
final success = await _biometricService.enableBiometric(userId);
|
||||
if (success) {
|
||||
_analytics.logEvent('biometric_enabled');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Disable biometric login
|
||||
Future<bool> disableBiometric() async {
|
||||
final success = await _biometricService.disableBiometric();
|
||||
if (success) {
|
||||
_analytics.logEvent('biometric_disabled');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Authenticate with biometrics and sign in
|
||||
Future<bool> signInWithBiometric() async {
|
||||
try {
|
||||
// Check if biometric is enabled
|
||||
final isEnabled = await _biometricService.isBiometricEnabled();
|
||||
if (!isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the stored user ID
|
||||
final biometricUserId = await _biometricService.getBiometricUserId();
|
||||
if (biometricUserId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Authenticate with biometrics
|
||||
final authenticated = await _biometricService.authenticate(
|
||||
reason: 'Sign in to your 1356 day challenge',
|
||||
localizedReason: 'Use your biometric to quickly access your challenge',
|
||||
);
|
||||
|
||||
if (authenticated) {
|
||||
// Try to restore session for the stored user
|
||||
await _authRepository.refreshSession();
|
||||
|
||||
// Verify the current user matches the stored biometric user
|
||||
final currentUser = _authRepository.currentUser;
|
||||
final currentUserId = currentUser?.id;
|
||||
if (currentUserId == biometricUserId) {
|
||||
_analytics.logSignIn(method: 'biometric');
|
||||
return true;
|
||||
} else {
|
||||
// User mismatch, disable biometric
|
||||
await disableBiometric();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get biometric status message
|
||||
Future<String> getBiometricStatusMessage() async {
|
||||
return await _biometricService.getBiometricStatusMessage();
|
||||
}
|
||||
|
||||
/// Get available biometric types
|
||||
Future<List<local_auth.BiometricType>> getAvailableBiometrics() async {
|
||||
return await _biometricService.getAvailableBiometrics();
|
||||
}
|
||||
|
||||
/// Get primary biometric type
|
||||
Future<local_auth.BiometricType?> getPrimaryBiometricType() async {
|
||||
return await _biometricService.getPrimaryBiometricType();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authRepository.dispose();
|
||||
|
||||
@@ -2,18 +2,38 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../application/auth_controller.dart';
|
||||
import '../../onboarding/application/onboarding_controller.dart';
|
||||
import '../../profile/application/profile_controller.dart';
|
||||
import 'auth_showcase_screen.dart';
|
||||
import '../../onboarding/presentation/onboarding_intro_screen.dart';
|
||||
import '../../profile/presentation/profile_setup_screen.dart';
|
||||
import '../../countdown/presentation/home_countdown_screen.dart';
|
||||
import '../../../bootstrap/supabase_client.dart';
|
||||
|
||||
class AuthGate extends ConsumerWidget {
|
||||
class AuthGate extends ConsumerStatefulWidget {
|
||||
const AuthGate({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<AuthGate> createState() => _AuthGateState();
|
||||
}
|
||||
|
||||
class _AuthGateState extends ConsumerState<AuthGate> {
|
||||
bool _isCheckingProfile = false;
|
||||
bool _profileSetupComplete = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authControllerProvider);
|
||||
final onboardingState = ref.watch(onboardingControllerProvider);
|
||||
|
||||
// If no backend is configured and there is no overridden auth state,
|
||||
// keep the app usable by continuing through the local onboarding flow.
|
||||
if (supabaseClient == null && authState == null) {
|
||||
if (!onboardingState) {
|
||||
return const OnboardingIntroScreen();
|
||||
}
|
||||
return const HomeCountdownScreen();
|
||||
}
|
||||
|
||||
if (authState == null) {
|
||||
return const AuthShowcaseScreen();
|
||||
}
|
||||
@@ -23,7 +43,29 @@ class AuthGate extends ConsumerWidget {
|
||||
return const OnboardingIntroScreen();
|
||||
}
|
||||
|
||||
// User is authenticated and has completed onboarding
|
||||
// Check if profile setup is complete
|
||||
if (!_isCheckingProfile && !_profileSetupComplete) {
|
||||
_isCheckingProfile = true;
|
||||
_checkProfileSetup(authState.id);
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
// If profile setup is not complete, show profile setup screen
|
||||
if (!_profileSetupComplete) {
|
||||
return const ProfileSetupScreen();
|
||||
}
|
||||
|
||||
// User is authenticated and has completed onboarding and profile setup
|
||||
return const HomeCountdownScreen();
|
||||
}
|
||||
|
||||
Future<void> _checkProfileSetup(String userId) async {
|
||||
final isComplete = await ref.read(profileControllerProvider.notifier).isProfileSetupComplete(userId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_profileSetupComplete = isComplete;
|
||||
_isCheckingProfile = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../application/auth_controller.dart';
|
||||
import '../../../data/services/biometric_service.dart';
|
||||
import '../../../core/widgets/app_scaffold.dart';
|
||||
import '../../../core/widgets/primary_button.dart';
|
||||
import '../../../core/utils/validators.dart';
|
||||
import '../application/auth_controller.dart';
|
||||
|
||||
class SignInScreen extends ConsumerStatefulWidget {
|
||||
const SignInScreen({super.key});
|
||||
@@ -19,8 +21,86 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final BiometricService _biometricService = BiometricService();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isBiometricAvailable = false;
|
||||
bool _isBiometricEnabled = false;
|
||||
bool _isBiometricLoading = false;
|
||||
bool _showEmailVerificationMessage = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkBiometricStatus();
|
||||
_checkIfComingFromRegistration();
|
||||
}
|
||||
|
||||
void _checkIfComingFromRegistration() async {
|
||||
// Check if user navigated from registration by checking shared preferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final justRegistered = prefs.getBool('just_registered') ?? false;
|
||||
final registrationTime = prefs.getInt('registration_time') ?? 0;
|
||||
|
||||
if (mounted) {
|
||||
// Show message if registration happened in the last 5 minutes
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final fiveMinutesAgo = now - (5 * 60 * 1000);
|
||||
|
||||
if (justRegistered && registrationTime > fiveMinutesAgo) {
|
||||
setState(() {
|
||||
_showEmailVerificationMessage = true;
|
||||
});
|
||||
// Clear the flag so it doesn't show again
|
||||
await prefs.remove('just_registered');
|
||||
await prefs.remove('registration_time');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkBiometricStatus() async {
|
||||
try {
|
||||
final availability = await _biometricService.checkAvailability();
|
||||
final isEnabled = await _biometricService.isBiometricEnabled();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBiometricAvailable = availability == BiometricAvailability.available;
|
||||
_isBiometricEnabled = isEnabled;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Biometric not available, ignore
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleBiometricSignIn() async {
|
||||
setState(() => _isBiometricLoading = true);
|
||||
try {
|
||||
final authController = ref.read(authControllerProvider.notifier);
|
||||
final success = await authController.signInWithBiometric();
|
||||
|
||||
if (success) {
|
||||
// Navigation will be handled by AuthGate
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Biometric login failed')),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Biometric login error: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isBiometricLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSignIn() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
@@ -172,6 +252,45 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email verification reminder message
|
||||
if (_showEmailVerificationMessage)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.blue.shade700, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Please verify your email before signing in. Check your inbox for the verification link.',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showEmailVerificationMessage = false;
|
||||
});
|
||||
},
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_showEmailVerificationMessage)
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 24),
|
||||
Semantics(
|
||||
label: 'Email address field',
|
||||
@@ -240,6 +359,39 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Biometric Login Button
|
||||
if (_isBiometricAvailable && _isBiometricEnabled)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isBiometricLoading ? null : _handleBiometricSignIn,
|
||||
icon: _isBiometricLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.fingerprint),
|
||||
label: Text(_isBiometricLoading ? 'Authenticating...' : 'Sign in with Biometric'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
elevation: 0,
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_isBiometricAvailable && _isBiometricEnabled)
|
||||
const SizedBox(height: 12),
|
||||
|
||||
PrimaryButton(
|
||||
onPressed: _handleSignIn,
|
||||
text: _isLoading ? 'Signing in...' : 'Sign In',
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../core/widgets/app_scaffold.dart';
|
||||
import '../../../core/widgets/primary_button.dart';
|
||||
import '../../../core/widgets/unit_input_field.dart';
|
||||
import '../../../core/utils/validators.dart';
|
||||
import '../application/auth_controller.dart';
|
||||
import '../../../core/utils/unit_conversion_utils.dart';
|
||||
|
||||
class SignUpScreen extends ConsumerStatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
@@ -21,20 +24,57 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _ageController = TextEditingController();
|
||||
Gender? _selectedGender;
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
bool _showAdditionalInfo = false;
|
||||
double? _heightCm;
|
||||
double? _weightKg;
|
||||
HeightUnit _selectedHeightUnit = HeightUnit.metric;
|
||||
WeightUnit _selectedWeightUnit = WeightUnit.metric;
|
||||
|
||||
Future<void> _handleSignUp() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final age = _ageController.text.trim().isNotEmpty
|
||||
? int.tryParse(_ageController.text.trim())
|
||||
: null;
|
||||
|
||||
await ref.read(authControllerProvider.notifier).signUpWithEmail(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text,
|
||||
_usernameController.text.trim(),
|
||||
heightCm: _heightCm,
|
||||
weightKg: _weightKg,
|
||||
age: age,
|
||||
gender: _selectedGender,
|
||||
heightUnit: _selectedHeightUnit,
|
||||
weightUnit: _selectedWeightUnit,
|
||||
);
|
||||
|
||||
// Show success message and navigate to login screen
|
||||
if (mounted) {
|
||||
// Mark that registration just happened for the login screen
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('just_registered', true);
|
||||
await prefs.setInt('registration_time', DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Account created successfully! Please check your email and verify it before signing in.'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
// Navigate to login screen after successful registration
|
||||
context.pushReplacement('/sign-in');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -54,6 +94,7 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_usernameController.dispose();
|
||||
_ageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -234,6 +275,121 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
enabled: !_isLoading,
|
||||
onFieldSubmitted: (_) => _handleSignUp(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Additional optional info section
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showAdditionalInfo = !_showAdditionalInfo;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_showAdditionalInfo
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Add more info for better recommendations',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_showAdditionalInfo) ...[
|
||||
const SizedBox(height: 16),
|
||||
UnitInputField(
|
||||
labelText: 'Height',
|
||||
prefixIcon: Icons.height_outlined,
|
||||
helperText: 'Optional: For personalized recommendations',
|
||||
enabled: !_isLoading,
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_heightCm = value;
|
||||
});
|
||||
},
|
||||
onUnitChanged: (unit) {
|
||||
setState(() {
|
||||
_selectedHeightUnit = unit as HeightUnit;
|
||||
});
|
||||
},
|
||||
isHeight: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
UnitInputField(
|
||||
labelText: 'Weight',
|
||||
prefixIcon: Icons.monitor_weight_outlined,
|
||||
helperText: 'Optional: For personalized recommendations',
|
||||
enabled: !_isLoading,
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_weightKg = value;
|
||||
});
|
||||
},
|
||||
onUnitChanged: (unit) {
|
||||
setState(() {
|
||||
_selectedWeightUnit = unit as WeightUnit;
|
||||
});
|
||||
},
|
||||
isHeight: false,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _ageController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Age',
|
||||
prefixIcon: Icon(Icons.cake_outlined),
|
||||
helperText: 'Optional: For age-appropriate recommendations',
|
||||
),
|
||||
enabled: !_isLoading,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<Gender>(
|
||||
value: _selectedGender,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Gender',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
helperText: 'Optional: For personalized recommendations',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: Gender.male,
|
||||
child: Text('Male'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: Gender.female,
|
||||
child: Text('Female'),
|
||||
),
|
||||
],
|
||||
onChanged: _isLoading ? null : (Gender? value) {
|
||||
setState(() {
|
||||
_selectedGender = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
PrimaryButton(
|
||||
onPressed: _handleSignUp,
|
||||
|
||||
Reference in New Issue
Block a user