Repository: slovnicki/beamer

Issue:
Deep Links are appended instead of replacing the route
> [!NOTE]  
> **Disclaimer**: As my time is currently severely limited, this report might be lacking some nicer or more appropriate phrasings and is likely not as polished as I would like it to be. I am sorry for this. This is also the reason why I am, at least at the moment, not able to provide a fix by myself and open a pull request.

### Describe the bug
In bfe7ce3cc7c9f2c08bd62a4845da883a4213c1d0 all `location` usages were replaced with `uri.toString()` calls.
However, this broke the deep linking feature and possibly even more.
Now when triggering a deep link the link gets appended to the root URI (`/`), which results in weird and incorrect route URLs.

### Example:
* App is in route `/`.
* User triggers Deep Link `example://app/settings`
* _Behaviors before and after the mentioned commit:_
  * _Before_: App routes to `/setttings`.
  * _After_: App routes to `/example://app/settings`.

This happens because the absolute path check is broken in **at least** `Utils#maybeAppend`, however, a quick search through the code found more possible breakages due to the above-mentioned replacement action.

### Beamer version: (e.g. `v0.14.1`, `master`, ...)
v1.6.0

### To Reproduce
Steps to reproduce the behavior:
1. Have a Flutter app that is configured for Deep Links.
2. Trigger a deep link.
3. Watch as the route URI is mangled with the deep link URI.

### Expected behavior
Correctly checking for an absolute URI, so the deep link isn't incorrectly appended.

### Device (please complete the following information):
Not applicable.

### Additional context
The following code has been changed to just check if the **whole** URI starts with a slash instead of, as before, just the location (aka. the `path` part of a URI).
As the old check only considered the `path` part, simply switching to `uri.toString()` broke it for every URI that has a [`scheme` and/or `authority` part](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs) (which is now possible due to the new type):
https://github.com/slovnicki/beamer/blob/bfe7ce3cc7c9f2c08bd62a4845da883a4213c1d0/package/lib/src/utils.dart#L250-L264

Therefore, the new **absolute** URI (`example://app/settings`) is simply appended to the existing URI (`/`)

#### Possible Fix

This seems to be a usable fix for this exact method (there are potentially more needing a similar fix) that allows deep links to work again, at the very least, in my short use case test:
```dart
static RouteInformation maybeAppend(
  RouteInformation current,
  RouteInformation incoming,
) {
  if (!incoming.uri.hasAbsolutePath) {
    return current.copyWith(
      location: current.uri.path.endsWith('/')
          ? '${current.uri}${incoming.uri}'
          : '${current.uri}/${incoming.uri}',
      state: incoming.state,
    );
  }
  return incoming;
}
```
_Note: This would still be mishandling URIs that are not absolute but contain a `scheme` or `authority`, however, I am not sure if such a URI would be even valid!_



Discussion: same issue here

i used this version and it worked for me

beamer: 1.5.6

