Skip to content

Commit b876d6c

Browse files
authored
Merge pull request #182 from flutter-news-app-full-source-code/fix/auth-bugs-in-dev-and-prod-env
Fix/auth bugs in dev and prod env
2 parents 84ea497 + 0f54362 commit b876d6c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2363
-1668
lines changed

.vscode/launch.json

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
{
2-
// Use IntelliSense to learn about possible attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
52
"version": "0.2.0",
63
"configurations": [
74
{
8-
"name": "flutter-news-app-mobile-client-full-source-code",
5+
"name": "Demo - Flutter News App",
96
"request": "launch",
10-
"type": "dart"
7+
"type": "dart",
8+
"program": "lib/main.dart",
9+
"args": [
10+
"--dart-define=APP_ENVIRONMENT=demo"
11+
]
1112
},
1213
{
13-
"name": "flutter-news-app-mobile-client-full-source-code (profile mode)",
14+
"name": "Development - Flutter News App",
1415
"request": "launch",
1516
"type": "dart",
16-
"flutterMode": "profile"
17+
"program": "lib/main.dart",
18+
"args": [
19+
"--dart-define=APP_ENVIRONMENT=development",
20+
"--dart-define=BASE_URL=http://localhost:8080"
21+
]
1722
},
1823
{
19-
"name": "flutter-news-app-mobile-client-full-source-code (release mode)",
24+
"name": "Production - Flutter News App",
2025
"request": "launch",
2126
"type": "dart",
22-
"flutterMode": "release"
27+
"program": "lib/main.dart",
28+
"args": [
29+
"--dart-define=APP_ENVIRONMENT=production",
30+
"--dart-define=BASE_URL=https://api.your-production-domain.com"
31+
]
2332
}
2433
]
2534
}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Upcoming Release
4+
5+
- **refactor!**: Overhauled the application startup and authentication lifecycle to be robust and free of race conditions. This was a major architectural change that introduced a new `AppInitializationPage` and `AppInitializationBloc` to act as a "gatekeeper," ensuring all critical data is fetched *before* the main UI is built. This fixes a class of bugs related to indefinite loading screens, data migration on account linking, and inconsistent state during startup.
6+
37
## 1.4.0 - 2025-10-17
48

59
- **feat**: overhauled search and account features with a new sliver-based feed UI, integrated search bar, and modal account sheet.

README.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,16 @@ Gain complete command over your application's operational state and user experie
109109
<details>
110110
<summary><strong>🏗️ Architecture & Technical Excellence</strong></summary>
111111

112-
### 📱 Adaptive UI for All Screens
113-
- Built with `flutter_adaptive_scaffold`, the app offers responsive navigation and layouts that look great on both phones and tablets.
114-
> **↔️ Your Advantage:** Deliver a consistent and optimized UX across a wide range of devices effortlessly.
115-
116-
---
117-
118112
### 🏗️ Clean & Modern Architecture
119113
- Developed with best practices for a maintainable and scalable codebase:
120-
- **Flutter & Dart:** Cutting-edge mobile development.
121-
- **BLoC Pattern:** Predictable and robust state management, enhanced with `bloc_concurrency` transformers (droppable, restartable, sequential) for advanced event handling.
122-
- **GoRouter:** Well-structured and powerful navigation.
123-
- **KV Storage Service:** Utilizes `KVStorageService` for secure and efficient local key-value storage.
114+
- **Multi-Layered Architecture:** A clear separation of concerns into a Data Layer (handling raw data retrieval), Repository Layer (abstracting data sources), and Business Logic Layer (managing state with BLoC) ensures the codebase is decoupled, testable, and easy to reason about.
115+
- **Robust Startup & Lifecycle Management:** The app features a rock-solid, "gatekeeper" startup architecture. An `AppInitializationPage` and dedicated `AppInitializationBloc` orchestrate a sequential, race-condition-free startup process using an `AppInitializer` service. This guarantees all critical dependencies (Remote Config, User Settings) are loaded and validated *before* the main application UI is ever built, eliminating an entire class of complex lifecycle bugs.
116+
- **Advanced State Management:** The app leverages the **BLoC pattern** for predictable state management, enhanced with `bloc_concurrency` transformers (droppable, restartable) for sophisticated UI event handling.
117+
- **Dependency Injection:** Dependencies are provided throughout the app using `RepositoryProvider` and `BlocProvider`, making components highly testable and reusable.
118+
- **Type-Safe, Declarative Routing:** Navigation is managed by **GoRouter**, using named routes for a well-structured and maintainable navigation system.
124119
> **📈 Your Advantage:** The app is built on a clean, modern architecture that's easy to understand and maintain. It's solid and built to last.
125120
121+
126122
---
127123

