Complete Telegram bot login from callback URL
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m18s

This commit is contained in:
Ruslan Bakiev
2026-05-13 19:36:09 +07:00
parent 5b2cd4158c
commit a8b6aa6e02
5 changed files with 23 additions and 82 deletions

View File

@@ -126,14 +126,11 @@ class MapflowApi {
); );
} }
Future<TelegramBotLoginStatus> fetchTelegramBotLoginStatus( Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
String token,
) async {
final data = await _graphql( final data = await _graphql(
''' '''
query TelegramBotLoginStatus(\$token: String!) { mutation CompleteTelegramBotLogin(\$token: String!) {
telegramBotLoginStatus(token: \$token) { completeTelegramBotLogin(token: \$token) {
status
sessionToken sessionToken
user { user {
id id
@@ -149,8 +146,8 @@ class MapflowApi {
''', ''',
variables: {'token': token}, variables: {'token': token},
); );
return TelegramBotLoginStatus.fromJson( return TelegramBotLoginSession.fromJson(
data['telegramBotLoginStatus'] as Map<String, dynamic>, data['completeTelegramBotLogin'] as Map<String, dynamic>,
); );
} }

View File

@@ -4,18 +4,12 @@ String telegramLoginData() => '';
String mapflowSessionToken() => ''; String mapflowSessionToken() => '';
String pendingTelegramLoginToken() => '';
String telegramLoginTokenFromUrl() => ''; String telegramLoginTokenFromUrl() => '';
void saveMapflowSessionToken(String token) {} void saveMapflowSessionToken(String token) {}
void clearMapflowSession() {} void clearMapflowSession() {}
void savePendingTelegramLoginToken(String token) {}
void clearPendingTelegramLoginToken() {}
void openExternalUrl(String url) {} void openExternalUrl(String url) {}
void reloadApp() {} void reloadApp() {}

View File

@@ -4,7 +4,6 @@ import 'package:web/web.dart' as web;
const telegramLoginStorageKey = 'mapflow.telegramLoginData'; const telegramLoginStorageKey = 'mapflow.telegramLoginData';
const mapflowSessionStorageKey = 'mapflow.sessionToken'; const mapflowSessionStorageKey = 'mapflow.sessionToken';
const pendingTelegramLoginStorageKey = 'mapflow.pendingTelegramLoginToken';
@JS('window.Telegram.WebApp.initData') @JS('window.Telegram.WebApp.initData')
external JSString? get _telegramInitData; external JSString? get _telegramInitData;
@@ -20,9 +19,6 @@ String telegramLoginData() =>
String mapflowSessionToken() => String mapflowSessionToken() =>
web.window.localStorage.getItem(mapflowSessionStorageKey) ?? ''; web.window.localStorage.getItem(mapflowSessionStorageKey) ?? '';
String pendingTelegramLoginToken() =>
web.window.localStorage.getItem(pendingTelegramLoginStorageKey) ?? '';
String telegramLoginTokenFromUrl() { String telegramLoginTokenFromUrl() {
final uri = Uri.parse(web.window.location.href); final uri = Uri.parse(web.window.location.href);
return uri.queryParameters['telegram_login'] ?? ''; return uri.queryParameters['telegram_login'] ?? '';
@@ -40,19 +36,10 @@ void saveMapflowSessionToken(String token) {
void clearMapflowSession() { void clearMapflowSession() {
web.window.localStorage.removeItem(mapflowSessionStorageKey); web.window.localStorage.removeItem(mapflowSessionStorageKey);
web.window.localStorage.removeItem(telegramLoginStorageKey); web.window.localStorage.removeItem(telegramLoginStorageKey);
clearPendingTelegramLoginToken();
}
void savePendingTelegramLoginToken(String token) {
web.window.localStorage.setItem(pendingTelegramLoginStorageKey, token);
}
void clearPendingTelegramLoginToken() {
web.window.localStorage.removeItem(pendingTelegramLoginStorageKey);
} }
void openExternalUrl(String url) { void openExternalUrl(String url) {
web.window.open(url, '_blank', 'noopener,noreferrer'); web.window.location.assign(url);
} }
void reloadApp() { void reloadApp() {

View File

@@ -153,26 +153,19 @@ class TelegramBotLogin {
final DateTime expiresAt; final DateTime expiresAt;
} }
class TelegramBotLoginStatus { class TelegramBotLoginSession {
const TelegramBotLoginStatus({ const TelegramBotLoginSession({
required this.status,
required this.sessionToken, required this.sessionToken,
required this.user, required this.user,
}); });
factory TelegramBotLoginStatus.fromJson(Map<String, dynamic> json) { factory TelegramBotLoginSession.fromJson(Map<String, dynamic> json) {
final user = json['user']; return TelegramBotLoginSession(
return TelegramBotLoginStatus( sessionToken: json['sessionToken'] as String,
status: json['status'] as String, user: AppUser.fromJson(json['user'] as Map<String, dynamic>),
sessionToken: json['sessionToken'] as String?,
user: user is Map<String, dynamic> ? AppUser.fromJson(user) : null,
); );
} }
final String status; final String sessionToken;
final String? sessionToken; final AppUser user;
final AppUser? user;
bool get isConfirmed => status == 'CONFIRMED' && sessionToken != null;
bool get isExpired => status == 'EXPIRED';
} }

View File

@@ -297,7 +297,6 @@ class _TelegramLoginScreen extends StatefulWidget {
class _TelegramLoginScreenState extends State<_TelegramLoginScreen> { class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
final _api = MapflowApi(); final _api = MapflowApi();
Timer? _pollTimer;
Timer? _countdownTimer; Timer? _countdownTimer;
DateTime? _loginExpiresAt; DateTime? _loginExpiresAt;
var _loading = false; var _loading = false;
@@ -307,22 +306,13 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
void initState() { void initState() {
super.initState(); super.initState();
final urlToken = telegram_session.telegramLoginTokenFromUrl(); final urlToken = telegram_session.telegramLoginTokenFromUrl();
final pendingToken = urlToken.isNotEmpty if (urlToken.isNotEmpty) {
? urlToken _completeLogin(urlToken);
: telegram_session.pendingTelegramLoginToken();
if (pendingToken.isNotEmpty) {
telegram_session.savePendingTelegramLoginToken(pendingToken);
_pollLogin(pendingToken);
_pollTimer = Timer.periodic(
const Duration(seconds: 2),
(_) => _pollLogin(pendingToken),
);
} }
} }
@override @override
void dispose() { void dispose() {
_pollTimer?.cancel();
_countdownTimer?.cancel(); _countdownTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -333,14 +323,8 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
_message = ''; _message = '';
}); });
final login = await _api.startTelegramBotLogin(); final login = await _api.startTelegramBotLogin();
telegram_session.savePendingTelegramLoginToken(login.token);
telegram_session.openExternalUrl(login.botUrl); telegram_session.openExternalUrl(login.botUrl);
_pollTimer?.cancel();
_countdownTimer?.cancel(); _countdownTimer?.cancel();
_pollTimer = Timer.periodic(
const Duration(seconds: 2),
(_) => _pollLogin(login.token),
);
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) { _countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
@@ -353,28 +337,14 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
}); });
} }
Future<void> _pollLogin(String token) async { Future<void> _completeLogin(String token) async {
final status = await _api.fetchTelegramBotLoginStatus(token); setState(() {
if (status.isExpired) { _loading = true;
_pollTimer?.cancel(); _message = '';
_countdownTimer?.cancel(); });
telegram_session.clearPendingTelegramLoginToken(); final session = await _api.completeTelegramBotLogin(token);
if (!mounted) {
return;
}
_loginExpiresAt = null;
setState(() => _message = 'Ссылка устарела. Запусти вход заново.');
return;
}
if (!status.isConfirmed) {
return;
}
_pollTimer?.cancel();
_countdownTimer?.cancel(); _countdownTimer?.cancel();
telegram_session.saveMapflowSessionToken(status.sessionToken!); telegram_session.saveMapflowSessionToken(session.sessionToken);
telegram_session.clearPendingTelegramLoginToken();
widget.onAuthenticated(); widget.onAuthenticated();
telegram_session.reloadApp(); telegram_session.reloadApp();
} }