Логин flow, некоторые фиксiкi

This commit is contained in:
noxzion
2026-04-01 22:50:00 +05:00
parent 8d315e7cc6
commit c22ba7cc04
9 changed files with 782 additions and 464 deletions
@@ -3,11 +3,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import '../chats/chat_list_screen.dart';
import 'password_2fa_screen.dart';
import '../../../main.dart';
class CodeConfirmationScreen extends StatefulWidget {
final String phoneNumber;
final String token;
const CodeConfirmationScreen({super.key, required this.phoneNumber});
const CodeConfirmationScreen({
super.key,
required this.phoneNumber,
required this.token,
});
@override
State<CodeConfirmationScreen> createState() => _CodeConfirmationScreenState();
@@ -53,16 +60,64 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
void _resendCode() {
if (_timerSeconds == 0) {
_startTimer();
// TODO: вызвать accountModule.resendCode
print('Resending code to ${widget.phoneNumber}');
}
}
Future<void> _verifyCode() async {
if (_codeController.text.length != 6) return;
try {
final result = await accountModule.verifyCode(
_codeController.text,
widget.token,
);
if (!mounted) return;
if (result.requiresPassword) {
final trackId = result.challengeTrackId;
if (trackId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка: отсутствуют данные для 2FA')),
);
return;
}
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => Password2FAScreen(
trackId: trackId,
hint: result.challengeHint,
),
),
);
return;
}
// Если 2FA не требуется, делаем login
final loginResult = await accountModule.login();
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const ChatListScreen()),
(route) => false,
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка: $e')),
);
}
}
void _navigateToChats() {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const ChatListScreen()),
(route) => false,
);
_verifyCode();
}
@override
@@ -118,11 +173,11 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
autofillHints: const [AutofillHints.oneTimeCode],
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(5),
LengthLimitingTextInputFormatter(6),
],
onChanged: (value) {
setState(() {});
if (value.length == 5) {
if (value.length == 6) {
_navigateToChats();
}
},
@@ -131,9 +186,9 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
),
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: FittedBox(
child: FittedBox(
child: Row(
children: List.generate(5, (index) {
children: List.generate(6, (index) {
bool isFocused = _codeController.text.length == index && _focusNode.hasFocus;
bool hasValue = _codeController.text.length > index;
String char = hasValue ? _codeController.text[index] : '';
@@ -141,7 +196,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
return Container(
width: 44,
height: 54,
margin: EdgeInsets.only(right: index == 4 ? 0 : 10),
margin: EdgeInsets.only(right: index == 5 ? 0 : 10),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
@@ -198,11 +253,11 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
children: [
FloatingActionButton(
onPressed: () {
if (_codeController.text.length == 5) {
if (_codeController.text.length == 6) {
_navigateToChats();
}
},
backgroundColor: _codeController.text.length == 5
backgroundColor: _codeController.text.length == 6
? cs.primaryContainer
: cs.surfaceContainerHighest,
elevation: 0,
@@ -211,7 +266,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
),
child: Icon(
Icons.arrow_forward,
color: _codeController.text.length == 5 ? cs.onPrimaryContainer : cs.onSurfaceVariant,
color: _codeController.text.length == 6 ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
),
],
+24 -10
View File
@@ -9,6 +9,7 @@ import 'code_confirmation_screen.dart';
import 'select_country_screen.dart';
import 'spoff_redacted_screen.dart';
import '../../widgets/custom_notification.dart';
import '../../../main.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -371,17 +372,30 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
TextButton(
onPressed: () {
onPressed: () async {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CodeConfirmationScreen(
phoneNumber:
'${_selectedCountry.phoneCode} $formattedPhone',
),
),
);
final fullPhone = '${_selectedCountry.phoneCode}${_phoneController.text}';
try {
final result = await accountModule.requestCode(fullPhone);
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CodeConfirmationScreen(
phoneNumber: '${_selectedCountry.phoneCode} $formattedPhone',
token: result.token,
),
),
);
}
} catch (e) {
if (mounted) {
showCustomNotification(context, 'Ошибка: $e');
}
}
},
child: Text(
'Готово',
@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../chats/chat_list_screen.dart';
import '../../../main.dart';
class Password2FAScreen extends StatefulWidget {
final String trackId;
final String? hint;
const Password2FAScreen({
super.key,
required this.trackId,
this.hint,
});
@override
State<Password2FAScreen> createState() => _Password2FAScreenState();
}
class _Password2FAScreenState extends State<Password2FAScreen> {
final TextEditingController _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
Future<void> _checkPassword() async {
if (_passwordController.text.isEmpty) return;
setState(() {
_isLoading = true;
});
try {
await accountModule.checkPassword(
password: _passwordController.text,
trackId: widget.trackId,
);
if (!mounted) return;
// После успешной 2FA делаем login
final loginResult = await accountModule.login();
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const ChatListScreen()),
(route) => false,
);
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Неверный пароль: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant),
onPressed: () => Navigator.pop(context),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
'Двухфакторная аутентификация',
style: GoogleFonts.inter(
color: cs.onSurface,
fontSize: 22,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Text(
'Введите пароль для завершения входа',
style: TextStyle(
color: cs.outline,
fontSize: 15,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
if (widget.hint != null) ...[
const SizedBox(height: 8),
Text(
'Подсказка: ${widget.hint}',
style: TextStyle(
color: cs.tertiary,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
const SizedBox(height: 24),
TextField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
autofocus: true,
enabled: !_isLoading,
decoration: InputDecoration(
hintText: 'Пароль',
filled: true,
fillColor: cs.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility_off : Icons.visibility,
color: cs.onSurfaceVariant,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
),
onSubmitted: (_) => _checkPassword(),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: _isLoading ? null : _checkPassword,
backgroundColor: _passwordController.text.isNotEmpty
? cs.primaryContainer
: cs.surfaceContainerHighest,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimaryContainer,
),
)
: Icon(
Icons.arrow_forward,
color: _passwordController.text.isNotEmpty
? cs.onPrimaryContainer
: cs.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 24),
],
),
),
),
);
}
}