feat(web_v2): Redesign Authorization Page to Match App Design

Task ID: WEB-001
Priority: high

Changes:

Completed by: AI Agent
Duration: 188331ms
This commit is contained in:
AI Agent 2025-11-22 16:12:38 +00:00
parent 028837c4a3
commit 7feaf5ab7d
3 changed files with 204 additions and 40 deletions

View file

@ -1,7 +1,7 @@
{
"component": "web_v2",
"current_task_id": "WEB-002",
"iteration_count": 9,
"current_task_id": "WEB-001",
"iteration_count": 10,
"max_iterations": 10,
"started_at": "2025-11-21T00:31:28.302997+00:00",
"last_commit": null,

View file

@ -335,23 +335,28 @@ class _AuthPageState extends State<AuthPage> {
IconData icon;
String statusLabel;
Color iconColor;
switch (status.state) {
case TelegramAuthCodeState.claimed:
icon = Icons.verified;
statusLabel = 'Код подтверждён';
iconColor = theme.colorScheme.secondary;
break;
case TelegramAuthCodeState.used:
icon = Icons.done_all;
statusLabel = 'Код использован';
iconColor = theme.colorScheme.secondary;
break;
case TelegramAuthCodeState.expired:
icon = Icons.timer_off;
statusLabel = 'Код истёк';
iconColor = Colors.red;
break;
case TelegramAuthCodeState.pending:
icon = Icons.hourglass_top;
statusLabel = 'Ожидаем отправку кода в боте';
iconColor = theme.colorScheme.onSurface;
break;
}
@ -375,13 +380,17 @@ class _AuthPageState extends State<AuthPage> {
children: [
Icon(
icon,
color: theme.colorScheme.onSurface,
color: iconColor,
size: 20,
),
const SizedBox(width: 8),
Text(
Expanded(
child: Text(
statusLabel,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
),
),
],
@ -392,6 +401,7 @@ class _AuthPageState extends State<AuthPage> {
style: theme.textTheme.bodyLarge?.copyWith(
fontFamily: 'monospace',
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@ -401,6 +411,7 @@ class _AuthPageState extends State<AuthPage> {
: 'Код больше не активен',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
if (status.isReadyForLogin)
@ -410,6 +421,7 @@ class _AuthPageState extends State<AuthPage> {
'Код отправлен. Нажмите «Войти» для завершения.',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
)
@ -420,6 +432,7 @@ class _AuthPageState extends State<AuthPage> {
'Получите новый код и повторите попытку.',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
)
@ -430,6 +443,7 @@ class _AuthPageState extends State<AuthPage> {
'Код уже использован для входа.',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
)
@ -440,6 +454,7 @@ class _AuthPageState extends State<AuthPage> {
'Откройте бота и отправьте код, чтобы продолжить.',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
@ -501,47 +516,66 @@ class _AuthPageState extends State<AuthPage> {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar(
title: const Text('Login'),
title: Text(
'Login',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
backgroundColor: theme.colorScheme.surface,
elevation: 0,
),
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16.0 : 24.0,
vertical: 24.0,
horizontal: isMobile ? 16.0 : 32.0,
vertical: 32.0,
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.lock_outline,
size: 80,
size: 64,
color: theme.colorScheme.onSurface,
),
const SizedBox(height: 32),
const SizedBox(height: 24),
Text(
'Sign in to your account',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Save your progress and sync data',
style: theme.textTheme.bodyLarge?.copyWith(
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Error message display
if (_errorMessage != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: 24),
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.red.withOpacity(0.3),
width: 1,
),
),
child: SelectableText.rich(
TextSpan(
children: [
@ -555,9 +589,10 @@ class _AuthPageState extends State<AuthPage> {
const WidgetSpan(child: SizedBox(width: 8)),
TextSpan(
text: _errorMessage,
style: const TextStyle(
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.w700,
fontSize: theme.textTheme.bodyMedium?.fontSize,
),
),
],
@ -575,35 +610,54 @@ class _AuthPageState extends State<AuthPage> {
? null
: () => _loginWithGoogle(context),
icon: _isLoading
? const SizedBox(
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
color: theme.colorScheme.onPrimary,
),
)
: const Icon(Icons.g_mobiledata, size: 32),
: const Icon(Icons.g_mobiledata, size: 28),
label: Text(
_isLoading ? 'Signing in...' : 'Sign in with Google',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 24),
// Divider
Row(
children: [
Expanded(child: Divider(color: theme.dividerColor)),
Expanded(
child: Divider(
color: theme.dividerColor,
thickness: 1,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'OR',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
Expanded(child: Divider(color: theme.dividerColor)),
Expanded(
child: Divider(
color: theme.dividerColor,
thickness: 1,
),
),
],
),
const SizedBox(height: 24),
@ -615,6 +669,7 @@ class _AuthPageState extends State<AuthPage> {
'Введите код из Telegram бота',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
@ -623,6 +678,7 @@ class _AuthPageState extends State<AuthPage> {
enabled: !_isLoading,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '123456',
@ -632,8 +688,31 @@ class _AuthPageState extends State<AuthPage> {
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.dividerColor,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.dividerColor,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: theme.cardColor,
prefixIcon: Icon(
Icons.code,
color: theme.colorScheme.onSurface,
),
prefixIcon: const Icon(Icons.code),
counterText: '',
),
keyboardType: TextInputType.number,
@ -649,26 +728,40 @@ class _AuthPageState extends State<AuthPage> {
onPressed: _isLoading
? null
: () => _generateTelegramCode(context),
icon: const Icon(Icons.bolt),
icon: const Icon(Icons.bolt, size: 20),
label: Text(
_isLoading ? 'Создание...' : 'Получить код',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _isLoading ? null : _openTelegramBot,
icon: const Icon(Icons.telegram),
icon: const Icon(Icons.telegram, size: 20),
label: Text(
'Открыть бота',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(
color: theme.dividerColor,
width: 1,
),
),
),
),
],
@ -687,22 +780,34 @@ class _AuthPageState extends State<AuthPage> {
onPressed: _isLoading
? null
: () => _loginWithTelegram(context),
icon: const Icon(Icons.telegram),
icon: const Icon(Icons.telegram, size: 24),
label: Text(
'Войти с кодом Telegram',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 24),
TextButton(
onPressed: _isLoading ? null : () => context.go('/home'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
),
child: Text(
'Continue as guest',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurfaceVariant,
),
),
),

View file

@ -86,8 +86,11 @@ void main() {
testWidgets('has responsive layout with max width constraint', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
// Verify the page has a ConstrainedBox with maxWidth
expect(find.byType(ConstrainedBox), findsWidgets);
// Verify the page has a ConstrainedBox with maxWidth of 480
final constrainedBox = tester.widget<ConstrainedBox>(
find.byType(ConstrainedBox).first,
);
expect(constrainedBox.constraints.maxWidth, 480);
});
testWidgets('displays divider with OR text', (tester) async {
@ -97,23 +100,31 @@ void main() {
expect(find.byType(Divider), findsWidgets);
});
testWidgets('telegram code input field is present', (tester) async {
testWidgets('telegram code input field is present with theme styling', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
expect(find.byType(TextField), findsOneWidget);
expect(find.byIcon(Icons.code), findsOneWidget);
// Verify TextField uses theme colors
final textField = tester.widget<TextField>(find.byType(TextField));
expect(textField.decoration?.filled, true);
expect(textField.decoration?.border, isNotNull);
});
testWidgets('all buttons use elevated button theme', (tester) async {
testWidgets('all buttons use elevated button theme with rounded corners', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
// Find all ElevatedButton widgets
final elevatedButtons = find.byType(ElevatedButton);
expect(elevatedButtons, findsWidgets);
// Verify buttons have proper styling
// Verify buttons have proper styling with rounded corners
final googleButton = find.widgetWithText(ElevatedButton, 'Sign in with Google');
expect(googleButton, findsOneWidget);
final buttonWidget = tester.widget<ElevatedButton>(googleButton);
expect(buttonWidget.style, isNotNull);
});
testWidgets('icon uses theme color scheme', (tester) async {
@ -123,16 +134,14 @@ void main() {
expect(iconFinder, findsOneWidget);
final iconWidget = tester.widget<Icon>(iconFinder);
// Icon should use theme color, not hardcoded AppColors.black
// Icon should use theme colorScheme.onSurface
expect(iconWidget.color, isNotNull);
expect(iconWidget.size, 64);
});
testWidgets('error message uses SelectableText.rich with red color', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
// Initially no error
expect(find.byType(SelectableText), findsWidgets);
// Verify SelectableText.rich is used (we can't easily test error state
// without mocking, but we verify the structure)
final selectableTexts = find.byType(SelectableText);
@ -142,11 +151,23 @@ void main() {
testWidgets('uses black/white minimalistic design', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
// Verify no colored containers for error messages
// Error messages should be simple SelectableText.rich, not in colored containers
final containers = find.byType(Container);
// We expect containers but not with colored backgrounds for errors
expect(containers, findsWidgets);
// Verify Scaffold uses theme surface color
final scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
expect(scaffold.backgroundColor, isNotNull);
// Verify AppBar uses theme surface color with no elevation
final appBar = tester.widget<AppBar>(find.byType(AppBar));
expect(appBar.elevation, 0);
expect(appBar.backgroundColor, isNotNull);
});
testWidgets('error message container has red background when error present', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
// Error container should not be visible initially
// This test verifies the structure exists for when errors occur
// The error container uses red.withOpacity(0.1) background
expect(find.byType(Container), findsWidgets);
});
testWidgets('telegram status card uses theme colors', (tester) async {
@ -185,16 +206,54 @@ void main() {
find.text('Sign in to your account'),
);
expect(titleWidget.style?.fontWeight, FontWeight.w700);
expect(titleWidget.style?.color, isNotNull);
final subtitleWidget = tester.widget<Text>(
find.text('Save your progress and sync data'),
);
expect(subtitleWidget.style?.fontWeight, FontWeight.w700);
expect(subtitleWidget.style?.color, isNotNull);
final telegramLabelWidget = tester.widget<Text>(
find.text('Введите код из Telegram бота'),
);
expect(telegramLabelWidget.style?.fontWeight, FontWeight.w700);
expect(telegramLabelWidget.style?.color, isNotNull);
// Check AppBar title
final appBarTitle = tester.widget<Text>(
find.text('Login'),
);
expect(appBarTitle.style?.fontWeight, FontWeight.w700);
});
testWidgets('guest button uses TextButton with theme colors', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
final guestButton = find.widgetWithText(TextButton, 'Continue as guest');
expect(guestButton, findsOneWidget);
final buttonWidget = tester.widget<TextButton>(guestButton);
expect(buttonWidget.style, isNotNull);
final buttonText = tester.widget<Text>(
find.descendant(
of: guestButton,
matching: find.text('Continue as guest'),
),
);
expect(buttonText.style?.fontWeight, FontWeight.w700);
expect(buttonText.style?.color, isNotNull);
});
testWidgets('outlined button uses theme divider color for border', (tester) async {
await tester.pumpWidget(createTestWidget(const AuthPage()));
final outlinedButton = find.widgetWithText(OutlinedButton, 'Открыть бота');
expect(outlinedButton, findsOneWidget);
final buttonWidget = tester.widget<OutlinedButton>(outlinedButton);
expect(buttonWidget.style, isNotNull);
});
});
}