| // 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 |
| subloc: prevMatchList.uri.path, |
| queryParams: 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(), |
| subloc: match.subloc, |
| name: route.name, |
| path: route.path, |
| fullpath: matchList.fullpath, |
| extra: match.extra, |
| params: matchList.pathParameters, |
| queryParams: 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; |
| }()); |
| } |