Fix patch:
diff --git a/package/lib/src/beam_location.dart b/package/lib/src/beam_location.dart
index 1166317..84c8029 100644
--- a/package/lib/src/beam_location.dart
+++ b/package/lib/src/beam_location.dart
@@ -520,14 +520,7 @@ class RoutesBeamLocation extends BeamLocation<BeamState> {
 
     final matched = <Pattern, String>{};
     var overrideNotFound = false;
-    if (routeInformation.uri.toString() != '/' &&
-        (routeInformation.uri.toString().endsWith('/'))) {
-      final location = routeInformation.uri.toString();
-      routeInformation = routeInformation.copyWith(
-        location: location.substring(0, location.length - 1),
-      );
-    }
-    final uri = routeInformation.uri;
+    final uri = Utils.removeTrailingSlash(routeInformation.uri);
 
     for (final route in routes) {
       if (route is String) {
diff --git a/package/lib/src/beam_page.dart b/package/lib/src/beam_page.dart
index 31aa105..d7cf97c 100644
--- a/package/lib/src/beam_page.dart
+++ b/package/lib/src/beam_page.dart
@@ -1,9 +1,8 @@
+import 'package:beamer/beamer.dart';
 import 'package:beamer/src/utils.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 
-import 'package:beamer/beamer.dart';
-
 /// Types for how to route should be built.
 ///
 /// See [BeamPage.type]
@@ -145,7 +144,7 @@ class BeamPage extends Page {
 
     delegate.update(
       configuration: delegate.configuration.copyWith(
-        location: popUri.toString(),
+        uri: popUri,
         state: lastRouteInformation?.state,
       ),
       data: data,
diff --git a/package/lib/src/beam_state.dart b/package/lib/src/beam_state.dart
index 5d79780..997e016 100644
--- a/package/lib/src/beam_state.dart
+++ b/package/lib/src/beam_state.dart
@@ -1,9 +1,8 @@
 import 'dart:convert';
 
-import 'package:flutter/widgets.dart';
-
-import 'package:beamer/src/utils.dart';
 import 'package:beamer/src/beam_location.dart';
+import 'package:beamer/src/utils.dart';
+import 'package:flutter/widgets.dart';
 
 /// A class to mix with when defining a custom state for [BeamLocation].
 ///
@@ -71,10 +70,8 @@ class BeamState with RouteInformationSerializable<BeamState> {
     BeamLocation? beamLocation,
     Object? routeState,
   }) {
-    uriString = Utils.trimmed(uriString);
-    final uri = Uri.parse(uriString);
     return BeamState.fromUri(
-      uri,
+      Utils.removeTrailingSlash(Uri.parse(uriString)),
       beamLocation: beamLocation,
       routeState: routeState,
     );
diff --git a/package/lib/src/beamer_delegate.dart b/package/lib/src/beamer_delegate.dart
index 11d1023..4d521bf 100644
--- a/package/lib/src/beamer_delegate.dart
+++ b/package/lib/src/beamer_delegate.dart
@@ -76,6 +76,7 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
   /// This is not null only if multiple [Beamer]s are used;
   /// `*App.router` and at least one more [Beamer] in the Widget tree.
   BeamerDelegate? get parent => _parent;
+
   set parent(BeamerDelegate? parent) {
     if (parent == null && _parent != null) {
       _parent!.removeListener(_updateFromParent);
@@ -802,8 +803,9 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
       configuration = currentBeamLocation.state.routeInformation;
     } else if (uri.path == '/') {
       configuration = RouteInformation(
-        uri: Uri.parse(
-          initialPath + (uri.query.isNotEmpty ? '?${uri.query}' : ''),
+        uri: uri.replace(
+          path: initialPath,
+          query: uri.hasQuery ? uri.query : null,
         ),
       );
     }
@@ -813,8 +815,8 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
 
   @override
   SynchronousFuture<void> setNewRoutePath(RouteInformation configuration) {
-    if (configuration.uri.toString() == '/' && initialPath != '/') {
-      configuration = configuration.copyWith(location: initialPath);
+    if (configuration.uri.path == '/' && initialPath != '/') {
+      configuration = configuration.copyWith(uri: Uri.parse(initialPath));
     }
     update(configuration: configuration);
     return SynchronousFuture(null);
@@ -907,7 +909,7 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
       );
     }
 
-    if (clearBeamingHistoryOn.contains(configuration.uri.toString())) {
+    if (clearBeamingHistoryOn.contains(configuration.uri.path)) {
       _clearBeamingHistory();
     }
   }
diff --git a/package/lib/src/utils.dart b/package/lib/src/utils.dart
index 846ec02..51dbc01 100644
--- a/package/lib/src/utils.dart
+++ b/package/lib/src/utils.dart
@@ -231,37 +231,42 @@ abstract class Utils {
     }
   }
 
-  /// Removes the trailing / in an URI String and returns the result.
+  /// Removes the trailing / in an URI path and returns the new URI.
   ///
-  /// If there is no trailing /, returns the input.
-  static String trimmed(String? uriString) {
-    if (uriString == null) {
-      return '';
+  /// If there is no trailing /, returns the input URI.
+  static Uri removeTrailingSlash(Uri uri) {
+    String path = uri.path;
+    if (path.length > 1 && path.endsWith('/')) {
+      return uri.replace(path: path.substring(0, path.length - 1));
     }
-    if (uriString.length > 1 && uriString.endsWith('/')) {
-      return uriString.substring(0, uriString.length - 1);
+    return uri;
+  }
+
+  /// If [incoming] is a relative URI append it to [current].
+  /// Else, return incoming URI.
+  static Uri maybeAppend(Uri current, Uri incoming) {
+    if (!incoming.hasAbsolutePath && !incoming.hasEmptyPath) {
+      String currentPath =
+          current.path.endsWith('/') ? current.path : '${current.path}/';
+      return current.replace(
+        path: currentPath + incoming.path,
+        query: incoming.hasQuery ? incoming.query : null,
+        fragment: incoming.hasFragment ? incoming.fragment : null,
+      );
     }
-    return uriString;
+    return incoming;
   }
 
-  /// If incoming RouteInformation doesn't start with slash,
-  /// append it to the current RouteInformation.location.
+  /// Merges the URIs of the RouteInformation's.
   ///
-  /// Else, return incoming RouteInformation
-  static RouteInformation maybeAppend(
+  /// See [maybeAppend] and [removeTrailingSlash].
+  static RouteInformation mergeConfiguration(
     RouteInformation current,
     RouteInformation incoming,
   ) {
-    final incomingLocation = incoming.uri.toString();
-    if (!incomingLocation.startsWith('/')) {
-      return current.copyWith(
-        location: current.uri.toString().endsWith('/')
-            ? '${current.uri}$incomingLocation'
-            : '${current.uri}/$incomingLocation',
-        state: incoming.state,
-      );
-    }
-    return incoming;
+    return incoming.copyWith(
+      uri: maybeAppend(current.uri, removeTrailingSlash(incoming.uri)),
+    );
   }
 
   /// Creates a new configuration for [BeamerDelegate.update].
@@ -269,15 +274,12 @@ abstract class Utils {
   /// Takes into consideration trimming and potentially appending
   /// the incoming to current (used in relative beaming).
   ///
-  /// See [trimmed] and [maybeAppend].
+  /// See [removeTrailingSlash] and [maybeAppend].
   static RouteInformation createNewConfiguration(
     RouteInformation current,
     RouteInformation incoming,
   ) {
-    incoming = incoming.copyWith(
-      location: trimmed(incoming.uri.toString()),
-    );
-    return maybeAppend(current, incoming);
+    return mergeConfiguration(current, incoming);
   }
 }
 
@@ -285,11 +287,11 @@ abstract class Utils {
 extension BeamerRouteInformationExtension on RouteInformation {
   /// Returns a new [RouteInformation] created from this.
   RouteInformation copyWith({
-    String? location,
+    Uri? uri,
     Object? state,
   }) {
     return RouteInformation(
-      uri: Uri.parse(location ?? this.uri.toString()),
+      uri: uri ?? this.uri,
       state: state ?? this.state,
     );
   }


Test patch:
diff --git a/package/test/utils_test.dart b/package/test/utils_test.dart
index dde3748..c4d90d6 100644
--- a/package/test/utils_test.dart
+++ b/package/test/utils_test.dart
@@ -125,48 +125,120 @@ void main() {
   });
 
   group('Creating new configuration for BeamerDelegate', () {
-    test('Trimming', () {
-      expect(Utils.trimmed(null), '');
-      expect(Utils.trimmed('/'), '/');
-      expect(Utils.trimmed('/xxx/'), '/xxx');
+    test('Remove trailing slashes', () {
+      expect(Utils.removeTrailingSlash(Uri.parse('')), Uri.parse(''));
+      expect(Utils.removeTrailingSlash(Uri.parse('/')), Uri.parse('/'));
+      expect(Utils.removeTrailingSlash(Uri.parse('/xxx/')), Uri.parse('/xxx'));
+      expect(
+        Utils.removeTrailingSlash(Uri.parse('https://example.com/')),
+        Uri.parse('https://example.com/'),
+      );
+      expect(
+        Utils.removeTrailingSlash(Uri.parse('https://example.com/test/')),
+        Uri.parse('https://example.com/test'),
+      );
     });
 
-    test('Appending without new routeState', () {
-      final current = RouteInformation(uri: Uri.parse('/current'));
+    test('Appending URIs to relative URI', () {
+      final current = Uri.parse('/current');
       expect(
-        Utils.maybeAppend(current, RouteInformation(uri: Uri.parse('incoming')))
-            .uri
-            .toString(),
+        Utils.maybeAppend(current, Uri.parse('incoming')).toString(),
         '/current/incoming',
       );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('/incoming')).toString(),
+        '/incoming',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('')).toString(),
+        '',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('/')).toString(),
+        '/',
+      );
       expect(
         Utils.maybeAppend(
-                current, RouteInformation(uri: Uri.parse('/incoming')))
-            .uri
-            .toString(),
+          current,
+          Uri.parse('example://app/incoming'),
+        ).toString(),
+        'example://app/incoming',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('//app/incoming')).toString(),
+        '//app/incoming',
+      );
+    });
+
+    test('Appending URIs to absolute URI', () {
+      final current = Uri.parse('example://app/current');
+      expect(
+        Utils.maybeAppend(current, Uri.parse('incoming')).toString(),
+        'example://app/current/incoming',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('/incoming')).toString(),
         '/incoming',
       );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('')).toString(),
+        '',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('/')).toString(),
+        '/',
+      );
+      expect(
+        Utils.maybeAppend(
+          current,
+          Uri.parse('example://app/incoming'),
+        ).toString(),
+        'example://app/incoming',
+      );
+      expect(
+        Utils.maybeAppend(current, Uri.parse('//app/incoming')).toString(),
+        '//app/incoming',
+      );
     });
 
-    test('Appending with new routeState', () {
+    test('Merging with new routeState', () {
       final current = RouteInformation(uri: Uri.parse('/current'));
       expect(
-        Utils.maybeAppend(current, RouteInformation(uri: Uri(), state: 42))
+        Utils.mergeConfiguration(
+                current, RouteInformation(uri: Uri(), state: 42))
             .state,
         42,
       );
       expect(
-        Utils.maybeAppend(current,
+        Utils.mergeConfiguration(current,
                 RouteInformation(uri: Uri.parse('incoming'), state: 42))
             .state,
         42,
       );
       expect(
-        Utils.maybeAppend(current,
+        Utils.mergeConfiguration(current,
                 RouteInformation(uri: Uri.parse('/incoming'), state: 42))
             .state,
         42,
       );
+      expect(
+        Utils.mergeConfiguration(
+            current,
+            RouteInformation(
+              uri: Uri.parse('example://app/incoming'),
+              state: 42,
+            )).state,
+        42,
+      );
+      expect(
+        Utils.mergeConfiguration(
+            current,
+            RouteInformation(
+              uri: Uri.parse('//app/incoming'),
+              state: 42,
+            )).state,
+        42,
+      );
     });
   });