Skip to content

Commit 7b62181

Browse files
authored
Merge pull request #142 from flutter-news-app-full-source-code/refactor/auth-flow-and-account-linking
Refactor/auth flow and account linking
2 parents 3497617 + 4fa036f commit 7b62181

File tree

12 files changed

+251
-239
lines changed

12 files changed

+251
-239
lines changed

lib/account/view/account_page.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ class AccountPage extends StatelessWidget {
113113
textStyle: textTheme.labelLarge,
114114
),
115115
onPressed: () {
116-
context.goNamed(
117-
Routes.authenticationName,
118-
queryParameters: {'context': 'linking'},
119-
);
116+
context.pushNamed(Routes.accountLinkingName);
120117
},
121118
),
122119
);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
6+
import 'package:go_router/go_router.dart';
7+
import 'package:ui_kit/ui_kit.dart';
8+
9+
/// {@template account_linking_page}
10+
/// Displays options for an anonymous user to link their account to an email.
11+
///
12+
/// {@endtemplate}
13+
class AccountLinkingPage extends StatelessWidget {
14+
/// {@macro account_linking_page}
15+
const AccountLinkingPage({super.key});
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
final l10n = AppLocalizationsX(context).l10n;
20+
final textTheme = Theme.of(context).textTheme;
21+
final colorScheme = Theme.of(context).colorScheme;
22+
23+
return Scaffold(
24+
appBar: AppBar(title: Text(l10n.accountLinkingHeadline)),
25+
body: BlocConsumer<AuthenticationBloc, AuthenticationState>(
26+
listener: (context, state) {
27+
if (state.status == AuthenticationStatus.failure) {
28+
ScaffoldMessenger.of(context)
29+
..hideCurrentSnackBar()
30+
..showSnackBar(
31+
SnackBar(
32+
content: Text(state.exception!.toFriendlyMessage(context)),
33+
backgroundColor: colorScheme.error,
34+
),
35+
);
36+
}
37+
},
38+
builder: (context, state) {
39+
final isLoading = state.status == AuthenticationStatus.loading;
40+
41+
return Padding(
42+
padding: const EdgeInsets.all(AppSpacing.paddingLarge),
43+
child: Center(
44+
child: SingleChildScrollView(
45+
child: Column(
46+
mainAxisAlignment: MainAxisAlignment.center,
47+
crossAxisAlignment: CrossAxisAlignment.stretch,
48+
children: [
49+
Padding(
50+
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
51+
child: Icon(
52+
Icons.sync,
53+
size: AppSpacing.xxl * 2,
54+
color: colorScheme.primary,
55+
),
56+
),
57+
Text(
58+
l10n.accountLinkingHeadline,
59+
style: textTheme.headlineMedium?.copyWith(
60+
fontWeight: FontWeight.bold,
61+
),
62+
textAlign: TextAlign.center,
63+
),
64+
const SizedBox(height: AppSpacing.md),
65+
Text(
66+
l10n.accountLinkingBody,
67+
style: textTheme.bodyLarge?.copyWith(
68+
color: colorScheme.onSurfaceVariant,
69+
),
70+
textAlign: TextAlign.center,
71+
),
72+
const SizedBox(height: AppSpacing.xxl),
73+
ElevatedButton.icon(
74+
icon: const Icon(Icons.email_outlined),
75+
onPressed: isLoading
76+
? null
77+
: () {
78+
context.goNamed(
79+
Routes.accountLinkingRequestCodeName,
80+
);
81+
},
82+
label: Text(l10n.accountLinkingSendLinkButton),
83+
style: ElevatedButton.styleFrom(
84+
padding: const EdgeInsets.symmetric(
85+
vertical: AppSpacing.md,
86+
),
87+
textStyle: textTheme.labelLarge,
88+
),
89+
),
90+
if (isLoading) ...[
91+
const Padding(
92+
padding: EdgeInsets.only(top: AppSpacing.xl),
93+
child: Center(child: CircularProgressIndicator()),
94+
),
95+
],
96+
],
97+
),
98+
),
99+
),
100+
);
101+
},
102+
),
103+
);
104+
}
105+
}

lib/authentication/view/authentication_page.dart

Lines changed: 22 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,13 @@ import 'package:go_router/go_router.dart';
1010
import 'package:ui_kit/ui_kit.dart';
1111

