feat(flutter): GoRouter shell + version gate + auth-aware redirects
Cold launch flow: no server-url -> /server-url. URL set, no token -> /login. Token present -> /home with shell. VersionGate runs once when the URL changes, hits /healthz, blocks if min_client_version > package version. /home is a placeholder until Task 14. Replaces Navigator.pushReplacementNamed in server_url_screen + login_screen with context.go now that go_router owns the routes.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
|
||||
import '../../api/client.dart';
|
||||
import '../../api/endpoints/health.dart';
|
||||
import '../../auth/auth_provider.dart';
|
||||
|
||||
final _versionCheckProvider = FutureProvider<_VersionResult>((ref) async {
|
||||
final url = await ref.watch(serverUrlProvider.future);
|
||||
if (url == null) return _VersionResult.skipped;
|
||||
final dio = ApiClient.buildDio(baseUrl: url, tokenResolver: () async => null);
|
||||
final body = await HealthApi(dio).check();
|
||||
final min = body['min_client_version'];
|
||||
if (min == null || min.isEmpty) return _VersionResult.skipped;
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final mine = Version.parse(info.version);
|
||||
final required = Version.parse(min);
|
||||
return mine < required ? _VersionResult.tooOld : _VersionResult.ok;
|
||||
});
|
||||
|
||||
enum _VersionResult { ok, tooOld, skipped }
|
||||
|
||||
class VersionGate extends ConsumerWidget {
|
||||
const VersionGate({required this.child, super.key});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(_versionCheckProvider).when(
|
||||
data: (r) => r == _VersionResult.tooOld ? const _TooOldScreen() : child,
|
||||
error: (_, __) => child, // soft-fail if /healthz is unreachable; let the rest of the flow surface a real error
|
||||
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TooOldScreen extends StatelessWidget {
|
||||
const _TooOldScreen();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Update required',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'This client is too old for that Minstrel server. Install the latest build to continue.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user