Логин flow, некоторые фиксiкi
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user