1212
/// {@template authentication_page}
13-
/// Displays authentication options (Google, Email, Anonymous) based on context.
13+
/// Displays authentication options (Email, Anonymous) for new users.
1414
///
15-
/// This page can be used for both initial sign-in and for connecting an
16-
/// existing anonymous account.
15+
/// This page is exclusively for initial sign-in/sign-up.
1716
/// {@endtemplate}
1817
class AuthenticationPage extends StatelessWidget {
1918
/// {@macro authentication_page}
20-
const AuthenticationPage({
21-
required this.headline,
22-
required this.subHeadline,
23-
required this.showAnonymousButton,
24-
required this.isLinkingContext,
25-
super.key,
26-
});
27-
28-
/// The main title displayed on the page.
29-
final String headline;
30-
31-
/// The descriptive text displayed below the headline.
32-
final String subHeadline;
33-
34-
/// Whether to show the "Continue Anonymously" button.
35-
final bool showAnonymousButton;
36-
37-
/// Whether this page is being shown in the account linking context.
38-
final bool isLinkingContext;
19+
const AuthenticationPage({super.key});
3920

4021
@override
4122
Widget build(BuildContext context) {
@@ -44,21 +25,7 @@ class AuthenticationPage extends StatelessWidget {
4425
final colorScheme = Theme.of(context).colorScheme;
4526

4627
return Scaffold(
47-
appBar: AppBar(
48-
backgroundColor: Colors.transparent,
49-
elevation: 0,
50-
// Conditionally add the leading close button only in linking context
51-
leading: isLinkingContext
52-
? IconButton(
53-
icon: const Icon(Icons.close),
54-
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
55-
onPressed: () {
56-
// Navigate back to the account page when close is pressed
57-
context.goNamed(Routes.accountName);
58-
},
59-
)
60-
: null,
61-
),
28+
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0),
6229
body: SafeArea(
6330
child: BlocConsumer<AuthenticationBloc, AuthenticationState>(
6431
listener: (context, state) {
@@ -88,23 +55,23 @@ class AuthenticationPage extends StatelessWidget {
8855
Padding(
8956
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
9057
child: Icon(
91-
isLinkingContext ? Icons.sync : Icons.newspaper,
58+
Icons.newspaper,
9259
size: AppSpacing.xxl * 2,
9360
color: colorScheme.primary,
9461
),
9562
),
9663
// const SizedBox(height: AppSpacing.lg),
9764
// --- Headline and Subheadline ---
9865
Text(
99-
headline,
66+
l10n.authenticationSignInHeadline,
10067
style: textTheme.headlineMedium?.copyWith(
10168
fontWeight: FontWeight.bold,
10269
),
10370
textAlign: TextAlign.center,
10471
),
10572
const SizedBox(height: AppSpacing.md),
10673
Text(
107-
subHeadline,
74+
l10n.authenticationSignInSubheadline,
10875
style: textTheme.bodyLarge?.copyWith(
10976
color: colorScheme.onSurfaceVariant,
11077
),
@@ -118,11 +85,7 @@ class AuthenticationPage extends StatelessWidget {
11885
onPressed: isLoading
11986
? null
12087
: () {
121-
context.goNamed(
122-
isLinkingContext
123-
? Routes.linkingRequestCodeName
124-
: Routes.requestCodeName,
125-
);
88+
context.goNamed(Routes.requestCodeName);
12689
},
12790
label: Text(l10n.authenticationEmailSignInButton),
12891
style: ElevatedButton.styleFrom(
@@ -134,24 +97,22 @@ class AuthenticationPage extends StatelessWidget {
13497
),
13598
const SizedBox(height: AppSpacing.lg),
13699

137-
// --- Anonymous Sign-In Button (Conditional) ---
138-
if (showAnonymousButton) ...[
139-
OutlinedButton.icon(
140-
icon: const Icon(Icons.person_outline),
141-
onPressed: isLoading
142-
? null
143-
: () => context.read<AuthenticationBloc>().add(
144-
const AuthenticationAnonymousSignInRequested(),
145-
),
146-
label: Text(l10n.authenticationAnonymousSignInButton),
147-
style: OutlinedButton.styleFrom(
148-
padding: const EdgeInsets.symmetric(
149-
vertical: AppSpacing.md,
150-
),
151-
textStyle: textTheme.labelLarge,
100+
// --- Anonymous Sign-In Button ---
101+
OutlinedButton.icon(
102+
icon: const Icon(Icons.person_outline),
103+
onPressed: isLoading
104+
? null
105+
: () => context.read<AuthenticationBloc>().add(
106+
const AuthenticationAnonymousSignInRequested(),
107+
),
108+
label: Text(l10n.authenticationAnonymousSignInButton),
109+
style: OutlinedButton.styleFrom(
110+
padding: const EdgeInsets.symmetric(
111+
vertical: AppSpacing.md,
152112
),
113+
textStyle: textTheme.labelLarge,
153114
),
154-
],
115+
),
155116

156117
// --- Loading Indicator ---
157118
if (isLoading) ...[

lib/authentication/view/request_code_page.dart

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,17 @@ import 'package:ui_kit/ui_kit.dart';
1818
/// {@endtemplate}
1919
class RequestCodePage extends StatelessWidget {
2020
/// {@macro request_code_page}
21-
const RequestCodePage({required this.isLinkingContext, super.key});
22-
23-
/// Whether this page is being shown in the account linking context.
24-
final bool isLinkingContext;
21+
const RequestCodePage({super.key});
2522

2623
@override
2724
Widget build(BuildContext context) {
2825
// AuthenticationBloc is assumed to be provided by a parent route.
29-
// Pass the linking context flag down to the view.
30-
return _RequestCodeView(isLinkingContext: isLinkingContext);
26+
return const _RequestCodeView();
3127
}
3228
}
3329

3430
class _RequestCodeView extends StatelessWidget {
35-
// Accept the flag from the parent page.
36-
const _RequestCodeView({required this.isLinkingContext});
37-
38-
final bool isLinkingContext;
31+
const _RequestCodeView();
3932

4033
@override
4134
Widget build(BuildContext context) {
@@ -51,19 +44,10 @@ class _RequestCodeView extends StatelessWidget {
5144
icon: const Icon(Icons.arrow_back),
5245
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
5346
onPressed: () {
54-
// Navigate back differently based on the context.
55-
if (isLinkingContext) {
56-
// If linking, go back to Auth page preserving the linking query param.
57-
context.goNamed(
58-
Routes.authenticationName,
59-
queryParameters: isLinkingContext
60-
? {'context': 'linking'}
61-
: const {},
62-
);
63-
} else {
64-
// If normal sign-in, just go back to the Auth page.
65-
context.goNamed(Routes.authenticationName);
66-
}
47+
// Navigate back to the previous page in the stack.
48+
// GoRouter will handle the correct navigation based on the current
49+
// route's parent.
50+
context.pop();
6751
},
6852
),
6953
),
@@ -81,13 +65,23 @@ class _RequestCodeView extends StatelessWidget {
8165
);
8266
} else if (state.status ==
8367
AuthenticationStatus.requestCodeSuccess) {
84-
// Navigate to the code verification page on success, passing the email
85-
context.pushNamed(
86-
isLinkingContext
87-
? Routes.linkingVerifyCodeName
88-
: Routes.verifyCodeName,
89-
pathParameters: {'email': state.email!},
90-
);
68+
// Navigate to the code verification page on success, passing the email.
69+
// The current route's parent will determine if this is for linking
70+
// or standard authentication.
71+
final currentRouteName = GoRouter.of(
72+
context,
73+
).routerDelegate.currentConfiguration.last.route.name;
74+
if (currentRouteName == Routes.accountLinkingRequestCodeName) {
75+
context.goNamed(
76+
Routes.accountLinkingVerifyCodeName,
77+
pathParameters: {'email': state.email!},
78+
);
79+
} else {
80+
context.goNamed(
81+
Routes.verifyCodeName,
82+
pathParameters: {'email': state.email!},
83+
);
84+
}
9185
}
9286
},
9387
// BuildWhen prevents unnecessary rebuilds if only listening
@@ -130,33 +124,6 @@ class _RequestCodeView extends StatelessWidget {
130124
),
131125
textAlign: TextAlign.center,
132126
),
133-
134-
// NOT NEEDED; any email is accepted in demo mode
135-
//
136-
//Display demo email suggestion if in demo environment
137-
// BlocSelector<AppBloc, AppState, AppEnvironment?>(
138-
// selector: (state) => state.environment,
139-
// builder: (context, environment) {
140-
// if (environment == AppEnvironment.demo) {
141-
// return Column(
142-
// children: [
143-
// const SizedBox(height: AppSpacing.md),
144-
// Text(
145-
// l10n.demoEmailSuggestionMessage(
146-
// 'admin@mail.com',
147-
// ),
148-
// style: textTheme.bodyMedium?.copyWith(
149-
// color: colorScheme.secondary,
150-
// fontWeight: FontWeight.bold,
151-
// ),
152-
// textAlign: TextAlign.center,
153-
// ),
154-
// ],
155-
// );
156-
// }
157-
// return const SizedBox.shrink();
158-
// },
159-
// ),
160127
const SizedBox(height: AppSpacing.xxl),
161128
_EmailLinkForm(isLoading: isLoading),
162129
],

lib/l10n/app_localizations.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ abstract class AppLocalizations {
119119
/// Headline text on the account linking page
120120
///
121121
/// In en, this message translates to:
122-
/// **'Create or Link Account to Save Progress'**
122+
/// **'Save your progress'**
123123
String get accountLinkingHeadline;
124124

125125
/// Body text explaining the benefits of linking an account
126126
///
127127
/// In en, this message translates to:
128-
/// **'Signing up or linking allows you to access your information across multiple devices and ensures your progress isn\'t lost.'**
128+
/// **'Signing up allows you to access your information across multiple devices and ensures your progress isn\'t lost.'**
129129
String get accountLinkingBody;
130130

131131
/// Text for the Google sign-in button

0 commit comments

Comments
 (0)