diff --git a/lib/src/guarded_go_router.dart b/lib/src/guarded_go_router.dart index 27e347f..a6ac5ee 100644 --- a/lib/src/guarded_go_router.dart +++ b/lib/src/guarded_go_router.dart @@ -175,6 +175,17 @@ class GuardedGoRouter { return null; } + final resolvedContinuePath = state.maybeResolveContinuePath(); + if (resolvedContinuePath != null) { + if (goRouter.namedLocation(firstFollowUpRouteName) == resolvedContinuePath) { + return goRouter.namedLocation( + firstFollowUpRouteName, + queryParameters: {...state.uri.queryParametersAll}..remove("continue"), + pathParameters: state.pathParameters, + ); + } + } + return goRouter.namedLocationFrom(state, firstFollowUpRouteName); } @@ -184,28 +195,23 @@ class GuardedGoRouter { if (firstBlockingGuard != null) { final blockingShieldName = _getShieldRouteName(firstBlockingGuard.guard); - final isThisAShieldRoute = guardsShieldingOnThisRoute.isNotEmpty; - final guardSavesLocation = firstBlockingGuard.savesLocation; + final destinationPersistence = firstBlockingGuard.destinationPersistence; final routeIgnoreAsContinue = thisRoute.ignoreAsContinueLocation; - if (isThisAShieldRoute || _isNeglectingContinue || routeIgnoreAsContinue || !guardSavesLocation) { - final destination = goRouter.namedLocation(blockingShieldName, queryParameters: state.uri.queryParameters); - if (firstBlockingGuard.clearsContinue) { - return destination.removeContinuePath(); - } - return destination; + if (_isNeglectingContinue || routeIgnoreAsContinue || destinationPersistence == DestinationPersistence.ignore) { + return goRouter.namedLocation(blockingShieldName, queryParameters: state.uri.queryParameters); } final resolvedContinuePath = state.maybeResolveContinuePath(); if (resolvedContinuePath != null) { final Map queryParameters = {...state.uri.queryParameters}; - if (firstBlockingGuard.clearsContinue) { + if (destinationPersistence == DestinationPersistence.clear) { queryParameters.remove('continue'); } return goRouter.namedLocation(blockingShieldName, queryParameters: queryParameters); } - if (firstBlockingGuard.clearsContinue) { + if (destinationPersistence == DestinationPersistence.clear) { return goRouter.namedLocation(blockingShieldName); } @@ -268,8 +274,7 @@ class GuardedGoRouter { final shell = guardShellRoutes.firstWhere((element) => element.guardType == guard.runtimeType); return _GuardShellContext( guard: guard, - savesLocation: shell.savesLocation, - clearsContinue: shell.clearsContinue, + destinationPersistence: shell.destinationPersistence, ); }, ).toList(); @@ -422,11 +427,9 @@ extension GoGuardX on GoGuard { class _GuardShellContext { _GuardShellContext({ required this.guard, - required this.savesLocation, - required this.clearsContinue, + required this.destinationPersistence, }); final T guard; - final bool savesLocation; - final bool clearsContinue; + final DestinationPersistence destinationPersistence; } diff --git a/lib/src/routes/guard_shell.dart b/lib/src/routes/guard_shell.dart index 4f0f88d..933cbb8 100644 --- a/lib/src/routes/guard_shell.dart +++ b/lib/src/routes/guard_shell.dart @@ -2,17 +2,32 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:guarded_go_router/src/go_guard.dart'; +/// Defines how the destination should be persisted when a guard is activated. +enum DestinationPersistence { + /// Stores the path as the 'continue' query parameter. + store, + + /// Ignores the path, meaning it doesn not store it as 'continue' query parameter. + ignore, + + /// Clears any existing 'continue' query parameter. This is useful when the guard is a "hard" block, + /// meaning that users are not able to resolve the guard's requirements. So it is not needed to store + /// where should the user be continued, as they won't be continued to that path. + /// Eg: A user wanting to access a page to which they are not authorized. + /// + /// This option (more strict than the `ignore` option) is needed when the app is deep linked with a continue query param. + /// Or maybe a previous guard already stored a continue path, although that should resolve itself. + clear, +} + +/// A shell route that applies a guard to its child routes. class GuardShell extends ShellRoute { - /// By default when a destination is protected by a guard, then the router will redirect to - /// the associated shield route of that guard and also append the original destination as `continue` query param. - /// When `savesLocation` is set to false, then the original destination is ignored and the app simply redirects to the shield route - final bool savesLocation; - final bool clearsContinue; + /// Determines how the destination should be handled when this guard blocks and it is the first one doing so. + final DestinationPersistence destinationPersistence; GuardShell( List routes, { - this.savesLocation = true, - this.clearsContinue = false, + this.destinationPersistence = DestinationPersistence.store, super.navigatorKey, }) : super(routes: routes); @@ -23,16 +38,18 @@ class GuardShell extends ShellRoute { Widget child, )? get builder => (context, state, child) => child; + /// The type of guard associated with this shell. Type get guardType => GuardType; + /// Creates a copy of this [GuardShell] with the given fields replaced with new values. GuardShell copyWith({ List? routes, GlobalKey? navigatorKey, + DestinationPersistence? destinationPersistence, }) => GuardShell( routes ?? this.routes, - savesLocation: savesLocation, - clearsContinue: clearsContinue, + destinationPersistence: destinationPersistence ?? this.destinationPersistence, navigatorKey: navigatorKey ?? this.navigatorKey, ); } diff --git a/test/guarded_go_router_test.dart b/test/guarded_go_router_test.dart index cca689a..b361444 100644 --- a/test/guarded_go_router_test.dart +++ b/test/guarded_go_router_test.dart @@ -121,14 +121,12 @@ void main() { GuardShell _guardShell( List routes, { - bool savesLocation = true, - bool clearsContinue = false, + DestinationPersistence destinationPersistence = DestinationPersistence.store, GlobalKey? navigatorKey, }) { return GuardShell( routes, - savesLocation: savesLocation, - clearsContinue: clearsContinue, + destinationPersistence: destinationPersistence, navigatorKey: navigatorKey, ); } @@ -799,48 +797,6 @@ void main() { expect(router.location.sanitized, "/shield2?continue=/route"); }); - testWidgets( - "without appending continue param if destination is protected by a guard which does not save location", - (WidgetTester tester) async { - final router = await pumpRouter( - tester, - guards: [guard1, guard2], - routes: [ - _goRoute("shield1", shieldOf: [Guard1]), - _goRoute("shield2", shieldOf: [Guard2]), - _goRoute("shield3", shieldOf: [Guard3]), - _goRoute( - "root", - routes: [ - _guardShell([ - _goRoute( - "1", - routes: [ - _guardShell(savesLocation: false, [ - _goRoute( - "2", - routes: [ - _guardShell( - [ - _goRoute("3"), - ], - ), - ], - ), - ]), - ], - ), - ]), - ], - ), - ], - ); - - router.goNamed("3", queryParameters: {"continue": "/route"}); - - await tester.pumpAndSettle(); - expect(router.location.sanitized, "/shield2?continue=/route"); - }); testWidgets( "with carrying over existing continue param even if the route redirected from is also a shield of a guard", ( @@ -985,7 +941,7 @@ void main() { expect(router.location.sanitized, "/shield2?continue=/consultation/page"); }); - testWidgets("without appending continue param if the route redirected from a shield of a guard", ( + testWidgets("with appending continue param if the route redirected from a shield of a guard", ( WidgetTester tester, ) async { reset(guard4); @@ -1027,72 +983,310 @@ void main() { router.goNamed("2"); await tester.pumpAndSettle(); - expect(router.location.sanitized, "/shield2"); + expect(router.location.sanitized, "/shield2?continue=/root/1/2"); }); - testWidgets("does not add continue query param if guardShell defines so", ( - WidgetTester tester, - ) async { - final router = await pumpRouter( - tester, - guards: [guard1, guard2, guard3], - routes: [ - _goRoute("shield1", shieldOf: [Guard1]), - _goRoute("shield2", shieldOf: [Guard2]), - _goRoute("shield3", shieldOf: [Guard3]), - _goRoute("anotherPage"), - _guardShell([ - _guardShell( - clearsContinue: true, - [ - _guardShell([ - _goRoute( - "consultation", - ), - ]), - ], - ), - ]), - ], - ); + group('DestinationPersistence', () { + group('store', () { + testWidgets("without overriding continue param if going with an existing continue param", + (WidgetTester tester) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute( + "root", + routes: [ + _guardShell([ + _goRoute( + "1", + routes: [ + // ignore: avoid_redundant_argument_values + _guardShell(destinationPersistence: DestinationPersistence.store, [ + _goRoute( + "2", + routes: [ + _guardShell( + [ + _goRoute("3"), + ], + ), + ], + ), + ]), + ], + ), + ]), + ], + ), + ], + ); - router.goNamed('consultation'); + router.goNamed("3", queryParameters: {"continue": "/route"}); - await tester.pumpAndSettle(); - expect(router.location.sanitized, "/shield2"); - }); + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2?continue=/route"); + }); + testWidgets("with storing current location if there is no continue param", (WidgetTester tester) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute( + "root", + routes: [ + _guardShell([ + _goRoute( + "1", + routes: [ + // ignore: avoid_redundant_argument_values + _guardShell(destinationPersistence: DestinationPersistence.store, [ + _goRoute( + "2", + routes: [ + _guardShell( + [ + _goRoute("3"), + ], + ), + ], + ), + ]), + ], + ), + ]), + ], + ), + ], + ); - testWidgets("removes continue query param if guardShell defines so", ( - WidgetTester tester, - ) async { - final router = await pumpRouter( - tester, - guards: [guard1, guard2, guard3], - routes: [ - _goRoute("shield1", shieldOf: [Guard1]), - _goRoute("shield2", shieldOf: [Guard2]), - _goRoute("shield3", shieldOf: [Guard3]), - _guardShell([ - _guardShell( - clearsContinue: true, - [ - _guardShell([ - _goRoute("consultation"), - _goRoute("anotherInnerPage"), + router.goNamed("3"); + + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2?continue=/root/1/2/3"); + }); + }); + + group('ignore', () { + testWidgets("without overriding continue param if going with an existing continue param", + (WidgetTester tester) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute( + "root", + routes: [ + _guardShell([ + _goRoute( + "1", + routes: [ + _guardShell(destinationPersistence: DestinationPersistence.ignore, [ + _goRoute( + "2", + routes: [ + _guardShell( + [ + _goRoute("3"), + ], + ), + ], + ), + ]), + ], + ), + ]), + ], + ), + ], + ); + + router.goNamed("3", queryParameters: {"continue": "/route"}); + + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2?continue=/route"); + }); + testWidgets("does not add continue query param", ( + WidgetTester tester, + ) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2, guard3], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute("anotherPage"), + _guardShell([ + _guardShell( + destinationPersistence: DestinationPersistence.ignore, + [ + _guardShell([ + _goRoute( + "consultation", + ), + ]), + ], + ), + ]), + ], + ); + + router.goNamed('consultation'); + + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2"); + }); + }); + + group('clear', () { + testWidgets("removes explicit continue query param", ( + WidgetTester tester, + ) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2, guard3], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _guardShell([ + _guardShell( + destinationPersistence: DestinationPersistence.clear, + [ + _guardShell([ + _goRoute("consultation"), + _goRoute("anotherInnerPage"), + ]), + ], + ), + ]), + ], + ); + + router.go('/consultation?continue=/anotherInnerPage'); + + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2"); + }); + testWidgets("removes continue query param added by previous guard", ( + WidgetTester tester, + ) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2, guard3], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _guardShell([ + _guardShell([ + _guardShell( + destinationPersistence: DestinationPersistence.clear, + [ + _goRoute("consultation"), + _goRoute("anotherInnerPage"), + ], + ), ]), - ], - ), - ]), - ], - ); + ]), + ], + ); + + router.go('/consultation?continue=/anotherInnerPage'); + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2?continue=/anotherInnerPage"); + deactivateGuard(guard: guard2); + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield3"); + }); + + testWidgets("with clearing continue param if going with an existing continue param", + (WidgetTester tester) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute( + "root", + routes: [ + _guardShell([ + _goRoute( + "1", + routes: [ + _guardShell(destinationPersistence: DestinationPersistence.clear, [ + _goRoute( + "2", + routes: [ + _guardShell( + [ + _goRoute("3"), + ], + ), + ], + ), + ]), + ], + ), + ]), + ], + ), + ], + ); + + router.goNamed("3", queryParameters: {"continue": "/route"}); + + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2"); + }); + testWidgets("does not add continue query param", ( + WidgetTester tester, + ) async { + final router = await pumpRouter( + tester, + guards: [guard1, guard2, guard3], + routes: [ + _goRoute("shield1", shieldOf: [Guard1]), + _goRoute("shield2", shieldOf: [Guard2]), + _goRoute("shield3", shieldOf: [Guard3]), + _goRoute("anotherPage"), + _guardShell([ + _guardShell( + destinationPersistence: DestinationPersistence.ignore, + [ + _guardShell([ + _goRoute( + "consultation", + ), + ]), + ], + ), + ]), + ], + ); - router.go('/consultation?continue=/anotherInnerPage'); + router.goNamed('consultation'); - await tester.pumpAndSettle(); - expect(router.location.sanitized, "/shield2"); + await tester.pumpAndSettle(); + expect(router.location.sanitized, "/shield2"); + }); + }); }); }); }); + group('every() passes', () { setUp(() { reset(guard1); @@ -2232,7 +2426,7 @@ void main() { router.goNamed("pin"); await tester.pumpAndSettle(); - expect(router.location.sanitized, "/auth/login"); + expect(router.location.sanitized, "/auth/login?continue=/auth/pin"); }); testWidgets("subordinate path should be redirect away from to first passing guard's following path",