Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/kaisel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
main stack, shell branches, modules, and flows — so screen-view analytics
no longer re-logs a tab you return to
([#66](https://github.com/Mastersam07/kaisel/issues/66)).
- Deep-link decoding may now be asynchronous, following `kaisel_core`
([#64](https://github.com/Mastersam07/kaisel/issues/64)). The route
information parser awaits the codec, so an async `decode` resolves before
the stack is applied.

## 1.0.0+1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class KaiselRouteInformationParser<R extends KaiselRoute>
Future<KaiselConfig<R>> parseRouteInformation(
RouteInformation routeInformation,
) async {
final decoded = _codec.decode(routeInformation.uri);
final decoded = await _codec.decode(routeInformation.uri);
if (decoded == null) return KaiselConfig<R>(mainStack: _fallback);
return decoded;
}
Expand Down
21 changes: 17 additions & 4 deletions packages/kaisel/lib/src/kaisel_router_delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,13 @@ class KaiselRouterDelegate<R extends KaiselRoute>
}
bool roundTrips;
try {
roundTrips = codec.decode(uri) != null;
final decoded = codec.decode(uri);
if (decoded case final Future<KaiselConfig<R>?> pending) {
pending.ignore();
roundTrips = true;
} else {
roundTrips = decoded != null;
}
} catch (_) {
roundTrips = false;
}
Expand All @@ -900,8 +906,13 @@ class KaiselRouterDelegate<R extends KaiselRoute>
final codec = _codec;
if (codec == null) return null;
try {
final config = codec.decode(Uri.parse(url));
if (config == null) return null;
final decoded = codec.decode(Uri.parse(url));
if (decoded case final Future<KaiselConfig<R>?> pending) {
pending.ignore();
return null;
}
if (decoded is! KaiselConfig<R>) return null;
final config = decoded;
final lines = <String>[
'main: ${config.mainStack.map((r) => '$r').join(' → ')}',
];
Expand Down Expand Up @@ -932,7 +943,9 @@ class KaiselRouterDelegate<R extends KaiselRoute>
case 'deepLink':
final codec = _codec;
if (codec == null) return _cmd(false, 'No codec wired.');
final config = codec.decode(Uri.parse('${command['url'] ?? ''}'));
final config = await codec.decode(
Uri.parse('${command['url'] ?? ''}'),
);
if (config == null) return _cmd(false, 'URL did not decode.');
await setNewRoutePath(config);
return _cmd(true, 'Applied ${command['url']}.');
Expand Down
151 changes: 151 additions & 0 deletions packages/kaisel/test/kaisel_async_codec_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kaisel/kaisel.dart';

sealed class _R extends KaiselRoute {
const _R();
}

final class _Home extends _R {
const _Home();
}

final class _Vault extends _R {
const _Vault();
}

final class _Locked extends _R {
const _Locked();
}

class _EntitlementCodec extends KaiselStackCodec<_R> {
const _EntitlementCodec(this.hasEntitlement);

final Future<bool> Function() hasEntitlement;

@override
Uri encode(List<_R> stack) => switch (stack.last) {
_Home() => Uri(path: '/'),
_Vault() => Uri(path: '/vault'),
_Locked() => Uri(path: '/locked'),
};

@override
Future<List<_R>?> decode(Uri uri) async => switch (uri.pathSegments) {
[] || [''] => const [_Home()],
['vault'] =>
await hasEntitlement()
? const [_Home(), _Vault()]
: const [_Home(), _Locked()],
_ => null,
};
}

class _FailingCodec extends KaiselStackCodec<_R> {
const _FailingCodec();

@override
Uri encode(List<_R> stack) => Uri(path: '/');

@override
Future<List<_R>?> decode(Uri uri) async => throw StateError('storage down');
}

class _SyncCodec extends KaiselStackCodec<_R> {
const _SyncCodec();

@override
Uri encode(List<_R> stack) => Uri(path: '/');

@override
List<_R>? decode(Uri uri) =>
uri.path == '/vault' ? const [_Home(), _Vault()] : const [_Home()];
}

RouteInformationParser<KaiselConfig<_R>> _parserOf(
KaiselRouterConfig<_R> config,
) => config.routeInformationParser as RouteInformationParser<KaiselConfig<_R>>;

Widget _appWith(KaiselRouterConfig<_R> config) =>
MaterialApp.router(routerConfig: config);

KaiselRouterConfig<_R> _configWith(KaiselStackCodec<_R> codec) =>
KaiselRouterConfig<_R>(
initial: const _Home(),
codec: StackToConfigCodec(codec),
builder: (context, route) => switch (route) {
_Home() => const Scaffold(body: Text('home')),
_Vault() => const Scaffold(body: Text('vault')),
_Locked() => const Scaffold(body: Text('locked')),
},
);

void main() {
testWidgets('an async codec resolves a deep link once its state is read', (
tester,
) async {
final gate = Completer<bool>();
final config = _configWith(_EntitlementCodec(() => gate.future));
await tester.pumpWidget(_appWith(config));

final pending = _parserOf(
config,
).parseRouteInformation(RouteInformation(uri: Uri.parse('/vault')));
gate.complete(true);

expect((await pending).mainStack, const [_Home(), _Vault()]);
});

testWidgets('the async result decides the destination', (tester) async {
final config = _configWith(_EntitlementCodec(() async => false));
await tester.pumpWidget(_appWith(config));

final decoded = await _parserOf(
config,
).parseRouteInformation(RouteInformation(uri: Uri.parse('/vault')));

expect(decoded.mainStack, const [_Home(), _Locked()]);
});

testWidgets('an unrecognised URL still falls back', (tester) async {
final config = _configWith(_EntitlementCodec(() async => true));
await tester.pumpWidget(_appWith(config));

final decoded = await _parserOf(
config,
).parseRouteInformation(RouteInformation(uri: Uri.parse('/nope')));

expect(decoded.mainStack, const [_Home()]);
});

testWidgets('a rejecting async decode does not leak from DevTools paths', (
tester,
) async {
final router = KaiselRouter<_R>(initial: const _Home());
final delegate = KaiselRouterDelegate<_R>(
router: router,
codec: const StackToConfigCodec(_FailingCodec()),
builder: (context, route) => const Scaffold(body: Text('home')),
);
await tester.pumpWidget(MaterialApp.router(routerDelegate: delegate));

expect(delegate.debugDecode('/anything'), isNull);
delegate.debugSnapshot();
await tester.pump(const Duration(milliseconds: 50));

expect(tester.takeException(), isNull);
});

testWidgets('synchronous codecs are unaffected', (tester) async {
final config = _configWith(const _SyncCodec());
await tester.pumpWidget(_appWith(config));

final decoded = await _parserOf(
config,
).parseRouteInformation(RouteInformation(uri: Uri.parse('/vault')));

expect(decoded.mainStack, const [_Home(), _Vault()]);
});
}
Loading