mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 13:58:49 +05:00
Initial commit
This commit is contained in:
221
lib/presentation/screens/auth/login_screen.dart
Normal file
221
lib/presentation/screens/auth/login_screen.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/auth/verify_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Account'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: 'Login'),
|
||||
Tab(text: 'Register'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_LoginForm(),
|
||||
_RegisterForm(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginForm extends StatefulWidget {
|
||||
@override
|
||||
__LoginFormState createState() => __LoginFormState();
|
||||
}
|
||||
|
||||
class __LoginFormState extends State<_LoginForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String _email = '';
|
||||
String _password = '';
|
||||
|
||||
void _submit() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
Provider.of<AuthProvider>(context, listen: false).login(_email, _password);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
if (auth.needsVerification && auth.pendingEmail != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => VerifyScreen(email: auth.pendingEmail!),
|
||||
),
|
||||
);
|
||||
auth.clearVerificationFlag();
|
||||
});
|
||||
} else if (auth.state == AuthState.authenticated) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) => value!.isEmpty ? 'Email is required' : null,
|
||||
onSaved: (value) => _email = value!,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
obscureText: true,
|
||||
validator: (value) => value!.isEmpty ? 'Password is required' : null,
|
||||
onSaved: (value) => _password = value!,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (auth.state == AuthState.loading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
if (auth.state == AuthState.error && auth.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegisterForm extends StatefulWidget {
|
||||
@override
|
||||
__RegisterFormState createState() => __RegisterFormState();
|
||||
}
|
||||
|
||||
class __RegisterFormState extends State<_RegisterForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String _name = '';
|
||||
String _email = '';
|
||||
String _password = '';
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
|
||||
try {
|
||||
await Provider.of<AuthProvider>(context, listen: false)
|
||||
.register(_name, _email, _password);
|
||||
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// Проверяем, что регистрация прошла успешно
|
||||
if (auth.state != AuthState.error) {
|
||||
// Переходим к экрану верификации
|
||||
if (mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => VerifyScreen(email: _email),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Обрабатываем ошибку, если она произошла
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Registration error: ${e.toString()}'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
validator: (value) => value!.isEmpty ? 'Name is required' : null,
|
||||
onSaved: (value) => _name = value!,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) => value!.isEmpty ? 'Email is required' : null,
|
||||
onSaved: (value) => _email = value!,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
obscureText: true,
|
||||
validator: (value) => value!.length < 6 ? 'Password must be at least 6 characters long' : null,
|
||||
onSaved: (value) => _password = value!,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (auth.state == AuthState.loading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text('Register'),
|
||||
),
|
||||
if (auth.state == AuthState.error && auth.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
159
lib/presentation/screens/auth/profile_screen.dart
Normal file
159
lib/presentation/screens/auth/profile_screen.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
|
||||
import 'package:neomovies_mobile/presentation/screens/auth/login_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../misc/licenses_screen.dart' as licenses;
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile'),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, authProvider, child) {
|
||||
switch (authProvider.state) {
|
||||
case AuthState.initial:
|
||||
case AuthState.loading:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case AuthState.unauthenticated:
|
||||
return _buildUnauthenticatedView(context);
|
||||
case AuthState.authenticated:
|
||||
return _buildAuthenticatedView(context, authProvider);
|
||||
case AuthState.error:
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Error: ${authProvider.error}'),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => authProvider.checkAuthStatus(),
|
||||
child: const Text('Try again'),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnauthenticatedView(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Please log in to continue'),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const LoginScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text('Login or Register'),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
TextButton(
|
||||
onPressed: () => _showLicensesScreen(context),
|
||||
child: const Text('Libraries licenses'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAuthenticatedView(BuildContext context, AuthProvider authProvider) {
|
||||
final user = authProvider.user!;
|
||||
final initial = user.name.isNotEmpty ? user.name[0].toUpperCase() : '?';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
child: Text(initial, style: Theme.of(context).textTheme.headlineMedium),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: Text(user.name, style: Theme.of(context).textTheme.headlineSmall),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text(user.email, style: Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => _showLicensesScreen(context),
|
||||
child: const Text('Libraries licenses'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
authProvider.logout();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
child: const Text('Logout'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
OutlinedButton(
|
||||
onPressed: () => _showDeleteConfirmationDialog(context, authProvider),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
side: BorderSide(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
child: const Text('Delete account'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(BuildContext context, AuthProvider authProvider) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Delete account'),
|
||||
content: const Text('Are you sure you want to delete your account? This action is irreversible.'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
|
||||
child: const Text('Delete'),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
authProvider.deleteAccount();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showLicensesScreen(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const licenses.LicensesScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
129
lib/presentation/screens/auth/verify_screen.dart
Normal file
129
lib/presentation/screens/auth/verify_screen.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VerifyScreen extends StatefulWidget {
|
||||
final String email;
|
||||
const VerifyScreen({super.key, required this.email});
|
||||
|
||||
@override
|
||||
State<VerifyScreen> createState() => _VerifyScreenState();
|
||||
}
|
||||
|
||||
class _VerifyScreenState extends State<VerifyScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String _code = '';
|
||||
|
||||
Timer? _timer;
|
||||
int _resendCooldown = 60;
|
||||
bool _canResend = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCooldown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCooldown() {
|
||||
_canResend = false;
|
||||
_resendCooldown = 60;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_resendCooldown > 0) {
|
||||
setState(() {
|
||||
_resendCooldown--;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_canResend = true;
|
||||
});
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _resendCode() {
|
||||
if (_canResend) {
|
||||
// Here you would call the provider to resend the code
|
||||
// For now, just restart the timer
|
||||
_startCooldown();
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
Provider.of<AuthProvider>(context, listen: false)
|
||||
.verifyEmail(widget.email, _code)
|
||||
.then((_) {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
if (auth.state != AuthState.error) {
|
||||
Navigator.of(context).pop(); // Go back to LoginScreen
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Email verified. You can now login.')),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Verify Email'),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('We sent a verification code to ${widget.email}. Enter it below.'),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Verification code'),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) => value!.isEmpty ? 'Enter code' : null,
|
||||
onSaved: (value) => _code = value!,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (auth.state == AuthState.loading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text('Verify'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextButton(
|
||||
onPressed: _canResend ? _resendCode : null,
|
||||
child: Text(
|
||||
_canResend
|
||||
? 'Resend code'
|
||||
: 'Resend code in $_resendCooldown seconds',
|
||||
),
|
||||
),
|
||||
if (auth.state == AuthState.error && auth.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user