From 80897692a3f6f833979b6eb5423eea5a88eeb9fc Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Sat, 7 Mar 2026 21:30:51 -0300 Subject: [PATCH] Add deep link resolver, closes #341 --- lib/router.dart | 17 +++----- lib/services/deep_link_resolver.dart | 45 ++++++++++++++++++++ lib/services/deep_link_service.dart | 61 ++++------------------------ 3 files changed, 59 insertions(+), 64 deletions(-) create mode 100644 lib/services/deep_link_resolver.dart diff --git a/lib/router.dart b/lib/router.dart index 324248e..4e575d3 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -12,6 +12,7 @@ import 'package:zapstore/screens/search_screen.dart'; import 'package:zapstore/screens/updates_screen.dart'; import 'package:zapstore/screens/profile_screen.dart'; import 'package:zapstore/services/package_manager/package_manager.dart'; +import 'package:zapstore/services/deep_link_resolver.dart'; /// Root paths for each navigation branch (used for back navigation handling) const kBranchRoots = ['/search', '/updates', '/profile']; @@ -103,19 +104,11 @@ final routerProvider = Provider((ref) { final router = GoRouter( navigatorKey: rootNavigatorKey, initialLocation: '/search', + // Intercept deep link URIs that GoRouter cannot match as paths. + // This fires before any route is rendered, eliminating the GoException + // flash when the app is launched via market:// or https://zapstore.dev. + redirect: (context, state) => resolveDeepLinkPath(state.uri), routes: [ - // Top-level route for market:// intents - // Redirect to search branch to show with nav bar - GoRoute( - path: '/market/:packageId', - redirect: (context, state) { - final packageId = state.pathParameters['packageId']; - if (packageId == null || packageId.isEmpty) { - return '/search'; - } - return '/search/app/$packageId'; - }, - ), // Single stateful shell route that handles everything StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => diff --git a/lib/services/deep_link_resolver.dart b/lib/services/deep_link_resolver.dart new file mode 100644 index 0000000..2fe4a1e --- /dev/null +++ b/lib/services/deep_link_resolver.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +/// Converts a deep link URI into a router path string, or returns null if +/// the URI is not a recognized deep link. +/// +/// Handles: +/// - `https://zapstore.dev/apps/{id}` +/// - `market://details?id=com.example.app` +/// - `market://search?q=search+query` +String? resolveDeepLinkPath(Uri uri) { + // https://zapstore.dev/apps/ + if (uri.scheme == 'https' && + uri.host == 'zapstore.dev' && + uri.pathSegments.length == 2 && + uri.pathSegments[0] == 'apps') { + final id = uri.pathSegments[1]; + if (id.isNotEmpty) { + debugPrint('App link: app ID = $id'); + return '/search/app/$id'; + } + } + + // market://details?id=com.example.app (Google Play-style intents) + if (uri.scheme == 'market') { + if (uri.host == 'details' || uri.path == '/details') { + final id = uri.queryParameters['id']; + if (id != null && id.isNotEmpty) { + debugPrint('Market intent: app ID = $id'); + return '/search/app/$id'; + } + } + + if (uri.host == 'search' || uri.path == '/search') { + final query = uri.queryParameters['q']; + if (query != null && query.isNotEmpty) { + debugPrint('Market intent: search query = $query'); + return '/search/app/$query'; + } + } + + debugPrint('Market intent: unhandled URI = $uri'); + } + + return null; +} diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart index acc5f2f..418bf65 100644 --- a/lib/services/deep_link_service.dart +++ b/lib/services/deep_link_service.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; -import 'package:flutter/foundation.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/router.dart'; +import 'package:zapstore/services/deep_link_resolver.dart'; final deepLinkServiceProvider = Provider((ref) { return DeepLinkService(ref); @@ -20,39 +20,24 @@ class DeepLinkService { GoRouter get _router => _ref.read(routerProvider); - /// Initialize the service - call once after app is ready + /// Initialize the service - call once after app is ready. + /// + /// The initial launch URI is not re-processed here because GoRouter's + /// top-level redirect already maps it to the correct path before any + /// screen renders. Only links received while the app is running are handled. Future initialize() async { if (_initialized) return; _initialized = true; - // Handle initial link (app launched via intent) - final initialUri = await _appLinks.getInitialLink(); - _handleUri(initialUri); - - // Handle links while app is running _subscription = _appLinks.uriLinkStream.listen(_handleUri); } void _handleUri(Uri? uri) { if (uri == null) return; - // https://zapstore.dev/apps/ - if (uri.scheme == 'https' && - uri.host == 'zapstore.dev' && - uri.pathSegments.length == 2 && - uri.pathSegments[0] == 'apps') { - final id = uri.pathSegments[1]; - if (id.isNotEmpty) { - debugPrint('App link: app ID = $id'); - _router.go('/search/app/$id'); - return; - } - } - - // market://details?id=com.example.app (Google Play-style intents) - final packageId = _extractMarketPackageId(uri); - if (packageId != null) { - _router.go('/search/app/$packageId'); + final path = resolveDeepLinkPath(uri); + if (path != null) { + _router.go(path); } } @@ -60,31 +45,3 @@ class DeepLinkService { _subscription?.cancel(); } } - -/// Extract package ID from a market:// URI. -/// -/// Handles: -/// - market://details?id=com.example.app -/// - market://search?q=search+query -String? _extractMarketPackageId(Uri uri) { - if (uri.scheme != 'market') return null; - - if (uri.host == 'details' || uri.path == '/details') { - final id = uri.queryParameters['id']; - if (id != null && id.isNotEmpty) { - debugPrint('Market intent: app ID = $id'); - return id; - } - } - - if (uri.host == 'search' || uri.path == '/search') { - final query = uri.queryParameters['q']; - if (query != null && query.isNotEmpty) { - debugPrint('Market intent: search query = $query'); - return query; - } - } - - debugPrint('Market intent: unhandled URI = $uri'); - return null; -}