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 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.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/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/presentation/theme/app_colors.dart';
|
|
||||||
|
|
||||||
/// Authentication page
|
/// Authentication page
|
||||||
///
|
///
|
||||||
|
|
@ -33,8 +31,6 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
DateTime? _codeExpiryTime;
|
DateTime? _codeExpiryTime;
|
||||||
bool _autoLoginAttempted = false;
|
bool _autoLoginAttempted = false;
|
||||||
|
|
||||||
static const String _telegramStartPrefix = 'login_';
|
|
||||||
|
|
||||||
Future<void> _loginWithGoogle(BuildContext context) async {
|
Future<void> _loginWithGoogle(BuildContext context) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
|
@ -216,6 +212,8 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_startCodePolling(context, status.code);
|
_startCodePolling(context, status.code);
|
||||||
|
// Automatically open Telegram bot with the code
|
||||||
|
await _openTelegramBotWithCode(status.code);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log(
|
log(
|
||||||
|
|
@ -368,17 +366,13 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
|
|
||||||
final isActive = !status.isExpired && !status.isConsumed;
|
final isActive = !status.isExpired && !status.isConsumed;
|
||||||
|
|
||||||
return Container(
|
return Card(
|
||||||
width: double.infinity,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.all(16),
|
shape: RoundedRectangleBorder(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
|
||||||
color: theme.dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -457,7 +451,7 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Откройте бота и отправьте код, чтобы продолжить.',
|
'Бот должен открыться автоматически. Отправьте код в боте, чтобы продолжить.',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -466,22 +460,22 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openTelegramBot() async {
|
/// Open Telegram bot with a specific code in the start parameter
|
||||||
final code = _telegramCodeController.text.trim();
|
Future<void> _openTelegramBotWithCode(String code) async {
|
||||||
|
|
||||||
if (mounted && _errorMessage != null) {
|
if (mounted && _errorMessage != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final deepLink = code.isEmpty
|
// Format: https://t.me/bot_username?start=code
|
||||||
? Uri.parse(ApiConfigV2.telegramBotDeepLinkBase)
|
// Bot extracts 6-digit code from payload using regex (\d{6})$
|
||||||
: Uri.parse(
|
final deepLink = Uri.parse(
|
||||||
ApiConfigV2.telegramBotDeepLink('$_telegramStartPrefix$code'),
|
ApiConfigV2.telegramBotDeepLink(code),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -508,6 +502,40 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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(
|
||||||
|
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()}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_codeStatusTimer?.cancel();
|
_codeStatusTimer?.cancel();
|
||||||
|
|
@ -541,8 +569,15 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
),
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -575,10 +610,10 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
margin: const EdgeInsets.only(bottom: 24),
|
margin: const EdgeInsets.only(bottom: 24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red.withOpacity(0.1),
|
color: Colors.red.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.red.withOpacity(0.3),
|
color: Colors.red.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -822,6 +857,8 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/pages/auth/auth_page.dart';
|
||||||
import 'package:mnemo_cards_web_v2/presentation/theme/app_theme.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() {
|
void main() {
|
||||||
late MockAppScopeContainer mockAppScope;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
mockAppScope = MockAppScopeContainer();
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget createTestWidget(Widget child) {
|
Widget createTestWidget(Widget child) {
|
||||||
return ScopeProvider<AppScopeContainer>(
|
return MaterialApp(
|
||||||
scope: mockAppScope,
|
|
||||||
child: MaterialApp(
|
|
||||||
theme: AppTheme.light,
|
theme: AppTheme.light,
|
||||||
home: child,
|
home: child,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,5 +240,71 @@ void main() {
|
||||||
final buttonWidget = tester.widget<OutlinedButton>(outlinedButton);
|
final buttonWidget = tester.widget<OutlinedButton>(outlinedButton);
|
||||||
expect(buttonWidget.style, isNotNull);
|
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