Files
iiEsaywebUIapp/lib/features/auth/views/authentication_page.dart

634 lines
20 KiB
Dart
Raw Normal View History

2025-08-16 15:51:27 +05:30
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/server_config.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/services/input_validation_service.dart';
import '../../../core/services/navigation_service.dart';
import '../../../core/widgets/error_boundary.dart';
import '../../../shared/services/brand_service.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../../core/auth/auth_state_manager.dart';
2025-08-20 22:15:26 +05:30
import '../../../core/utils/debug_logger.dart';
2025-08-16 15:51:27 +05:30
class AuthenticationPage extends ConsumerStatefulWidget {
final ServerConfig serverConfig;
2025-08-20 22:15:26 +05:30
const AuthenticationPage({super.key, required this.serverConfig});
2025-08-16 15:51:27 +05:30
@override
ConsumerState<AuthenticationPage> createState() => _AuthenticationPageState();
}
class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _apiKeyController = TextEditingController();
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
bool _obscurePassword = true;
bool _useApiKey = false;
String? _loginError;
bool _isSigningIn = false;
@override
void initState() {
super.initState();
_loadSavedCredentials();
}
Future<void> _loadSavedCredentials() async {
final storage = ref.read(optimizedStorageServiceProvider);
final savedCredentials = await storage.getSavedCredentials();
if (savedCredentials != null) {
setState(() {
_usernameController.text = savedCredentials['username'] ?? '';
});
}
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
_apiKeyController.dispose();
super.dispose();
}
Future<void> _signIn() async {
if (!_formKey.currentState!.validate()) return;
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
setState(() {
_isSigningIn = true;
_loginError = null;
});
try {
final authManager = ref.read(authStateManagerProvider.notifier);
bool success;
if (_useApiKey) {
success = await authManager.loginWithApiKey(
_apiKeyController.text.trim(),
rememberCredentials: true, // Consistent with credentials method
);
} else {
success = await authManager.login(
_usernameController.text.trim(),
_passwordController.text,
rememberCredentials: true,
);
}
if (!success) {
final authState = ref.read(authStateManagerProvider);
throw Exception(authState.error ?? 'Login failed');
}
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// Success - navigation will be handled by auth state change
} catch (e) {
setState(() {
_loginError = _formatLoginError(e.toString());
});
} finally {
if (mounted) {
setState(() {
_isSigningIn = false;
});
}
}
}
String _formatLoginError(String error) {
if (error.contains('401') || error.contains('Unauthorized')) {
return 'Invalid username or password. Please try again.';
} else if (error.contains('redirect')) {
return 'The server is redirecting requests. Check your server\'s HTTPS configuration.';
} else if (error.contains('SocketException')) {
return 'Unable to connect to server. Please check your connection.';
} else if (error.contains('timeout')) {
return 'The request timed out. Please try again.';
}
return 'We couldn\'t sign you in. Check your credentials and server settings.';
}
@override
Widget build(BuildContext context) {
// Listen for auth state changes to navigate on successful login
ref.listen<AuthState>(authStateManagerProvider, (previous, next) {
2025-08-20 22:15:26 +05:30
if (mounted &&
next.isAuthenticated &&
previous?.isAuthenticated != true) {
DebugLogger.auth(
'Authentication successful, initializing background resources',
);
2025-08-17 16:11:19 +05:30
// Model selection and onboarding will be handled by the chat page
// to avoid widget disposal issues
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Navigating to chat page');
2025-08-16 15:51:27 +05:30
// Navigate directly to chat page on successful authentication
Navigator.of(context).pushNamedAndRemoveUntil(
Routes.chat,
(route) => false, // Remove all previous routes
);
}
});
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
return ErrorBoundary(
child: Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.pagePadding,
vertical: Spacing.lg,
),
child: Column(
children: [
// Header with progress indicator
_buildHeader(),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
const SizedBox(height: Spacing.extraLarge),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// Main content
Expanded(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Server connection status
_buildServerStatus(),
const SizedBox(height: Spacing.sectionGap),
// Welcome section
_buildWelcomeSection(),
const SizedBox(height: Spacing.sectionGap),
// Authentication form
_buildAuthForm(),
],
),
),
),
),
),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// Bottom action button
_buildSignInButton(),
],
),
),
),
),
);
}
Widget _buildHeader() {
return Row(
children: [
ConduitIconButton(
icon: Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back,
onPressed: () => Navigator.pop(context),
tooltip: 'Back to server setup',
),
const Spacer(),
// Progress indicator (step 2 of 2)
Row(
children: [
Container(
width: 32,
height: 6,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.round),
),
),
const SizedBox(width: Spacing.xs),
Container(
width: 32,
height: 6,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.round),
),
),
],
),
const Spacer(),
const SizedBox(width: TouchTarget.minimum), // Balance the back button
],
);
}
Widget _buildServerStatus() {
return ConduitCard(
isElevated: false,
padding: const EdgeInsets.all(Spacing.lg),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: context.conduitTheme.successBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.round),
border: Border.all(
color: context.conduitTheme.success.withValues(alpha: 0.3),
width: BorderWidth.standard,
),
),
child: Icon(
2025-08-20 22:15:26 +05:30
Platform.isIOS
? CupertinoIcons.checkmark_circle_fill
: Icons.check_circle,
2025-08-16 15:51:27 +05:30
color: context.conduitTheme.success,
size: IconSize.medium,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Connected to Server',
style: context.conduitTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.conduitTheme.success,
),
),
const SizedBox(height: Spacing.xs),
Text(
Uri.parse(widget.serverConfig.url).host,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
fontFamily: 'monospace',
),
),
],
),
),
],
),
).animate().slideX(
begin: -0.05,
duration: AnimationDuration.messageSlide,
curve: Curves.easeOutCubic,
);
}
Widget _buildWelcomeSection() {
return Column(
children: [
BrandService.createBrandIcon(
size: 48,
useGradient: true,
addShadow: true,
).animate().scale(
duration: AnimationDuration.pageTransition,
curve: Curves.easeOutBack,
),
const SizedBox(height: Spacing.lg),
Text(
'Sign In',
textAlign: TextAlign.center,
style: context.conduitTheme.headingLarge?.copyWith(
fontWeight: FontWeight.w700,
height: 1.2,
),
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.microInteraction,
),
const SizedBox(height: Spacing.sm),
Text(
'Enter your credentials to access your AI conversations',
textAlign: TextAlign.center,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textSecondary,
height: 1.5,
),
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.fast,
),
],
);
}
Widget _buildAuthForm() {
return ConduitCard(
isElevated: true,
padding: const EdgeInsets.all(Spacing.xl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Authentication mode toggle
_buildAuthModeToggle(),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
const SizedBox(height: Spacing.lg),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// Authentication form fields
_buildAuthFields(),
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
if (_loginError != null) ...[
const SizedBox(height: Spacing.md),
_buildErrorMessage(_loginError!),
],
],
),
);
}
Widget _buildAuthModeToggle() {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: context.conduitTheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppBorderRadius.button),
border: Border.all(
color: context.conduitTheme.dividerColor,
width: BorderWidth.standard,
),
),
child: Row(
children: [
Expanded(
child: _buildAuthToggleOption(
2025-08-20 22:15:26 +05:30
icon: Platform.isIOS
? CupertinoIcons.person_circle
: Icons.account_circle_outlined,
2025-08-16 15:51:27 +05:30
label: 'Credentials',
isSelected: !_useApiKey,
onTap: () => setState(() => _useApiKey = false),
),
),
Expanded(
child: _buildAuthToggleOption(
2025-08-20 22:15:26 +05:30
icon: Platform.isIOS
? CupertinoIcons.lock_shield
: Icons.vpn_key_outlined,
2025-08-16 15:51:27 +05:30
label: 'API Key',
isSelected: _useApiKey,
onTap: () => setState(() => _useApiKey = true),
),
),
],
),
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.microInteraction,
);
}
Widget _buildAuthToggleOption({
required IconData icon,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return AnimatedContainer(
duration: AnimationDuration.microInteraction,
curve: Curves.easeInOutCubic,
child: Material(
color: isSelected
? context.conduitTheme.buttonPrimary
: Colors.transparent,
borderRadius: BorderRadius.circular(AppBorderRadius.button - 2),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.button - 2),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: Spacing.md,
horizontal: Spacing.sm,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: IconSize.small,
color: isSelected
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.iconSecondary,
),
const SizedBox(width: Spacing.sm),
Text(
label,
style: context.conduitTheme.bodyMedium?.copyWith(
color: isSelected
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textSecondary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
],
),
),
),
),
);
}
Widget _buildAuthFields() {
return AnimatedSwitcher(
duration: AnimationDuration.pageTransition,
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: _useApiKey ? _buildApiKeyForm() : _buildCredentialsForm(),
);
}
Widget _buildApiKeyForm() {
return Column(
key: const ValueKey('api_key_form'),
children: [
AccessibleFormField(
label: 'API Key',
hint: 'sk-...',
controller: _apiKeyController,
validator: InputValidationService.combine([
InputValidationService.validateRequired,
(value) => InputValidationService.validateMinLength(
value,
10,
fieldName: 'API Key',
),
]),
obscureText: _obscurePassword,
semanticLabel: 'Enter your API key',
prefixIcon: Icon(
2025-08-20 22:15:26 +05:30
Platform.isIOS
? CupertinoIcons.lock_shield
: Icons.vpn_key_outlined,
2025-08-16 15:51:27 +05:30
color: context.conduitTheme.iconSecondary,
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
2025-08-20 22:15:26 +05:30
? (Platform.isIOS
? CupertinoIcons.eye_slash
: Icons.visibility_off)
2025-08-16 15:51:27 +05:30
: (Platform.isIOS ? CupertinoIcons.eye : Icons.visibility),
color: context.conduitTheme.iconSecondary,
),
2025-08-20 22:15:26 +05:30
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
2025-08-16 15:51:27 +05:30
),
onSubmitted: (_) => _signIn(),
isRequired: true,
autofillHints: const [AutofillHints.password],
),
],
);
}
Widget _buildCredentialsForm() {
return Column(
key: const ValueKey('credentials_form'),
children: [
AccessibleFormField(
label: 'Username or Email',
hint: 'Enter your username or email',
controller: _usernameController,
validator: InputValidationService.combine([
InputValidationService.validateRequired,
(value) => InputValidationService.validateEmailOrUsername(value),
]),
keyboardType: TextInputType.emailAddress,
semanticLabel: 'Enter your username or email',
prefixIcon: Icon(
Platform.isIOS ? CupertinoIcons.person : Icons.person_outline,
color: context.conduitTheme.iconSecondary,
),
2025-08-20 22:15:26 +05:30
autofillHints: const [AutofillHints.username, AutofillHints.email],
2025-08-16 15:51:27 +05:30
isRequired: true,
),
const SizedBox(height: Spacing.lg),
AccessibleFormField(
label: 'Password',
hint: 'Enter your password',
controller: _passwordController,
validator: InputValidationService.combine([
InputValidationService.validateRequired,
(value) => InputValidationService.validateMinLength(
value,
1,
fieldName: 'Password',
),
]),
obscureText: _obscurePassword,
semanticLabel: 'Enter your password',
prefixIcon: Icon(
Platform.isIOS ? CupertinoIcons.lock : Icons.lock_outline,
color: context.conduitTheme.iconSecondary,
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
2025-08-20 22:15:26 +05:30
? (Platform.isIOS
? CupertinoIcons.eye_slash
: Icons.visibility_off)
2025-08-16 15:51:27 +05:30
: (Platform.isIOS ? CupertinoIcons.eye : Icons.visibility),
color: context.conduitTheme.iconSecondary,
),
2025-08-20 22:15:26 +05:30
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
2025-08-16 15:51:27 +05:30
),
onSubmitted: (_) => _signIn(),
autofillHints: const [AutofillHints.password],
isRequired: true,
),
],
);
}
Widget _buildSignInButton() {
return Padding(
padding: const EdgeInsets.only(top: Spacing.lg),
2025-08-20 22:15:26 +05:30
child:
ConduitButton(
text: _isSigningIn
? 'Signing in...'
: _useApiKey
? 'Sign in with API Key'
2025-08-16 15:51:27 +05:30
: 'Sign In',
2025-08-20 22:15:26 +05:30
icon: _isSigningIn
? null
: (Platform.isIOS
? CupertinoIcons.arrow_right
: Icons.arrow_forward),
onPressed: _isSigningIn ? null : _signIn,
isLoading: _isSigningIn,
isFullWidth: true,
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.fast,
),
2025-08-16 15:51:27 +05:30
);
}
Widget _buildErrorMessage(String message) {
return Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: context.conduitTheme.errorBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.button),
border: Border.all(
color: context.conduitTheme.error.withValues(alpha: 0.3),
width: BorderWidth.standard,
),
),
child: Row(
children: [
Icon(
2025-08-20 22:15:26 +05:30
Platform.isIOS
? CupertinoIcons.exclamationmark_circle_fill
2025-08-16 15:51:27 +05:30
: Icons.error_outline,
color: context.conduitTheme.error,
size: IconSize.medium,
),
const SizedBox(width: Spacing.md),
Expanded(
child: Text(
message,
style: context.conduitTheme.bodyMedium?.copyWith(
color: context.conduitTheme.error,
),
),
),
],
),
).animate().slideX(
begin: 0.05,
duration: AnimationDuration.messageSlide,
curve: Curves.easeOutCubic,
);
}
2025-08-20 22:15:26 +05:30
}