diff --git a/flutter_modular/lib/src/infra/services/url_service/url_service.dart b/flutter_modular/lib/src/infra/services/url_service/url_service.dart index 6c97a234..bfbbd0c3 100644 --- a/flutter_modular/lib/src/infra/services/url_service/url_service.dart +++ b/flutter_modular/lib/src/infra/services/url_service/url_service.dart @@ -12,8 +12,15 @@ abstract class UrlService { String resolvePath(String path) { final uri = Uri.parse(path); if (uri.hasFragment) { + final fragmentUri = Uri.parse(uri.fragment); + if (fragmentUri.query.isNotEmpty) { + return '${fragmentUri.path}?${fragmentUri.query}'; + } return uri.fragment; } + if (uri.query.isNotEmpty) { + return '${uri.path}?${uri.query}'; + } return uri.path; } } diff --git a/flutter_modular/lib/src/presenter/navigation/modular_route_information_parser.dart b/flutter_modular/lib/src/presenter/navigation/modular_route_information_parser.dart index 419e57ef..6ecb65c9 100644 --- a/flutter_modular/lib/src/presenter/navigation/modular_route_information_parser.dart +++ b/flutter_modular/lib/src/presenter/navigation/modular_route_information_parser.dart @@ -51,8 +51,9 @@ class ModularRouteInformationParser path = Modular.initialRoutePath; } } else { - // 3.10 wrapper - path = location; + // Preserve query parameters from the original URI + final query = routeInformation.uri.query; + path = query.isNotEmpty ? '$location?$query' : location; } return selectBook( @@ -97,7 +98,18 @@ class ModularRouteInformationParser book.routes.insert(0, child); } - setArguments(modularArgs); + // Preserve query parameters from the resolved route. + // After resolving parent routes, the tracker updates arguments + // with the correct URI (including query params). Only restore + // old args if they already contain query params, otherwise + // keep the resolved args to avoid losing them. + final resolvedArgs = + getArguments().getOrElse((l) => ModularArguments.empty()); + if (modularArgs.uri.hasQuery) { + setArguments(modularArgs); + } else { + setArguments(resolvedArgs); + } for (final booksRoute in book.routes) { reportPush(booksRoute); diff --git a/flutter_modular/test/src/presenter/navigation/modular_route_information_parser_test.dart b/flutter_modular/test/src/presenter/navigation/modular_route_information_parser_test.dart index d2da93d3..60b7f453 100644 --- a/flutter_modular/test/src/presenter/navigation/modular_route_information_parser_test.dart +++ b/flutter_modular/test/src/presenter/navigation/modular_route_information_parser_test.dart @@ -33,6 +33,11 @@ class UrlServiceMock extends Mock implements UrlService {} class ParallelRouteFake extends Fake implements ModularRoute {} +class _ConcreteUrlService extends UrlService { + @override + String? getPath() => null; +} + void main() { late ModularRouteInformationParser parser; late GetRouteMock getRoute; @@ -140,6 +145,49 @@ void main() { expect(book.chapters('/').first.name, '/test'); }); + test('selectBook with parents preserves query params after parent resolution', + () async { + final queryUri = Uri.parse('/auth/login?type=EMPRESA'); + final routeMock = ParallelRouteMock(); + when(() => routeMock.uri).thenReturn(Uri.parse('/auth/login')); + when(() => routeMock.parent).thenReturn('/auth'); + when(() => routeMock.schema).thenReturn('/auth'); + when(() => routeMock.name).thenReturn('/login'); + when(() => routeMock.middlewares).thenReturn([Guard()]); + when(() => routeMock.copyWith(schema: any(named: 'schema'))) + .thenReturn(routeMock); + + final routeParent = ParallelRouteMock(); + when(() => routeParent.uri).thenReturn(Uri.parse('/auth')); + when(() => routeParent.parent).thenReturn(''); + when(() => routeParent.schema).thenReturn(''); + when(() => routeParent.name).thenReturn('/auth'); + when(() => routeParent.middlewares).thenReturn([Guard()]); + when(() => routeParent.copyWith(schema: any(named: 'schema'))) + .thenReturn(routeParent); + + when(() => reportPush(routeMock)).thenReturn(const Success(unit)); + when(() => reportPush(routeParent)).thenReturn(const Success(unit)); + + when(() => getRoute.call(any())).thenAnswer((invocation) async { + final dto = invocation.positionalArguments.first as RouteParmsDTO; + if (dto.url.startsWith('/auth/login')) { + return Success(routeMock); + } + return Success(routeParent); + }); + + final argsWithQuery = ModularArguments(uri: queryUri); + when(() => getArguments.call()).thenReturn(Success(argsWithQuery)); + when(() => setArguments.call(any())).thenReturn(const Success(unit)); + + await parser.selectBook('/auth/login?type=EMPRESA'); + + final captured = verify(() => setArguments.call(captureAny())).captured.last + as ModularArguments; + expect(captured.uri.queryParameters['type'], 'EMPRESA'); + }); + test('selectRoute with RedirectRoute', () async { final redirect = RedirectRoute('/oo', to: '/test'); final modularArgument = ModularArguments.empty(); @@ -181,6 +229,7 @@ void main() { expect(book.chapters().first.name, '/'); expect(book.chapters('/').first.name, '/test'); }); + test('selectRoute with resolver route withless /', () async { final args = ModularArguments.empty(); @@ -335,6 +384,40 @@ void main() { expect(book.uri.toString(), '/parent/test'); expect(book.chapters().first.name, '/parent'); }); + + group('UrlService.resolvePath', () { + test('preserves query parameters', () { + final service = _ConcreteUrlService(); + expect( + service.resolvePath('http://localhost/auth/login?type=EMPRESA'), + '/auth/login?type=EMPRESA', + ); + }); + + test('preserves query parameters in fragment (HashStrategy)', () { + final service = _ConcreteUrlService(); + expect( + service.resolvePath('http://localhost/#/auth/login?type=EMPRESA'), + '/auth/login?type=EMPRESA', + ); + }); + + test('returns path without query when no query exists', () { + final service = _ConcreteUrlService(); + expect( + service.resolvePath('http://localhost/auth/login'), + '/auth/login', + ); + }); + + test('returns fragment without query when fragment has no query', () { + final service = _ConcreteUrlService(); + expect( + service.resolvePath('http://localhost/#/auth/login'), + '/auth/login', + ); + }); + }); } class Guard extends RouteGuard {