stuff
This commit is contained in:
parent
9b02605ebe
commit
0cfcc0f600
5 changed files with 197 additions and 109 deletions
|
|
@ -1 +1 @@
|
|||
1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf
asset1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf2packages/cupertino_icons/assets/CupertinoIcons.ttf
asset2packages/cupertino_icons/assets/CupertinoIcons.ttf
|
||||
1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf
asset1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf
icons/ad.webp
asset
icons/ad.webpicons/back.webp
asseticons/back.webpicons/cards.webp
asseticons/cards.webpicons/chat.webp
asseticons/chat.webpicons/clock.webp
asseticons/clock.webpicons/crown.webp
asseticons/crown.webpicons/down.webp
asseticons/down.webpicons/download.webp
asseticons/download.webpicons/exit.webp
asseticons/exit.webpicons/exit2.webp
asseticons/exit2.webpicons/exit3.webp
asseticons/exit3.webpicons/gift.webp
asseticons/gift.webpicons/google.webp
asseticons/google.webpicons/heart.webp
asseticons/heart.webpicons/heart_fill.webp
asset1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf
asset1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttfasset1../mnemo_cards/fonts/Nunito-VariableFont_wght.ttfasseticons/mic_off.webpicons/mic_on.webp
asseticons/mic_on.webpicons/next.webp
asseticons/next.webpicons/profile.webp
asseticons/profile.webpicons/settings.webp
asseticons/settings.webpicons/shuffle.webp
asseticons/shuffle.webpicons/skull.webp
asseticons/skull.webpicons/small_circle.webp
asseticons/small_circle.webpicons/sound_off.webp
asseticons/sound_off.webpicons/sound_on.webp
asseticons/sound_on.webpicons/telegram.webp
asseticons/telegram.webp
icons/tg.webp
asset
icons/tg.webpicons/view.webp
asseticons/view.webp2packages/cupertino_icons/assets/CupertinoIcons.ttf
asset2packages/cupertino_icons/assets/CupertinoIcons.ttf
|
||||
|
|
@ -1 +1 @@
|
|||
{"../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf":["../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
|
||||
{"../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf":["../mnemo_cards/fonts/Nunito-VariableFont_wght.ttf"],"icons/ad.webp":["icons/ad.webp"],"icons/back.webp":["icons/back.webp"],"icons/cards.webp":["icons/cards.webp"],"icons/chat.webp":["icons/chat.webp"],"icons/clock.webp":["icons/clock.webp"],"icons/crown.webp":["icons/crown.webp"],"icons/down.webp":["icons/down.webp"],"icons/download.webp":["icons/download.webp"],"icons/exit.webp":["icons/exit.webp"],"icons/exit2.webp":["icons/exit2.webp"],"icons/exit3.webp":["icons/exit3.webp"],"icons/gift.webp":["icons/gift.webp"],"icons/google.webp":["icons/google.webp"],"icons/heart.webp":["icons/heart.webp"],"icons/heart_fill.webp":["icons/heart_fill.webp"],"icons/lock.webp":["icons/lock.webp"],"icons/mic_off.webp":["icons/mic_off.webp"],"icons/mic_on.webp":["icons/mic_on.webp"],"icons/next.webp":["icons/next.webp"],"icons/profile.webp":["icons/profile.webp"],"icons/settings.webp":["icons/settings.webp"],"icons/shuffle.webp":["icons/shuffle.webp"],"icons/skull.webp":["icons/skull.webp"],"icons/small_circle.webp":["icons/small_circle.webp"],"icons/sound_off.webp":["icons/sound_off.webp"],"icons/sound_on.webp":["icons/sound_on.webp"],"icons/telegram.webp":["icons/telegram.webp"],"icons/tg.webp":["icons/tg.webp"],"icons/view.webp":["icons/view.webp"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
|
||||
Binary file not shown.
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
@ -11,7 +10,6 @@ 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/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/presentation/theme/app_colors.dart';
|
||||
|
||||
/// Authentication page
|
||||
///
|
||||
|
|
@ -33,8 +31,6 @@ class _AuthPageState extends State<AuthPage> {
|
|||
DateTime? _codeExpiryTime;
|
||||
bool _autoLoginAttempted = false;
|
||||
|
||||
static const String _telegramStartPrefix = 'login_';
|
||||
|
||||
Future<void> _loginWithGoogle(BuildContext context) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
|
|
@ -216,6 +212,8 @@ class _AuthPageState extends State<AuthPage> {
|
|||
|
||||
if (mounted) {
|
||||
_startCodePolling(context, status.code);
|
||||
// Automatically open Telegram bot with the code
|
||||
await _openTelegramBotWithCode(status.code);
|
||||
}
|
||||
} catch (e, s) {
|
||||
log(
|
||||
|
|
@ -368,20 +366,16 @@ class _AuthPageState extends State<AuthPage> {
|
|||
|
||||
final isActive = !status.isExpired && !status.isConsumed;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
|
|
@ -457,32 +451,66 @@ class _AuthPageState extends State<AuthPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Откройте бота и отправьте код, чтобы продолжить.',
|
||||
'Бот должен открыться автоматически. Отправьте код в боте, чтобы продолжить.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openTelegramBot() async {
|
||||
final code = _telegramCodeController.text.trim();
|
||||
|
||||
/// Open Telegram bot with a specific code in the start parameter
|
||||
Future<void> _openTelegramBotWithCode(String code) async {
|
||||
if (mounted && _errorMessage != null) {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
final deepLink = code.isEmpty
|
||||
? Uri.parse(ApiConfigV2.telegramBotDeepLinkBase)
|
||||
: Uri.parse(
|
||||
ApiConfigV2.telegramBotDeepLink('$_telegramStartPrefix$code'),
|
||||
);
|
||||
// Format: https://t.me/bot_username?start=code
|
||||
// Bot extracts 6-digit code from payload using regex (\d{6})$
|
||||
final deepLink = Uri.parse(
|
||||
ApiConfigV2.telegramBotDeepLink(code),
|
||||
);
|
||||
|
||||
try {
|
||||
final launched = await launchUrl(
|
||||
deepLink,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!launched) {
|
||||
throw Exception('Could not launch Telegram');
|
||||
}
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'Error opening Telegram bot',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
name: 'AuthPage',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage =
|
||||
'Не удалось открыть Telegram. Попробуйте вручную: ${deepLink.toString()}';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open Telegram bot without code (base link)
|
||||
Future<void> _openTelegramBot() async {
|
||||
if (mounted && _errorMessage != null) {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
final deepLink = Uri.parse(ApiConfigV2.telegramBotDeepLinkBase);
|
||||
|
||||
try {
|
||||
final launched = await launchUrl(
|
||||
|
|
@ -541,72 +569,79 @@ class _AuthPageState extends State<AuthPage> {
|
|||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
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.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
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.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
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: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: theme.textTheme.bodyMedium?.fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
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.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
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.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
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: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: theme.textTheme.bodyMedium?.fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Login buttons
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
@ -817,7 +852,9 @@ class _AuthPageState extends State<AuthPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,29 +1,14 @@
|
|||
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,
|
||||
),
|
||||
return MaterialApp(
|
||||
theme: AppTheme.light,
|
||||
home: child,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -255,5 +240,71 @@ void main() {
|
|||
final buttonWidget = tester.widget<OutlinedButton>(outlinedButton);
|
||||
expect(buttonWidget.style, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets('main content is wrapped in Card with elevation 2', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||
|
||||
// Find Card widgets
|
||||
final cards = find.byType(Card);
|
||||
expect(cards, findsWidgets);
|
||||
|
||||
// Main content card should have elevation 2
|
||||
final cardWidgets = tester.widgetList<Card>(cards);
|
||||
expect(cardWidgets.isNotEmpty, true);
|
||||
expect(cardWidgets.first.elevation, 2);
|
||||
expect(cardWidgets.first.shape, isA<RoundedRectangleBorder>());
|
||||
});
|
||||
|
||||
testWidgets('main card has rounded corners with 16px radius', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||
|
||||
final cards = find.byType(Card);
|
||||
final cardWidgets = tester.widgetList<Card>(cards);
|
||||
expect(cardWidgets.isNotEmpty, true);
|
||||
final mainCard = cardWidgets.first;
|
||||
final shape = mainCard.shape as RoundedRectangleBorder?;
|
||||
expect(shape, isNotNull);
|
||||
expect(shape?.borderRadius, BorderRadius.circular(16));
|
||||
});
|
||||
|
||||
testWidgets('telegram status card uses Card with elevation 2', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||
|
||||
// Status card should not be visible initially
|
||||
// But we can verify the structure exists
|
||||
final cards = find.byType(Card);
|
||||
expect(cards, findsWidgets);
|
||||
|
||||
// All cards should have elevation 2
|
||||
final cardWidgets = tester.widgetList<Card>(cards);
|
||||
for (final card in cardWidgets) {
|
||||
expect(card.elevation, 2);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('main card has proper padding', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||
|
||||
final cards = find.byType(Card);
|
||||
expect(cards, findsWidgets);
|
||||
|
||||
// Card should have a Padding child
|
||||
final paddingFinder = find.descendant(
|
||||
of: cards.first,
|
||||
matching: find.byType(Padding),
|
||||
);
|
||||
expect(paddingFinder, findsOneWidget);
|
||||
|
||||
final padding = tester.widget<Padding>(paddingFinder);
|
||||
expect(padding.padding, const EdgeInsets.all(24.0));
|
||||
});
|
||||
|
||||
testWidgets('uses withValues instead of deprecated withOpacity', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const AuthPage()));
|
||||
|
||||
// Verify that error container uses withValues (not withOpacity)
|
||||
// This is tested by ensuring the code compiles without deprecation warnings
|
||||
expect(find.byType(Container), findsWidgets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue