Skip to content

Commit ddeeec7

Browse files
committed
feat(authentication): refactor account linking and route protection
- Create new top-level route for account linking - Implement modal presentation for account linking flow - Simplify authentication page by removing linking context parameters - Enhance route protection logic for better clarity and security - Update routing structure for improved maintainability
1 parent 3497617 commit ddeeec7

File tree

2 files changed

+82
-130
lines changed

2 files changed

+82
-130
lines changed

lib/router/router.dart

Lines changed: 76 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_th
1818
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
1919
import 'package:flutter_news_app_mobile_client_full_source_code/app/view/app_shell.dart';
2020
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
21+
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/view/account_linking_page.dart';
2122
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/view/authentication_page.dart';
2223
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/view/email_code_verification_page.dart';
2324
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/view/request_code_page.dart';
@@ -86,6 +87,8 @@ GoRouter createRouter({
8687
// Add any other necessary observers here. If none, this can be an empty list.
8788
],
8889
// --- Redirect Logic ---
90+
// This function is the single source of truth for route protection.
91+
// It's driven by the AppBloc's AppLifeCycleStatus.
8992
redirect: (BuildContext context, GoRouterState state) {
9093
final appStatus = context.read<AppBloc>().state.status;
9194
final currentLocation = state.matchedLocation;
@@ -98,141 +101,86 @@ GoRouter createRouter({
98101

99102
const rootPath = '/';
100103
const authenticationPath = Routes.authentication;
104+
const accountLinkingPath = Routes.accountLinking;
101105
const feedPath = Routes.feed;
102106
final isGoingToAuth = currentLocation.startsWith(authenticationPath);
103-
104-
// With the current App startup architecture, the router is only active when
105-
// the app is in a stable, running state. The `redirect` function's
106-
// only responsibility is to handle auth-based route protection.
107-
// States like `configFetching`, `underMaintenance`, etc., are now
108-
// handled by the root App widget *before* this router is ever built.
107+
final isGoingToLinking = currentLocation.startsWith(accountLinkingPath);
109108

110109
// --- Case 1: Unauthenticated User ---
111-
// If the user is unauthenticated, they should be on an auth path.
112-
// If they are trying to access any other part of the app, redirect them.
110+
// If the user is unauthenticated, they must be on an auth path.
111+
// If they try to go anywhere else, they are redirected to the sign-in page.
113112
if (appStatus == AppLifeCycleStatus.unauthenticated) {
114113
print(' Redirect: User is unauthenticated.');
115-
// If they are already on an auth path, allow it. Otherwise, redirect.
116114
return isGoingToAuth ? null : authenticationPath;
117115
}
118116

119-
// --- Case 2: Anonymous or Authenticated User ---
120-
// If a user is anonymous or authenticated, they should not be able to
121-
// access the main authentication flows, with an exception for account
122-
// linking for anonymous users.
123-
if (appStatus == AppLifeCycleStatus.anonymous ||
124-
appStatus == AppLifeCycleStatus.authenticated) {
125-
print(' Redirect: User is $appStatus.');
126-
127-
// If the user is trying to access an authentication path:
117+
// --- Case 2: Anonymous User ---
118+
// An anonymous user is partially authenticated. They can browse the app.
119+
if (appStatus == AppLifeCycleStatus.anonymous) {
120+
print(' Redirect: User is anonymous.');
121+
// Block anonymous users from the main sign-in page.
128122
if (isGoingToAuth) {
129-
// A fully authenticated user should never see auth pages.
130-
if (appStatus == AppLifeCycleStatus.authenticated) {
131-
print(
132-
' Action: Authenticated user on auth path. Redirecting to feed.',
133-
);
134-
return feedPath;
135-
}
136-
137-
// An anonymous user is only allowed on auth paths for account linking.
138-
final isLinking =
139-
state.uri.queryParameters['context'] == 'linking' ||
140-
currentLocation.contains('/linking/');
141-
142-
if (isLinking) {
143-
print(' Action: Anonymous user on linking path. Allowing.');
144-
return null;
145-
} else {
146-
print(
147-
' Action: Anonymous user on non-linking auth path. Redirecting to feed.',
148-
);
149-
return feedPath;
150-
}
123+
print(
124+
' Action: Anonymous user on auth path. Redirecting to feed.',
125+
);
126+
return feedPath;
151127
}
128+
// If at the root, send them to the feed.
129+
if (currentLocation == rootPath) {
130+
print(' Action: User at root. Redirecting to feed.');
131+
return feedPath;
132+
}
133+
// Allow navigation to other pages, including the new linking page.
134+
return null;
135+
}
152136

153-
// If the user is at the root path, they should be sent to the feed.
137+
// --- Case 3: Authenticated User ---
138+
// A fully authenticated user should be blocked from all auth/linking pages.
139+
if (appStatus == AppLifeCycleStatus.authenticated) {
140+
print(' Redirect: User is authenticated.');
141+
if (isGoingToAuth || isGoingToLinking) {
142+
print(
143+
' Action: Authenticated user on auth/linking path. Redirecting to feed.',
144+
);
145+
return feedPath;
146+
}
147+
// If at the root, send them to the feed.
154148
if (currentLocation == rootPath) {
155149
print(' Action: User at root. Redirecting to feed.');
156150
return feedPath;
157151
}
158152
}
159153

160154
// --- Fallback ---
161-
// For any other case, allow navigation.
155+
// For any other case (or if no conditions are met), allow navigation.
162156
print(' Redirect: No condition met. Allowing navigation.');
163157
return null;
164158
},
165159
// --- Authentication Routes ---
166160
routes: [
167161
// A neutral root route that the app starts on. The redirect logic will
168-
// immediately move the user to the correct location. This route's
169-
// builder will never be called in practice.
162+
// immediately move the user to the correct location.
170163
GoRoute(path: '/', builder: (context, state) => const SizedBox.shrink()),
164+
// --- Simplified Authentication Route for New Users ---
171165
GoRoute(
172166
path: Routes.authentication,
173167
name: Routes.authenticationName,
174168
builder: (BuildContext context, GoRouterState state) {
175-
final l10n = context.l10n;
176-
// Determine context from query parameter
177-
final isLinkingContext =
178-
state.uri.queryParameters['context'] == 'linking';
179-
180-
// Define content based on context
181-
final String headline;
182-
final String subHeadline;
183-
final bool showAnonymousButton;
184-
185-
if (isLinkingContext) {
186-
headline = l10n.authenticationLinkingHeadline;
187-
subHeadline = l10n.authenticationLinkingSubheadline;
188-
showAnonymousButton = false;
189-
} else {
190-
headline = l10n.authenticationSignInHeadline;
191-
subHeadline = l10n.authenticationSignInSubheadline;
192-
showAnonymousButton = true;
193-
}
194-
169+
// This page is now self-contained and doesn't need parameters.
170+
// It's only for truly unauthenticated users.
195171
return BlocProvider(
196172
create: (context) => AuthenticationBloc(
197173
authenticationRepository: context.read<AuthRepository>(),
198174
),
199-
child: AuthenticationPage(
200-
headline: headline,
201-
subHeadline: subHeadline,
202-
showAnonymousButton: showAnonymousButton,
203-
isLinkingContext: isLinkingContext,
204-
),
175+
child: const AuthenticationPage(),
205176
);
206177
},
207178
routes: [
208-
// Nested route for account linking flow (defined first for priority)
209-
GoRoute(
210-
path: Routes.accountLinking,
211-
name: Routes.accountLinkingName,
212-
builder: (context, state) => const SizedBox.shrink(),
213-
routes: [
214-
GoRoute(
215-
path: Routes.requestCode,
216-
name: Routes.linkingRequestCodeName,
217-
builder: (context, state) =>
218-
const RequestCodePage(isLinkingContext: true),
219-
),
220-
GoRoute(
221-
path: '${Routes.verifyCode}/:email',
222-
name: Routes.linkingVerifyCodeName,
223-
builder: (context, state) {
224-
final email = state.pathParameters['email']!;
225-
return EmailCodeVerificationPage(email: email);
226-
},
227-
),
228-
],
229-
),
230-
// Non-linking authentication routes (defined after linking routes)
179+
// Sub-routes for the standard sign-in flow.
231180
GoRoute(
232181
path: Routes.requestCode,
233182
name: Routes.requestCodeName,
234-
builder: (context, state) =>
235-
const RequestCodePage(isLinkingContext: false),
183+
builder: (context, state) => const RequestCodePage(),
236184
),
237185
GoRoute(
238186
path: '${Routes.verifyCode}/:email',
@@ -244,9 +192,40 @@ GoRouter createRouter({
244192
),
245193
],
246194
),
195+
// --- New Top-Level Modal Route for Account Linking ---
196+
GoRoute(
197+
path: Routes.accountLinking,
198+
name: Routes.accountLinkingName,
199+
// Use pageBuilder to create a modal (fullscreen dialog) presentation.
200+
pageBuilder: (context, state) {
201+
return MaterialPage(
202+
fullscreenDialog: true,
203+
child: BlocProvider(
204+
create: (context) => AuthenticationBloc(
205+
authenticationRepository: context.read<AuthRepository>(),
206+
),
207+
child: const AccountLinkingPage(),
208+
),
209+
);
210+
},
211+
routes: [
212+
// Nested routes for the account linking email/code flow.
213+
GoRoute(
214+
path: Routes.requestCode,
215+
name: Routes.accountLinkingRequestCodeName,
216+
builder: (context, state) => const RequestCodePage(),
217+
),
218+
GoRoute(
219+
path: '${Routes.verifyCode}/:email',
220+
name: Routes.accountLinkingVerifyCodeName,
221+
builder: (context, state) {
222+
final email = state.pathParameters['email']!;
223+
return EmailCodeVerificationPage(email: email);
224+
},
225+
),
226+
],
227+
),
247228
// --- Entity Details Route (Top Level) ---
248-
// This route handles displaying details for various content entities
249-
// (Topic, Source, Country) based on path parameters.
250229
GoRoute(
251230
path: Routes.entityDetails,
252231
name: Routes.entityDetailsName,
@@ -300,29 +279,6 @@ GoRouter createRouter({
300279
},
301280
),
302281
// --- Global Article Details Route (Top Level) ---
303-
// This GoRoute provides a top-level, globally accessible way to view the
304-
// HeadlineDetailsPage.
305-
//
306-
// Purpose:
307-
// It is specifically designed for navigating to article details from contexts
308-
// that are *outside* the main StatefulShellRoute's branches (e.g., from
309-
// EntityDetailsPage, which is itself a top-level route, or potentially
310-
// from other future top-level pages or deep links).
311-
//
312-
// Why it's necessary:
313-
// Attempting to push a route that is deeply nested within a specific shell
314-
// branch (like '/feed/article/:id') from a BuildContext outside of that
315-
// shell can lead to navigator context issues and assertion failures.
316-
// This global route avoids such problems by providing a clean, direct path
317-
// to the HeadlineDetailsPage.
318-
//
319-
// How it differs:
320-
// This route is distinct from the article detail routes nested within the
321-
// StatefulShellRoute branches (e.g., Routes.articleDetailsName under /feed,
322-
// Routes.searchArticleDetailsName under /search). Those nested routes are
323-
// intended for navigation *within* their respective shell branches,
324-
// preserving the shell's UI (like the bottom navigation bar).
325-
// This global route, being top-level, will typically cover the entire screen.
326282
GoRoute(
327283
path: Routes.globalArticleDetails,
328284
name: Routes.globalArticleDetailsName,

lib/router/routes.dart

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ abstract final class Routes {
5050
static const resetPasswordName = 'resetPassword';
5151
static const confirmEmail = 'confirm-email';
5252
static const confirmEmailName = 'confirmEmail';
53-
static const accountLinking = 'linking';
53+
54+
// Top-level account linking route
55+
static const accountLinking = '/account-linking';
5456
static const accountLinkingName = 'accountLinking';
5557

5658
// routes for email code verification flow
@@ -59,11 +61,9 @@ abstract final class Routes {
5961
static const verifyCode = 'verify-code';
6062
static const verifyCodeName = 'verifyCode';
6163

62-
// Linking-specific authentication routes
63-
static const linkingRequestCode = 'linking/request-code';
64-
static const linkingRequestCodeName = 'linkingRequestCode';
65-
static const linkingVerifyCode = 'linking/verify-code';
66-
static const linkingVerifyCodeName = 'linkingVerifyCode';
64+
// Linking-specific authentication routes (now nested under accountLinking)
65+
static const accountLinkingRequestCodeName = 'accountLinkingRequestCode';
66+
static const accountLinkingVerifyCodeName = 'accountLinkingVerifyCode';
6767

6868
// --- Settings Sub-Routes (relative to /account/settings) ---
6969
static const settingsAppearance = 'appearance';
@@ -86,10 +86,6 @@ abstract final class Routes {
8686
static const settingsLanguage = 'language';
8787
static const settingsLanguageName = 'settingsLanguage';
8888

89-
// Add names for notification sub-selection routes if needed later
90-
// static const settingsNotificationCategories = 'categories';
91-
// static const settingsNotificationCategoriesName = 'settingsNotificationCategories';
92-
9389
// --- Account Sub-Routes (relative to /account) ---
9490
static const manageFollowedItems = 'manage-followed-items';
9591
static const manageFollowedItemsName = 'manageFollowedItems';

0 commit comments

Comments
 (0)