feat(web_v2): Redesign Authorization Page to Match App Design
Task ID: WEB-001 Priority: high Changes: Completed by: AI Agent Duration: 147363ms
This commit is contained in:
parent
e7e48fcc12
commit
dbcc7a1ab1
4 changed files with 387 additions and 187 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"component": "web_v2",
|
"component": "web_v2",
|
||||||
"current_task_id": "WEB-003",
|
"current_task_id": "WEB-001",
|
||||||
"iteration_count": 5,
|
"iteration_count": 6,
|
||||||
"max_iterations": 10,
|
"max_iterations": 10,
|
||||||
"started_at": "2025-11-21T00:31:28.302997+00:00",
|
"started_at": "2025-11-21T00:31:28.302997+00:00",
|
||||||
"last_commit": null,
|
"last_commit": null,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"id": "WEB-001",
|
"id": "WEB-001",
|
||||||
"title": "Redesign Authorization Page to Match App Design",
|
"title": "Redesign Authorization Page to Match App Design",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"status": "pending",
|
"status": "in_progress",
|
||||||
"estimated_hours": 2.5,
|
"estimated_hours": 2.5,
|
||||||
"description": "Redesign the auth_page.dart to match the app's black/white minimalistic design with bold fonts (FontWeight.w700). Update colors, spacing, and layout to be consistent with AppTheme. Remove the test ad button and improve overall visual consistency.",
|
"description": "Redesign the auth_page.dart to match the app's black/white minimalistic design with bold fonts (FontWeight.w700). Update colors, spacing, and layout to be consistent with AppTheme. Remove the test ad button and improve overall visual consistency.",
|
||||||
"acceptance_criteria": [
|
"acceptance_criteria": [
|
||||||
|
|
@ -70,7 +70,9 @@
|
||||||
"5+ unit tests pass with >80% coverage",
|
"5+ unit tests pass with >80% coverage",
|
||||||
"Mock tests don't make real API calls"
|
"Mock tests don't make real API calls"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-002"],
|
"dependencies": [
|
||||||
|
"WEB-002"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart",
|
"mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart",
|
||||||
"mnemo_cards_web_v2/lib/domain/services/tasks_repository.dart",
|
"mnemo_cards_web_v2/lib/domain/services/tasks_repository.dart",
|
||||||
|
|
@ -96,7 +98,9 @@
|
||||||
"Error handling for upload failures",
|
"Error handling for upload failures",
|
||||||
"5+ unit tests pass with >80% coverage"
|
"5+ unit tests pass with >80% coverage"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-003"],
|
"dependencies": [
|
||||||
|
"WEB-003"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/presentation/pages/tasks/create_task_page.dart",
|
"mnemo_cards_web_v2/lib/presentation/pages/tasks/create_task_page.dart",
|
||||||
"mnemo_cards_web_v2/lib/presentation/widgets/proof_upload_widget.dart",
|
"mnemo_cards_web_v2/lib/presentation/widgets/proof_upload_widget.dart",
|
||||||
|
|
@ -143,7 +147,9 @@
|
||||||
"Accessible and responsive",
|
"Accessible and responsive",
|
||||||
"3+ widget tests pass"
|
"3+ widget tests pass"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-004"],
|
"dependencies": [
|
||||||
|
"WEB-004"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart",
|
"mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart",
|
||||||
"mnemo_cards_web_v2/lib/presentation/widgets/task_completion_dialog.dart"
|
"mnemo_cards_web_v2/lib/presentation/widgets/task_completion_dialog.dart"
|
||||||
|
|
@ -166,7 +172,9 @@
|
||||||
"Rewards persist across sessions",
|
"Rewards persist across sessions",
|
||||||
"5+ unit tests pass with >80% coverage"
|
"5+ unit tests pass with >80% coverage"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-003"],
|
"dependencies": [
|
||||||
|
"WEB-003"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/domain/state/tasks_state_manager.dart",
|
"mnemo_cards_web_v2/lib/domain/state/tasks_state_manager.dart",
|
||||||
"mnemo_cards_web_v2/lib/presentation/widgets/reward_notification_widget.dart"
|
"mnemo_cards_web_v2/lib/presentation/widgets/reward_notification_widget.dart"
|
||||||
|
|
@ -230,7 +238,9 @@
|
||||||
"Accessible and responsive",
|
"Accessible and responsive",
|
||||||
"2+ widget tests pass"
|
"2+ widget tests pass"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-002"],
|
"dependencies": [
|
||||||
|
"WEB-002"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart",
|
"mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart",
|
||||||
"mnemo_cards_web_v2/lib/presentation/router/app_router.dart"
|
"mnemo_cards_web_v2/lib/presentation/router/app_router.dart"
|
||||||
|
|
@ -254,7 +264,9 @@
|
||||||
"Navigation from task cards works",
|
"Navigation from task cards works",
|
||||||
"5+ widget tests pass"
|
"5+ widget tests pass"
|
||||||
],
|
],
|
||||||
"dependencies": ["WEB-004"],
|
"dependencies": [
|
||||||
|
"WEB-004"
|
||||||
|
],
|
||||||
"files_to_modify": [
|
"files_to_modify": [
|
||||||
"mnemo_cards_web_v2/lib/presentation/pages/tasks/task_details_page.dart",
|
"mnemo_cards_web_v2/lib/presentation/pages/tasks/task_details_page.dart",
|
||||||
"mnemo_cards_web_v2/lib/presentation/router/app_router.dart"
|
"mnemo_cards_web_v2/lib/presentation/router/app_router.dart"
|
||||||
|
|
@ -284,4 +296,4 @@
|
||||||
"component": "web_v2"
|
"component": "web_v2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ import 'package:yx_scope_flutter/yx_scope_flutter.dart';
|
||||||
import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart';
|
import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart';
|
||||||
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
||||||
import 'package:mnemo_cards_web_v2/domain/models/telegram_auth_code_status.dart';
|
import 'package:mnemo_cards_web_v2/domain/models/telegram_auth_code_status.dart';
|
||||||
import 'package:mnemo_cards_web_v2/utils/adsgram_stub.dart';
|
import 'package:mnemo_cards_web_v2/presentation/theme/app_colors.dart';
|
||||||
|
|
||||||
/// Authentication page
|
/// Authentication page
|
||||||
///
|
///
|
||||||
|
|
@ -368,7 +368,7 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|
@ -383,43 +383,69 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
statusLabel,
|
statusLabel,
|
||||||
style: TextStyle(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w700,
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
'Код: ${status.code}',
|
'Код: ${status.code}',
|
||||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 16),
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
isActive
|
isActive
|
||||||
? 'Осталось времени: ${_formatRemaining()}'
|
? 'Осталось времени: ${_formatRemaining()}'
|
||||||
: 'Код больше не активен',
|
: 'Код больше не активен',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (status.isReadyForLogin)
|
if (status.isReadyForLogin)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text('Код отправлен. Нажмите «Войти» для завершения.'),
|
child: Text(
|
||||||
|
'Код отправлен. Нажмите «Войти» для завершения.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else if (status.isExpired)
|
else if (status.isExpired)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text('Получите новый код и повторите попытку.'),
|
child: Text(
|
||||||
|
'Получите новый код и повторите попытку.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else if (status.isConsumed)
|
else if (status.isConsumed)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text('Код уже использован для входа.'),
|
child: Text(
|
||||||
|
'Код уже использован для входа.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text('Откройте бота и отправьте код, чтобы продолжить.'),
|
child: Text(
|
||||||
|
'Откройте бота и отправьте код, чтобы продолжить.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -473,181 +499,224 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showAd(int id) async {
|
|
||||||
try {
|
|
||||||
// Use the Adsgram SDK to show ad with specific block ID
|
|
||||||
await Adsgram.instance.showAdWithBlockId(id.toString());
|
|
||||||
} catch (e) {
|
|
||||||
log('Failed to show ad: $e', name: 'AuthPage');
|
|
||||||
// Fallback: show a simple alert
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Ad demo: Block ID $id')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Login')),
|
appBar: AppBar(
|
||||||
|
title: const Text('Login'),
|
||||||
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: EdgeInsets.symmetric(
|
||||||
child: Column(
|
horizontal: isMobile ? 16.0 : 24.0,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
vertical: 24.0,
|
||||||
children: [
|
),
|
||||||
const Icon(Icons.lock_outline, size: 80),
|
child: ConstrainedBox(
|
||||||
const SizedBox(height: 32),
|
constraints: const BoxConstraints(maxWidth: 500),
|
||||||
const Text(
|
child: Column(
|
||||||
'Sign in to your account',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
),
|
children: [
|
||||||
const SizedBox(height: 8),
|
const Icon(
|
||||||
const Text(
|
Icons.lock_outline,
|
||||||
'Save your progress and sync data',
|
size: 80,
|
||||||
textAlign: TextAlign.center,
|
color: AppColors.black,
|
||||||
style: TextStyle(fontSize: 16),
|
),
|
||||||
),
|
const SizedBox(height: 32),
|
||||||
ListTile(
|
Text(
|
||||||
title: Text('show ad'),
|
'Sign in to your account',
|
||||||
onTap: () {
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
showAd(16505);
|
fontWeight: FontWeight.w700,
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
// Error message display
|
|
||||||
if (_errorMessage != null)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
margin: const EdgeInsets.only(bottom: 24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.red.shade200),
|
|
||||||
),
|
),
|
||||||
child: SelectableText.rich(
|
textAlign: TextAlign.center,
|
||||||
TextSpan(
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Save your progress and sync data',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
// Error message display
|
||||||
|
if (_errorMessage != null) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
margin: const EdgeInsets.only(bottom: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.red.shade200),
|
||||||
|
),
|
||||||
|
child: SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const WidgetSpan(
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const WidgetSpan(child: SizedBox(width: 8)),
|
||||||
|
TextSpan(
|
||||||
|
text: _errorMessage,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Login buttons
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _isLoading
|
||||||
|
? null
|
||||||
|
: () => _loginWithGoogle(context),
|
||||||
|
icon: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.g_mobiledata, size: 32),
|
||||||
|
label: Text(
|
||||||
|
_isLoading ? 'Signing in...' : 'Sign in with Google',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Divider
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider(color: theme.dividerColor)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'OR',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: theme.dividerColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Telegram auth code input
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Введите код из Telegram бота',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _telegramCodeController,
|
||||||
|
enabled: !_isLoading,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '123456',
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: theme.hintColor,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
prefixIcon: const Icon(Icons.code),
|
||||||
|
counterText: '',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
maxLength: 6,
|
||||||
|
onSubmitted: (_) => _loginWithTelegram(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const WidgetSpan(
|
Expanded(
|
||||||
child: Icon(
|
child: ElevatedButton.icon(
|
||||||
Icons.error_outline,
|
onPressed: _isLoading
|
||||||
color: Colors.red,
|
? null
|
||||||
size: 20,
|
: () => _generateTelegramCode(context),
|
||||||
|
icon: const Icon(Icons.bolt),
|
||||||
|
label: Text(
|
||||||
|
_isLoading ? 'Создание...' : 'Получить код',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const WidgetSpan(child: SizedBox(width: 8)),
|
const SizedBox(width: 12),
|
||||||
TextSpan(
|
Expanded(
|
||||||
text: _errorMessage,
|
child: OutlinedButton.icon(
|
||||||
style: const TextStyle(color: Colors.red),
|
onPressed: _isLoading ? null : _openTelegramBot,
|
||||||
|
icon: const Icon(Icons.telegram),
|
||||||
|
label: Text(
|
||||||
|
'Открыть бота',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
if (_webCodeStatus != null) ...[
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 24),
|
_buildTelegramStatusCard(context),
|
||||||
// Login buttons
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 56,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () => _loginWithGoogle(context),
|
|
||||||
icon: _isLoading
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.g_mobiledata, size: 32),
|
|
||||||
label: Text(
|
|
||||||
_isLoading ? 'Signing in...' : 'Sign in with Google',
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Telegram auth code input
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Введите код из Telegram бота',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: _telegramCodeController,
|
|
||||||
enabled: !_isLoading,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '123456',
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(Icons.code),
|
|
||||||
counterText: '',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
maxLength: 6,
|
|
||||||
onSubmitted: (_) => _loginWithTelegram(context),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () => _generateTelegramCode(context),
|
|
||||||
icon: const Icon(Icons.bolt),
|
|
||||||
label: Text(
|
|
||||||
_isLoading ? 'Создание...' : 'Получить код',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: _isLoading ? null : _openTelegramBot,
|
|
||||||
icon: const Icon(Icons.telegram),
|
|
||||||
label: const Text('Открыть бота'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
if (_webCodeStatus != null) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildTelegramStatusCard(context),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 16),
|
SizedBox(
|
||||||
SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: 56,
|
||||||
height: 56,
|
child: ElevatedButton.icon(
|
||||||
child: ElevatedButton.icon(
|
onPressed: _isLoading
|
||||||
onPressed: _isLoading
|
? null
|
||||||
? null
|
: () => _loginWithTelegram(context),
|
||||||
: () => _loginWithTelegram(context),
|
icon: const Icon(Icons.telegram),
|
||||||
icon: const Icon(Icons.telegram),
|
label: Text(
|
||||||
label: const Text(
|
'Войти с кодом Telegram',
|
||||||
'Войти с кодом Telegram',
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
style: TextStyle(fontSize: 18),
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 32),
|
TextButton(
|
||||||
TextButton(
|
onPressed: _isLoading ? null : () => context.go('/home'),
|
||||||
onPressed: _isLoading ? null : () => context.go('/home'),
|
child: Text(
|
||||||
child: const Text(
|
'Continue as guest',
|
||||||
'Continue as guest',
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
style: TextStyle(fontSize: 16),
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/presentation/pages/auth/auth_page.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/presentation/theme/app_theme.dart';
|
||||||
|
import 'package:yx_scope_flutter/yx_scope_flutter.dart';
|
||||||
|
|
||||||
|
// Mock classes
|
||||||
|
class MockAppScopeContainer extends Mock implements AppScopeContainer {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MockAppScopeContainer mockAppScope;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockAppScope = MockAppScopeContainer();
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget createTestWidget(Widget child) {
|
||||||
|
return ScopeProvider<AppScopeContainer>(
|
||||||
|
scope: mockAppScope,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.light,
|
||||||
|
home: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('AuthPage', () {
|
||||||
|
testWidgets('displays login title and description', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.text('Sign in to your account'), findsOneWidget);
|
||||||
|
expect(find.text('Save your progress and sync data'), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.lock_outline), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('displays Google sign in button', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.text('Sign in with Google'), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.g_mobiledata), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('displays Telegram auth section', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.text('Введите код из Telegram бота'), findsOneWidget);
|
||||||
|
expect(find.text('Получить код'), findsOneWidget);
|
||||||
|
expect(find.text('Открыть бота'), findsOneWidget);
|
||||||
|
expect(find.text('Войти с кодом Telegram'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('displays continue as guest button', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.text('Continue as guest'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('displays error message when present', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
// Initially no error
|
||||||
|
expect(find.byIcon(Icons.error_outline), findsNothing);
|
||||||
|
|
||||||
|
// We can't easily test error state without triggering the actual login
|
||||||
|
// This test verifies the UI structure is correct
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('all text uses FontWeight.w700', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
// Find all Text widgets and verify they use FontWeight.w700
|
||||||
|
final textWidgets = find.byType(Text);
|
||||||
|
expect(textWidgets, findsWidgets);
|
||||||
|
|
||||||
|
// Check that title uses bold font
|
||||||
|
final titleFinder = find.text('Sign in to your account');
|
||||||
|
expect(titleFinder, findsOneWidget);
|
||||||
|
|
||||||
|
final titleWidget = tester.widget<Text>(titleFinder);
|
||||||
|
expect(titleWidget.style?.fontWeight, FontWeight.w700);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('displays divider with OR text', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.text('OR'), findsOneWidget);
|
||||||
|
expect(find.byType(Divider), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('telegram code input field is present', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.code), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('all buttons use elevated button theme', (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
|
||||||
|
final googleButton = find.widgetWithText(ElevatedButton, 'Sign in with Google');
|
||||||
|
expect(googleButton, findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue