From fba105999a7bd74b70a127bd08e2028579ee130a Mon Sep 17 00:00:00 2001
From: Noah <78898963+HaonRekcef@users.noreply.github.com>
Date: Tue, 3 Feb 2026 11:05:56 +0100
Subject: [PATCH] Improve Broadcast Deeplinks (#2597)
* add app_links
* Update lib/src/app_links.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Code Review
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
android/app/src/main/AndroidManifest.xml | 1 +
ios/Runner/Info.plist | 2 +
lib/src/app.dart | 34 +++++++---
lib/src/app_links.dart | 69 ++++++++++++--------
linux/flutter/generated_plugin_registrant.cc | 4 ++
linux/flutter/generated_plugins.cmake | 1 +
pubspec.lock | 40 ++++++++++++
pubspec.yaml | 1 +
8 files changed, 115 insertions(+), 37 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fb71c2101..2cb6d3238 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -53,6 +53,7 @@
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index c83c08bf2..c49309061 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -115,6 +115,8 @@
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ FlutterDeepLinkingEnabled
+
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/lib/src/app.dart b/lib/src/app.dart
index caf494921..52d6d55b8 100644
--- a/lib/src/app.dart
+++ b/lib/src/app.dart
@@ -1,3 +1,6 @@
+import 'dart:async';
+
+import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -19,7 +22,6 @@ import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/quick_actions.dart';
import 'package:lichess_mobile/src/tab_scaffold.dart';
import 'package:lichess_mobile/src/theme.dart';
-import 'package:lichess_mobile/src/utils/navigation.dart';
import 'package:lichess_mobile/src/utils/screen.dart';
/// Application initialization and main entry point.
@@ -61,6 +63,9 @@ class Application extends ConsumerStatefulWidget {
class _AppState extends ConsumerState {
/// Whether the app has checked for online status for the first time.
bool _firstTimeOnlineCheck = false;
+ final _appLinks = AppLinks();
+ final _navigatorKey = GlobalKey();
+ StreamSubscription? _linkSubscription;
@override
void initState() {
@@ -103,6 +108,13 @@ class _AppState extends ConsumerState {
});
super.initState();
+ _initAppLinks();
+ }
+
+ @override
+ void dispose() {
+ _linkSubscription?.cancel();
+ super.dispose();
}
@override
@@ -114,6 +126,7 @@ class _AppState extends ConsumerState {
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
return MaterialApp(
+ navigatorKey: _navigatorKey,
localizationsDelegates: const [
...AppLocalizations.localizationsDelegates,
MaterialLocalizationsEo.delegate,
@@ -129,16 +142,17 @@ class _AppState extends ConsumerState {
context,
).copyWith(height: isShortVerticalScreen(context) ? 60 : null),
),
- onGenerateRoute: (settings) =>
- settings.name != null ? resolveAppLinkUri(context, Uri.parse(settings.name!)) : null,
- onGenerateInitialRoutes: (initialRoute) {
- final homeRoute = buildScreenRoute(context, screen: const MainTabScaffold());
- return ?>[
- homeRoute,
- resolveAppLinkUri(context, Uri.parse(initialRoute)),
- ].nonNulls.toList(growable: false);
- },
+ home: const MainTabScaffold(),
navigatorObservers: [rootNavPageRouteObserver],
);
}
+
+ Future _initAppLinks() async {
+ _linkSubscription = _appLinks.uriLinkStream.listen((uri) {
+ final context = _navigatorKey.currentContext;
+ if (context != null && context.mounted) {
+ handleAppLink(context, uri);
+ }
+ });
+ }
}
diff --git a/lib/src/app_links.dart b/lib/src/app_links.dart
index eb95bdd85..780ca7850 100644
--- a/lib/src/app_links.dart
+++ b/lib/src/app_links.dart
@@ -14,56 +14,77 @@ import 'package:lichess_mobile/src/view/study/study_screen.dart';
import 'package:lichess_mobile/src/view/tournament/tournament_screen.dart';
import 'package:lichess_mobile/src/view/user/user_or_profile_screen.dart';
import 'package:linkify/linkify.dart';
+import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher.dart';
-/// Resolves an app link [Uri] to a corresponding [Route].
-Route? resolveAppLinkUri(BuildContext context, Uri appLinkUri) {
- if (appLinkUri.pathSegments.isEmpty) return null;
+final _logger = Logger('AppLinks');
+/// Resolves an app link [Uri] to one or more corresponding [Route]s.
+List>? resolveAppLinkUri(BuildContext context, Uri appLinkUri) {
+ if (appLinkUri.pathSegments.isEmpty) return null;
+ _logger.info('Resolving app link: $appLinkUri');
switch (appLinkUri.pathSegments[0]) {
case 'study':
final id = appLinkUri.pathSegments[1];
- return StudyScreen.buildRoute(context, StudyId(id));
+ return [StudyScreen.buildRoute(context, StudyId(id))];
case 'broadcast':
final roundId = BroadcastRoundId(appLinkUri.pathSegments[3]);
if (appLinkUri.pathSegments.length > 4) {
final gameId = BroadcastGameId(appLinkUri.pathSegments[4]);
- return BroadcastGameScreen.buildRoute(context, roundId: roundId, gameId: gameId);
+ return [
+ BroadcastRoundScreenLoading.buildRoute(
+ context,
+ roundId,
+ initialTab: BroadcastRoundTab.boards,
+ ),
+ BroadcastGameScreen.buildRoute(context, roundId: roundId, gameId: gameId),
+ ];
} else {
final tab = BroadcastRoundTab.tabOrNullFromString(appLinkUri.fragment);
- return BroadcastRoundScreenLoading.buildRoute(context, roundId, initialTab: tab);
+ return [BroadcastRoundScreenLoading.buildRoute(context, roundId, initialTab: tab)];
}
case 'tournament':
final tournamentId = TournamentId(appLinkUri.pathSegments[1]);
- return TournamentScreen.buildRoute(context, tournamentId);
-
+ return [TournamentScreen.buildRoute(context, tournamentId)];
case 'training':
final id = appLinkUri.pathSegments[1];
- return PuzzleScreen.buildRoute(
- context,
- angle: PuzzleAngle.fromKey('mix'),
- puzzleId: PuzzleId(id),
- );
+ return [
+ PuzzleScreen.buildRoute(context, angle: PuzzleAngle.fromKey('mix'), puzzleId: PuzzleId(id)),
+ ];
case _:
final gameId = GameId(appLinkUri.pathSegments[0]);
final orientation = appLinkUri.pathSegments.getOrNull(1);
final int ply = int.tryParse(appLinkUri.fragment) ?? 0;
// The game id can also be a challenge. Challenge by link is not supported yet so let's ignore it.
if (gameId.isValid) {
- return AnalysisScreen.buildRoute(
- context,
- AnalysisOptions.archivedGame(
- orientation: orientation == 'black' ? Side.black : Side.white,
- gameId: gameId,
- initialMoveCursor: ply,
+ return [
+ AnalysisScreen.buildRoute(
+ context,
+ AnalysisOptions.archivedGame(
+ orientation: orientation == 'black' ? Side.black : Side.white,
+ gameId: gameId,
+ initialMoveCursor: ply,
+ ),
),
- );
+ ];
}
}
return null;
}
+/// Handles an app link [Uri] by navigating to the corresponding screen(s).
+void handleAppLink(BuildContext context, Uri uri) {
+ final routes = resolveAppLinkUri(context, uri);
+ if (routes != null) {
+ for (final route in routes) {
+ Navigator.of(context).push(route);
+ }
+ } else {
+ launchUrl(uri);
+ }
+}
+
const kLichessLinkifiers = [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()];
/// Handles link clicks in Linkify widgets throughout the app.
@@ -71,13 +92,7 @@ void onLinkifyOpen(BuildContext context, LinkableElement link) {
if (link is UrlElement && link.url.startsWith(RegExp('https?:\\/\\/$kLichessHost'))) {
// Handle Lichess links specifically
final appLinkUri = Uri.parse(link.url);
- final route = resolveAppLinkUri(context, appLinkUri);
- if (route != null) {
- Navigator.of(context).push(route);
- } else {
- // If the link is not recognized, open it in a browser
- launchUrl(appLinkUri);
- }
+ handleAppLink(context, appLinkUri);
} else if (link.originText.startsWith('@')) {
final username = link.originText.substring(1);
Navigator.of(context).push(
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 80530edab..a7934b6dc 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
+ g_autoptr(FlPluginRegistrar) gtk_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
+ gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 178789e70..bc5ebe28f 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
file_selector_linux
flutter_secure_storage_linux
+ gtk
url_launcher_linux
)
diff --git a/pubspec.lock b/pubspec.lock
index b796877ad..78c19053d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -33,6 +33,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
+ app_links:
+ dependency: "direct main"
+ description:
+ name: app_links
+ sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ app_links_linux:
+ dependency: transitive
+ description:
+ name: app_links_linux
+ sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
+ app_links_platform_interface:
+ dependency: transitive
+ description:
+ name: app_links_platform_interface
+ sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ app_links_web:
+ dependency: transitive
+ description:
+ name: app_links_web
+ sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
app_settings:
dependency: "direct main"
description:
@@ -726,6 +758,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
+ gtk:
+ dependency: transitive
+ description:
+ name: gtk
+ sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
hooks:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 0fc95a7c0..e1d8d1159 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,6 +10,7 @@ environment:
flutter: ^3.38.0
dependencies:
+ app_links: ^7.0.0
app_settings: ^7.0.0
async: ^2.10.0
auto_size_text: ^3.0.0