summaryrefslogtreecommitdiff
path: root/player-android/lib/router.dart
blob: 45775cfe101e3bb18dee1163ab2b5ee6ca56665e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import 'app_routes.dart';
import 'navigation_key.dart';
import 'providers/auth_state_provider.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
import 'screens/media_detail_screen.dart';
import 'screens/share_screen.dart';

// Re-export AppRoutes so existing callers that import router.dart for routes
// do not need to change their import path.
export 'app_routes.dart' show AppRoutes;

// ---------------------------------------------------------------------------
// Router provider
// ---------------------------------------------------------------------------

/// Builds the [GoRouter] instance as a Riverpod [Provider] so that:
///   1. The navigator key is shared with [DioClient] (enabling 401 redirects).
///   2. The [refreshListenable] is driven by [authStateProvider] changes,
///      which triggers redirect re-evaluation on every auth state transition.
///   3. The provider is created lazily and disposed with [ProviderScope].
final routerProvider = Provider<GoRouter>((ref) {
  // Watch auth state so the router is rebuilt when it changes.
  // Using a ChangeNotifier bridge because GoRouter's refreshListenable expects
  // a Listenable, while Riverpod exposes streams/notifiers.
  final notifier = _RouterRefreshNotifier(ref);

  return GoRouter(
    // Share the navigator key with DioClient so imperative 401 redirects
    // work through go_router rather than the raw Navigator.
    navigatorKey: navigatorKey,

    // Trigger redirect re-evaluation whenever auth state changes.
    refreshListenable: notifier,

    // Default entry point before redirect logic resolves.
    initialLocation: AppRoutes.home,

    redirect: (context, state) {
      final authAsync = ref.read(authStateProvider);

      // While the initial token check is in-flight, hold the current path.
      // The router will re-evaluate once refreshListenable fires.
      if (authAsync.isLoading || authAsync.hasError) return null;

      final auth = authAsync.requireValue;
      final isLoginRoute = state.matchedLocation == AppRoutes.login;

      if (auth.isUnauthenticated && !isLoginRoute) {
        // Guard every authenticated route: bounce to login.
        return AppRoutes.login;
      }

      if (auth.isAuthenticated && isLoginRoute) {
        // Prevent the user from seeing the login screen once authenticated.
        return AppRoutes.home;
      }

      // No redirect needed.
      return null;
    },

    routes: [
      GoRoute(
        path: AppRoutes.login,
        builder: (context, state) => const LoginScreen(),
      ),
      GoRoute(
        path: AppRoutes.home,
        builder: (context, state) => const HomeScreen(),
      ),
      GoRoute(
        path: AppRoutes.mediaDetail,
        builder: (context, state) {
          // The ':id' path parameter is guaranteed by the route pattern.
          final id = state.pathParameters['id']!;
          return MediaDetailScreen(mediaId: id);
        },
      ),
      GoRoute(
        path: AppRoutes.share,
        builder: (context, state) => const ShareScreen(),
      ),
    ],
  );
});

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/// Bridges Riverpod's [authStateProvider] to [GoRouter.refreshListenable].
///
/// GoRouter expects a [ChangeNotifier] (or any [Listenable]) for its refresh
/// mechanism.  This notifier listens to the provider and calls [notifyListeners]
/// on every change, causing the router to re-run its redirect callback.
class _RouterRefreshNotifier extends ChangeNotifier {
  _RouterRefreshNotifier(Ref ref) {
    // Keep a reference to the subscription so we can cancel it on dispose.
    _subscription = ref.listen<AsyncValue<AuthState>>(
      authStateProvider,
      (_, __) => notifyListeners(),
    );
  }

  late final ProviderSubscription<AsyncValue<AuthState>> _subscription;

  @override
  void dispose() {
    _subscription.close();
    super.dispose();
  }
}