128124
### 🛠️ Flexible Environment Configuration

android/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ GeneratedPluginRegistrant.java
1212
key.properties
1313
**/*.keystore
1414
**/*.jks
15+
16+
# VS Code
17+
.vscode/

lib/account/view/account_page.dart

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import 'package:core/core.dart' hide AppStatus;
22
import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
5-
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/app/models/app_life_cycle_status.dart';
66
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
77
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/user_avatar.dart';
89
import 'package:go_router/go_router.dart';
910
import 'package:ui_kit/ui_kit.dart';
1011

@@ -39,6 +40,21 @@ class AccountPage extends StatelessWidget {
3940
icon: const Icon(Icons.settings_outlined),
4041
onPressed: () => context.pushNamed(Routes.settingsName),
4142
),
43+
// Conditionally add a sign-in or sign-out button to the AppBar.
44+
// This declutters the main content card and follows common UI patterns.
45+
if (isAnonymous)
46+
IconButton(
47+
icon: const Icon(Icons.login),
48+
tooltip: l10n.anonymousLimitButton,
49+
onPressed: () => context.goNamed(Routes.accountLinkingName),
50+
)
51+
else
52+
IconButton(
53+
icon: const Icon(Icons.logout),
54+
tooltip: l10n.accountSignOutTile,
55+
onPressed: () =>
56+
context.read<AppBloc>().add(const AppLogoutRequested()),
57+
),
4258
],
4359
),
4460
body: SingleChildScrollView(
@@ -66,12 +82,10 @@ class AccountPage extends StatelessWidget {
6682

6783
final String statusText;
6884
final String accountTypeText;
69-
final Widget actionButton;
7085

7186
if (isAnonymous) {
72-
statusText = l10n.accountAnonymousUser;
73-
accountTypeText = l10n.accountGuestAccount;
74-
actionButton = _buildSignInButton(context);
87+
statusText = l10n.accountGuestUserHeadline;
88+
accountTypeText = l10n.accountGuestUserSubheadline;
7589
} else {
7690
statusText = user?.email ?? l10n.accountNoNameUser;
7791

@@ -87,7 +101,6 @@ class AccountPage extends StatelessWidget {
87101
roleDisplayName = '';
88102
}
89103
accountTypeText = roleDisplayName;
90-
actionButton = _buildSignOutButton(context);
91104
}
92105

93106
return Card(
@@ -96,18 +109,11 @@ class AccountPage extends StatelessWidget {
96109
child: Row(
97110
crossAxisAlignment: CrossAxisAlignment.center,
98111
children: [
112+
// Use the standard UserAvatar widget, clipped to be square.
113+
// This ensures visual consistency with other avatars in the app.
99114
ClipRRect(
100115
borderRadius: BorderRadius.circular(AppSpacing.sm),
101-
child: Container(
102-
width: AppSpacing.xxl + AppSpacing.sm,
103-
height: AppSpacing.xxl + AppSpacing.sm,
104-
color: colorScheme.primaryContainer,
105-
child: Icon(
106-
Icons.person_outline,
107-
size: AppSpacing.xl,
108-
color: colorScheme.onPrimaryContainer,
109-
),
110-
),
116+
child: UserAvatar(user: user, radius: AppSpacing.lg),
111117
),
112118
const SizedBox(width: AppSpacing.md),
113119
Expanded(
@@ -123,45 +129,37 @@ class AccountPage extends StatelessWidget {
123129
overflow: TextOverflow.ellipsis,
124130
),
125131
const SizedBox(height: AppSpacing.xs),
132+
// Display the user role inside a styled "capsule" for a
133+
// more refined and less intrusive look.
126134
if (accountTypeText.isNotEmpty)
127-
Text(
128-
accountTypeText,
129-
style: theme.textTheme.bodySmall?.copyWith(
130-
color: colorScheme.onSurfaceVariant,
135+
DecoratedBox(
136+
decoration: BoxDecoration(
137+
color: colorScheme.secondaryContainer,
138+
borderRadius: BorderRadius.circular(AppSpacing.sm),
139+
),
140+
child: Padding(
141+
padding: const EdgeInsets.symmetric(
142+
horizontal: AppSpacing.sm,
143+
vertical: 2,
144+
),
145+
child: Text(
146+
accountTypeText,
147+
style: theme.textTheme.bodySmall?.copyWith(
148+
color: colorScheme.onSecondaryContainer,
149+
),
150+
),
131151
),
132-
maxLines: 1,
133-
overflow: TextOverflow.ellipsis,
134152
),
135153
],
136154
),
137155
),
138-
const SizedBox(width: AppSpacing.md),
139-
actionButton,
140156
],
141157
),
142158
),
143159
);
144160
}
145161

146162
/// Builds the sign-in button for anonymous users.
147-
Widget _buildSignInButton(BuildContext context) {
148-
final l10n = AppLocalizationsX(context).l10n;
149-
return ElevatedButton(
150-
onPressed: () => context.goNamed(Routes.accountLinkingName),
151-
child: Text(l10n.accountSignInPromptButton),
152-
);
153-
}
154-
155-
/// Builds the sign-out button for authenticated users.
156-
Widget _buildSignOutButton(BuildContext context) {
157-
final l10n = AppLocalizationsX(context).l10n;
158-
return OutlinedButton(
159-
onPressed: () => context.read<AuthenticationBloc>().add(
160-
const AuthenticationSignOutRequested(),
161-
),
162-
child: Text(l10n.accountSignOutTile),
163-
);
164-
}
165163
166164
/// Builds the list of navigation tiles for accessing different
167165
/// account-related sections.

lib/account/view/followed_contents/countries/followed_countries_list_page.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/app/models/app_life_cycle_status.dart';
67
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
78
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
89
import 'package:go_router/go_router.dart';
@@ -34,7 +35,6 @@ class FollowedCountriesListPage extends StatelessWidget {
3435
),
3536
body: BlocBuilder<AppBloc, AppState>(
3637
builder: (context, appState) {
37-
final user = appState.user;
3838
final userContentPreferences = appState.userContentPreferences;
3939

4040
if (appState.status == AppLifeCycleStatus.loadingUserData ||
@@ -46,11 +46,13 @@ class FollowedCountriesListPage extends StatelessWidget {
4646
);
4747
}
4848

49-
if (appState.initialUserPreferencesError != null) {
49+
if (appState.error != null) {
5050
return FailureStateWidget(
51-
exception: appState.initialUserPreferencesError!,
51+
exception: appState.error!,
5252
onRetry: () {
53-
context.read<AppBloc>().add(AppStarted(initialUser: user));
53+
context.read<AppBloc>().add(
54+
const AppUserContentPreferencesRefreshed(),
55+
);
5456
},
5557
);
5658
}

lib/account/view/followed_contents/sources/followed_sources_list_page.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/app/models/app_life_cycle_status.dart';
67
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
78
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
89
import 'package:go_router/go_router.dart';
@@ -34,7 +35,6 @@ class FollowedSourcesListPage extends StatelessWidget {
3435
),
3536
body: BlocBuilder<AppBloc, AppState>(
3637
builder: (context, appState) {
37-
final user = appState.user;
3838
final userContentPreferences = appState.userContentPreferences;
3939

4040
if (appState.status == AppLifeCycleStatus.loadingUserData ||
@@ -46,11 +46,13 @@ class FollowedSourcesListPage extends StatelessWidget {
4646
);
4747
}
4848

49-
if (appState.initialUserPreferencesError != null) {
49+
if (appState.error != null) {
5050
return FailureStateWidget(
51-
exception: appState.initialUserPreferencesError!,
51+
exception: appState.error!,
5252
onRetry: () {
53-
context.read<AppBloc>().add(AppStarted(initialUser: user));
53+
context.read<AppBloc>().add(
54+
const AppUserContentPreferencesRefreshed(),
55+
);
5456
},
5557
);
5658
}

lib/account/view/followed_contents/topics/followed_topics_list_page.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/app/models/app_life_cycle_status.dart';
67
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
78
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
89
import 'package:go_router/go_router.dart';
@@ -34,7 +35,6 @@ class FollowedTopicsListPage extends StatelessWidget {
3435
),
3536
body: BlocBuilder<AppBloc, AppState>(
3637
builder: (context, appState) {
37-
final user = appState.user;
3838
final userContentPreferences = appState.userContentPreferences;
3939

4040
if (appState.status == AppLifeCycleStatus.loadingUserData ||
@@ -46,11 +46,13 @@ class FollowedTopicsListPage extends StatelessWidget {
4646
);
4747
}
4848

49-
if (appState.initialUserPreferencesError != null) {
49+
if (appState.error != null) {
5050
return FailureStateWidget(
51-
exception: appState.initialUserPreferencesError!,
51+
exception: appState.error!,
5252
onRetry: () {
53-
context.read<AppBloc>().add(AppStarted(initialUser: user));
53+
context.read<AppBloc>().add(
54+
const AppUserContentPreferencesRefreshed(),
55+
);
5456
},
5557
);
5658
}

lib/account/view/saved_filters_page.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/widgets/save_filter_dialog.dart';
66
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
7+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/confirmation_dialog.dart';
78
import 'package:go_router/go_router.dart';
89
import 'package:ui_kit/ui_kit.dart';
910

@@ -66,7 +67,7 @@ class SavedFiltersPage extends StatelessWidget {
6667
),
6768
title: Text(filter.name),
6869
trailing: PopupMenuButton<String>(
69-
onSelected: (value) {
70+
onSelected: (value) async {
7071
switch (value) {
7172
case 'rename':
7273
showDialog<void>(
@@ -84,9 +85,26 @@ class SavedFiltersPage extends StatelessWidget {
8485
),
8586
);
8687
case 'delete':
87-
context.read<AppBloc>().add(
88-
SavedFilterDeleted(filterId: filter.id),
88+
// Awaiting the dialog result ensures the popup menu
89+
// has time to close before the list rebuilds,
90+
// preventing the red screen flicker.
91+
final confirmed = await showDialog<bool>(
92+
context: context,
93+
builder: (context) => ConfirmationDialog(
94+
title: l10n.deleteConfirmationDialogTitle,
95+
content: l10n.deleteConfirmationDialogContent,
96+
confirmButtonText:
97+
l10n.deleteConfirmationDialogConfirmButton,
98+
),
8999
);
100+
101+
// Only proceed with deletion if the user confirmed
102+
// and the widget is still mounted.
103+
if (confirmed == true && context.mounted) {
104+
context.read<AppBloc>().add(
105+
SavedFilterDeleted(filterId: filter.id),
106+
);
107+
}
90108
}
91109
},
92110
itemBuilder: (BuildContext context) =>

0 commit comments

Comments
 (0)