import "dart:async"; import "package:app_links/app_links.dart"; import "package:auto_route/auto_route.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:smol_mail/data/providers/providers.dart"; import "package:smol_mail/presentation/routes/app_router.dart"; import "package:smol_mail/presentation/routes/app_router.gr.dart"; import "package:smol_mail/presentation/theme/theme.dart"; import "package:smol_mail/smol/link.dart"; /// SnackBars need no context through this key, so the client's trust /// warnings can be shown from anywhere in the protocol stack. final rootScaffoldMessengerKey = GlobalKey(); class AppWidget extends ConsumerStatefulWidget { const AppWidget({super.key}); @override ConsumerState createState() => _AppWidgetState(); } class _AppWidgetState extends ConsumerState { late final AppRouter _router = AppRouter(ref); final AppLinks _appLinks = AppLinks(); StreamSubscription? _linkSub; @override void initState() { super.initState(); // §4/§8 trust warnings stay on screen until dismissed; ordinary notices // do not use this channel. ref.read(clientProvider).onWarning = (message) { rootScaffoldMessengerKey.currentState?.showSnackBar( SnackBar( backgroundColor: const Color.fromRGBO(198, 40, 40, 1), content: Text(message, style: const TextStyle(color: Colors.white)), duration: const Duration(days: 1), action: SnackBarAction( label: "Dismiss", textColor: Colors.white, onPressed: () => rootScaffoldMessengerKey.currentState?.hideCurrentSnackBar(), ), ), ); }; _linkSub = _appLinks.uriLinkStream.listen(_handleLink); } // A smol:// link is the self-certifying form: the §8 strong path, // confirmed in the import dialog route before its key is bound. The push // waits a frame — the stream can deliver the cold-start link while the // first frame is still being laid out. void _handleLink(Uri uri) { final link = parseSmolLink(uri.toString()); if (link == null) return; if (ref.read(identityProvider) == null) { rootScaffoldMessengerKey.currentState?.showSnackBar( const SnackBar( content: Text("finish onboarding before using smol:// links")), ); return; } WidgetsBinding.instance.addPostFrameCallback( (_) => _router.push(ImportContactDialogRoute(prefill: link.source)), ); } @override void dispose() { _linkSub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp.router( title: "kirakira", theme: appThemeLight, darkTheme: appThemeDark, themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, scaffoldMessengerKey: rootScaffoldMessengerKey, // smol:// URIs are for app_links, not the route table. The startup link // must still resolve to a real route — auto_route throws "Can not // resolve initial route" on an unmatched link and leaves a blank // screen — so it lands on the normal entry route, whose guard // redirects to home exactly like a launcher start. Every later // platform link is ignored by the router; the stream handler reacts. routerConfig: _router.config( deepLinkBuilder: (link) => link.initial ? DeepLink.single(const OnboardingRoute()) : DeepLink.none, ), ); } }