mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Add deep link resolver, closes #341
This commit is contained in:
+5
-12
@@ -12,6 +12,7 @@ import 'package:zapstore/screens/search_screen.dart';
|
|||||||
import 'package:zapstore/screens/updates_screen.dart';
|
import 'package:zapstore/screens/updates_screen.dart';
|
||||||
import 'package:zapstore/screens/profile_screen.dart';
|
import 'package:zapstore/screens/profile_screen.dart';
|
||||||
import 'package:zapstore/services/package_manager/package_manager.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)
|
/// Root paths for each navigation branch (used for back navigation handling)
|
||||||
const kBranchRoots = ['/search', '/updates', '/profile'];
|
const kBranchRoots = ['/search', '/updates', '/profile'];
|
||||||
@@ -103,19 +104,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
navigatorKey: rootNavigatorKey,
|
navigatorKey: rootNavigatorKey,
|
||||||
initialLocation: '/search',
|
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: [
|
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
|
// Single stateful shell route that handles everything
|
||||||
StatefulShellRoute.indexedStack(
|
StatefulShellRoute.indexedStack(
|
||||||
builder: (context, state, navigationShell) =>
|
builder: (context, state, navigationShell) =>
|
||||||
|
|||||||
@@ -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/<id>
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:zapstore/router.dart';
|
import 'package:zapstore/router.dart';
|
||||||
|
import 'package:zapstore/services/deep_link_resolver.dart';
|
||||||
|
|
||||||
final deepLinkServiceProvider = Provider<DeepLinkService>((ref) {
|
final deepLinkServiceProvider = Provider<DeepLinkService>((ref) {
|
||||||
return DeepLinkService(ref);
|
return DeepLinkService(ref);
|
||||||
@@ -20,39 +20,24 @@ class DeepLinkService {
|
|||||||
|
|
||||||
GoRouter get _router => _ref.read(routerProvider);
|
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<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
_initialized = true;
|
_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);
|
_subscription = _appLinks.uriLinkStream.listen(_handleUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleUri(Uri? uri) {
|
void _handleUri(Uri? uri) {
|
||||||
if (uri == null) return;
|
if (uri == null) return;
|
||||||
|
|
||||||
// https://zapstore.dev/apps/<id>
|
final path = resolveDeepLinkPath(uri);
|
||||||
if (uri.scheme == 'https' &&
|
if (path != null) {
|
||||||
uri.host == 'zapstore.dev' &&
|
_router.go(path);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,31 +45,3 @@ class DeepLinkService {
|
|||||||
_subscription?.cancel();
|
_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;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user