blob: 3ac4a5b9e6c7b43f5384d16177e41a8dce084dda [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
/// A GoRouter redirector function.
typedef RouteRedirector = FutureOr<RouteMatchList> Function(
BuildContext, FutureOr<RouteMatchList>, RouteConfiguration, RouteMatcher,
{List<RouteMatchList>? redirectHistory, Object? extra});
/// Processes redirects by returning a new [RouteMatchList] representing the new
/// location.
FutureOr<RouteMatchList> redirect(
BuildContext context,
FutureOr<RouteMatchList> prevMatchListFuture,
RouteConfiguration configuration,
RouteMatcher matcher,
{List<RouteMatchList>? redirectHistory,
Object? extra}) {
FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
final String prevLocation = prevMatchList.uri.toString();
FutureOr<RouteMatchList> processTopLevelRedirect(
String? topRedirectLocation) {
if (topRedirectLocation != null && topRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
topRedirectLocation,
prevMatchList.uri,
configuration,
matcher,
redirectHistory!,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
configuration,
matcher,
redirectHistory: redirectHistory,
extra: extra,
);
}
FutureOr<RouteMatchList> processRouteLevelRedirect(
String? routeRedirectLocation) {
if (routeRedirectLocation != null &&
routeRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
routeRedirectLocation,
prevMatchList.uri,
configuration,
matcher,
redirectHistory!,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
configuration,
matcher,
redirectHistory: redirectHistory,
extra: extra,
);
}
return prevMatchList;
}
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, configuration, prevMatchList, 0);
if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect);
}
redirectHistory ??= <RouteMatchList>[prevMatchList];
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = configuration.topRedirect(
context,
GoRouterState(
configuration,
location: prevLocation,
name: null,
// No name available at the top level trim the query params off the
// sub-location to match route.redirect
matchedLocation: prevMatchList.uri.path,
queryParameters: prevMatchList.uri.queryParameters,
queryParametersAll: prevMatchList.uri.queryParametersAll,
extra: extra,
pageKey: const ValueKey<String>('topLevel'),
),
);
if (topRedirectResult is String?) {
return processTopLevelRedirect(topRedirectResult);
}
return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
}
if (prevMatchListFuture is RouteMatchList) {
return processRedirect(prevMatchListFuture);
}
return prevMatchListFuture.then<RouteMatchList>(processRedirect);
}
FutureOr<String?> _getRouteLevelRedirect(
BuildContext context,
RouteConfiguration configuration,
RouteMatchList matchList,
int currentCheckIndex,
) {
if (currentCheckIndex >= matchList.matches.length) {
return null;
}
final RouteMatch match = matchList.matches[currentCheckIndex];
FutureOr<String?> processRouteRedirect(String? newLocation) =>
newLocation ??
_getRouteLevelRedirect(
context, configuration, matchList, currentCheckIndex + 1);
final RouteBase route = match.route;
FutureOr<String?> routeRedirectResult;
if (route is GoRoute && route.redirect != null) {
routeRedirectResult = route.redirect!(
context,
GoRouterState(
configuration,
location: matchList.uri.toString(),
matchedLocation: match.matchedLocation,
name: route.name,
path: route.path,
fullPath: matchList.fullPath,
extra: match.extra,
pathParameters: matchList.pathParameters,
queryParameters: matchList.uri.queryParameters,
queryParametersAll: matchList.uri.queryParametersAll,
pageKey: match.pageKey,
),
);
}
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
}
return routeRedirectResult.then<String?>(processRouteRedirect);
}
RouteMatchList _getNewMatches(
String newLocation,
Uri previousLocation,
RouteConfiguration configuration,
RouteMatcher matcher,
List<RouteMatchList> redirectHistory,
) {
try {
final RouteMatchList newMatch = matcher.findMatch(newLocation);
_addRedirect(redirectHistory, newMatch, previousLocation,
configuration.redirectLimit);
return newMatch;
} on RedirectionError catch (e) {
return _handleRedirectionError(e);
} on MatcherError catch (e) {
return _handleMatcherError(e);
}
}
RouteMatchList _handleMatcherError(MatcherError error) {
// The RouteRedirector uses the matcher to find the match, so a match
// exception can happen during redirection. For example, the redirector
// redirects from `/a` to `/b`, it needs to get the matches for `/b`.
log.info('Match error: ${error.message}');
final Uri uri = Uri.parse(error.location);
return errorScreen(uri, error.message);
}
RouteMatchList _handleRedirectionError(RedirectionError error) {
log.info('Redirection error: ${error.message}');
final Uri uri = error.location;
return errorScreen(uri, error.message);
}
/// A configuration error detected while processing redirects.
class RedirectionError extends Error implements UnsupportedError {
/// RedirectionError constructor.
RedirectionError(this.message, this.matches, this.location);
/// The matches that were found while processing redirects.
final List<RouteMatchList> matches;
@override
final String message;
/// The location that was originally navigated to, before redirection began.
final Uri location;
@override
String toString() => '${super.toString()} ${<String>[
...matches
.map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
].join(' => ')}';
}
/// Adds the redirect to [redirects] if it is valid.
void _addRedirect(List<RouteMatchList> redirects, RouteMatchList newMatch,
Uri prevLocation, int redirectLimit) {
// Verify that the redirect can be parsed and is not already
// in the list of redirects
assert(() {
if (redirects.contains(newMatch)) {
throw RedirectionError('redirect loop detected',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
if (redirects.length > redirectLimit) {
throw RedirectionError('too many redirects',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
return true;
}());
redirects.add(newMatch);
assert(() {
log.info('redirecting to $newMatch');
return true;
}());
}