Skip to content

Commit 5d376d9

Browse files
authored
Merge pull request #145 from flutter-news-app-full-source-code/138-feat-ui-and-mechanism-to-gracefuly-handle-feed-content-limits
138 feat UI and mechanism to gracefuly handle feed content limits
2 parents 84b23c6 + 540d7a3 commit 5d376d9

File tree

11 files changed

+562
-21
lines changed

11 files changed

+562
-21
lines changed

lib/app/view/app.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/services/pac
1515
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
1616
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
1717
import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart';
18+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/content_limitation_service.dart';
1819
import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart';
1920
import 'package:go_router/go_router.dart';
2021
import 'package:kv_storage_service/kv_storage_service.dart';
@@ -158,6 +159,12 @@ class App extends StatelessWidget {
158159
// Ensure it's created immediately
159160
lazy: false,
160161
),
162+
// Provide the ContentLimitationService.
163+
// It depends on AppBloc, so it is created here.
164+
RepositoryProvider(
165+
create: (context) =>
166+
ContentLimitationService(appBloc: context.read<AppBloc>()),
167+
),
161168
],
162169
child: _AppView(
163170
authenticationRepository: _authenticationRepository,

lib/entity_details/view/entity_details_page.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import 'package:flutter_news_app_mobile_client_full_source_code/entity_details/b
1212
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
1313
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
1414
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
15+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/content_limitation_service.dart';
16+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/content_limitation_bottom_sheet.dart';
1517
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_core/feed_core.dart';
1618
import 'package:go_router/go_router.dart';
1719
import 'package:ui_kit/ui_kit.dart';
@@ -165,9 +167,44 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
165167
? l10n.unfollowButtonLabel
166168
: l10n.followButtonLabel,
167169
onPressed: () {
168-
context.read<EntityDetailsBloc>().add(
169-
const EntityDetailsToggleFollowRequested(),
170-
);
170+
// If the user is unfollowing, always allow it.
171+
if (state.isFollowing) {
172+
context.read<EntityDetailsBloc>().add(
173+
const EntityDetailsToggleFollowRequested(),
174+
);
175+
} else {
176+
// If the user is following, check the limit first.
177+
final limitationService = context
178+
.read<ContentLimitationService>();
179+
final contentType = state.contentType;
180+
181+
if (contentType == null) return;
182+
183+
final action = switch (contentType) {
184+
ContentType.topic => ContentAction.followTopic,
185+
ContentType.source => ContentAction.followSource,
186+
ContentType.country => ContentAction.followCountry,
187+
_ => null,
188+
};
189+
190+
if (action == null) {
191+
return;
192+
}
193+
194+
final status = limitationService.checkAction(action);
195+
196+
if (status == LimitationStatus.allowed) {
197+
context.read<EntityDetailsBloc>().add(
198+
const EntityDetailsToggleFollowRequested(),
199+
);
200+
} else {
201+
showModalBottomSheet<void>(
202+
context: context,
203+
builder: (_) =>
204+
ContentLimitationBottomSheet(status: status),
205+
);
206+
}
207+
}
171208
},
172209
);
173210

lib/headline-details/view/headline_details_page.dart

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import 'package:flutter_news_app_mobile_client_full_source_code/headline-details
1313
import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/bloc/similar_headlines_bloc.dart';
1414
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
1515
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
16+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/content_limitation_service.dart';
17+
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/content_limitation_bottom_sheet.dart';
1618
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/feed_core/feed_core.dart';
1719
import 'package:go_router/go_router.dart';
1820
import 'package:intl/intl.dart';
@@ -198,23 +200,42 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
198200
return;
199201
}
200202

201-
final List<Headline> updatedSavedHeadlines;
203+
// If the user is un-saving, always allow it.
202204
if (isSaved) {
203-
updatedSavedHeadlines = currentPreferences.savedHeadlines
205+
final updatedSavedHeadlines = currentPreferences.savedHeadlines
204206
.where((h) => h.id != headline.id)
205207
.toList();
208+
final updatedPreferences = currentPreferences.copyWith(
209+
savedHeadlines: updatedSavedHeadlines,
210+
);
211+
context.read<AppBloc>().add(
212+
AppUserContentPreferencesChanged(preferences: updatedPreferences),
213+
);
206214
} else {
207-
updatedSavedHeadlines = List.from(currentPreferences.savedHeadlines)
208-
..add(headline);
209-
}
210-
211-
final updatedPreferences = currentPreferences.copyWith(
212-
savedHeadlines: updatedSavedHeadlines,
213-
);
215+
// If the user is saving, check the limit first.
216+
final limitationService = context.read<ContentLimitationService>();
217+
final status = limitationService.checkAction(
218+
ContentAction.bookmarkHeadline,
219+
);
214220

215-
context.read<AppBloc>().add(
216-
AppUserContentPreferencesChanged(preferences: updatedPreferences),
217-
);
221+
if (status == LimitationStatus.allowed) {
222+
final updatedSavedHeadlines = List<Headline>.from(
223+
currentPreferences.savedHeadlines,
224+
)..add(headline);
225+
final updatedPreferences = currentPreferences.copyWith(
226+
savedHeadlines: updatedSavedHeadlines,
227+
);
228+
context.read<AppBloc>().add(
229+
AppUserContentPreferencesChanged(preferences: updatedPreferences),
230+
);
231+
} else {
232+
// If the limit is reached, show the bottom sheet.
233+
showModalBottomSheet<void>(
234+
context: context,
235+
builder: (_) => ContentLimitationBottomSheet(status: status),
236+
);
237+
}
238+
}
218239
},
219240
);
220241

lib/l10n/app_localizations.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,60 @@ abstract class AppLocalizations {
17231723
/// In en, this message translates to:
17241724
/// **'Required version: {version}'**
17251725
String latestRequiredVersionLabel(String version);
1726+
1727+
/// Title for the bottom sheet when an anonymous user hits a content limit.
1728+
///
1729+
/// In en, this message translates to:
1730+
/// **'Sign in to Do More'**
1731+
String get anonymousLimitTitle;
1732+
1733+
/// Body text for the bottom sheet when an anonymous user hits a content limit.
1734+
///
1735+
/// In en, this message translates to:
1736+
/// **'Create a free account to bookmark more and follow more.'**
1737+
String get anonymousLimitBody;
1738+
1739+
/// Button text for the bottom sheet when an anonymous user hits a content limit.
1740+
///
1741+
/// In en, this message translates to:
1742+
/// **'Sign In'**
1743+
String get anonymousLimitButton;
1744+
1745+
/// Title for the bottom sheet when a standard user hits a content limit.
1746+
///
1747+
/// In en, this message translates to:
1748+
/// **'Unlock More Access'**
1749+
String get standardLimitTitle;
1750+
1751+
/// Body text for the bottom sheet when a standard user hits a content limit.
1752+
///
1753+
/// In en, this message translates to:
1754+
/// **'You\'ve reached your limit for the free plan. Upgrade to save and follow more.'**
1755+
String get standardLimitBody;
1756+
1757+
/// Button text for the bottom sheet when a standard user hits a content limit.
1758+
///
1759+
/// In en, this message translates to:
1760+
/// **'Upgrade to Premium'**
1761+
String get standardLimitButton;
1762+
1763+
/// Title for the bottom sheet when a premium user hits a content limit.
1764+
///
1765+
/// In en, this message translates to:
1766+
/// **'You\'ve Reached the Limit'**
1767+
String get premiumLimitTitle;
1768+
1769+
/// Body text for the bottom sheet when a premium user hits a content limit.
1770+
///
1771+
/// In en, this message translates to:
1772+
/// **'To add new items, please review and manage your existing saved and followed content.'**
1773+
String get premiumLimitBody;
1774+
1775+
/// Button text for the bottom sheet when a premium user hits a content limit.
1776+
///
1777+
/// In en, this message translates to:
1778+
/// **'Manage My Content'**
1779+
String get premiumLimitButton;
17261780
}
17271781

17281782
class _AppLocalizationsDelegate

lib/l10n/app_localizations_ar.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,4 +904,34 @@ class AppLocalizationsAr extends AppLocalizations {
904904
String latestRequiredVersionLabel(String version) {
905905
return 'الإصدار المطلوب: $version';
906906
}
907+
908+
@override
909+
String get anonymousLimitTitle => 'تسجيل الدخول للقيام بالمزيد';
910+
911+
@override
912+
String get anonymousLimitBody =>
913+
'قم بإنشاء حساب مجاني لإضافة المزيد ومتابعة المزيد.';
914+
915+
@override
916+
String get anonymousLimitButton => 'تسجيل الدخول';
917+
918+
@override
919+
String get standardLimitTitle => 'افتح الوصول غير المحدود';
920+
921+
@override
922+
String get standardLimitBody =>
923+
'لقد وصلت إلى الحد الأقصى للباقة المجانية. قم بالترقية لحفظ ومتابعة المزيد.';
924+
925+
@override
926+
String get standardLimitButton => 'الترقية إلى بريميوم';
927+
928+
@override
929+
String get premiumLimitTitle => 'لقد وصلت إلى الحد الأقصى';
930+
931+
@override
932+
String get premiumLimitBody =>
933+
'لإضافة عناصر جديدة، يرجى مراجعة وإدارة المحتوى المحفوظ والمتابع الحالي.';
934+
935+
@override
936+
String get premiumLimitButton => 'إدارة المحتوى الخاص بي';
907937
}

lib/l10n/app_localizations_en.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,4 +905,34 @@ class AppLocalizationsEn extends AppLocalizations {
905905
String latestRequiredVersionLabel(String version) {
906906
return 'Required version: $version';
907907
}
908+
909+
@override
910+
String get anonymousLimitTitle => 'Sign in to Do More';
911+
912+
@override
913+
String get anonymousLimitBody =>
914+
'Create a free account to bookmark more and follow more.';
915+
916+
@override
917+
String get anonymousLimitButton => 'Sign In';
918+
919+
@override
920+
String get standardLimitTitle => 'Unlock More Access';
921+
922+
@override
923+
String get standardLimitBody =>
924+
'You\'ve reached your limit for the free plan. Upgrade to save and follow more.';
925+
926+
@override
927+
String get standardLimitButton => 'Upgrade to Premium';
928+
929+
@override
930+
String get premiumLimitTitle => 'You\'ve Reached the Limit';
931+
932+
@override
933+
String get premiumLimitBody =>
934+
'To add new items, please review and manage your existing saved and followed content.';
935+
936+
@override
937+
String get premiumLimitButton => 'Manage My Content';
908938
}

lib/l10n/arb/app_ar.arb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,5 +1189,41 @@
11891189
"example": "1.1.0"
11901190
}
11911191
}
1192+
},
1193+
"anonymousLimitTitle": "تسجيل الدخول للقيام بالمزيد",
1194+
"@anonymousLimitTitle": {
1195+
"description": "Title for the bottom sheet when an anonymous user hits a content limit."
1196+
},
1197+
"anonymousLimitBody": "قم بإنشاء حساب مجاني لإضافة المزيد ومتابعة المزيد.",
1198+
"@anonymousLimitBody": {
1199+
"description": "Body text for the bottom sheet when an anonymous user hits a content limit."
1200+
},
1201+
"anonymousLimitButton": "تسجيل الدخول",
1202+
"@anonymousLimitButton": {
1203+
"description": "Button text for the bottom sheet when an anonymous user hits a content limit."
1204+
},
1205+
"standardLimitTitle": "افتح الوصول غير المحدود",
1206+
"@standardLimitTitle": {
1207+
"description": "Title for the bottom sheet when a standard user hits a content limit."
1208+
},
1209+
"standardLimitBody": "لقد وصلت إلى الحد الأقصى للباقة المجانية. قم بالترقية لحفظ ومتابعة المزيد.",
1210+
"@standardLimitBody": {
1211+
"description": "Body text for the bottom sheet when a standard user hits a content limit."
1212+
},
1213+
"standardLimitButton": "الترقية إلى بريميوم",
1214+
"@standardLimitButton": {
1215+
"description": "Button text for the bottom sheet when a standard user hits a content limit."
1216+
},
1217+
"premiumLimitTitle": "لقد وصلت إلى الحد الأقصى",
1218+
"@premiumLimitTitle": {
1219+
"description": "Title for the bottom sheet when a premium user hits a content limit."
1220+
},
1221+
"premiumLimitBody": "لإضافة عناصر جديدة، يرجى مراجعة وإدارة المحتوى المحفوظ والمتابع الحالي.",
1222+
"@premiumLimitBody": {
1223+
"description": "Body text for the bottom sheet when a premium user hits a content limit."
1224+
},
1225+
"premiumLimitButton": "إدارة المحتوى الخاص بي",
1226+
"@premiumLimitButton": {
1227+
"description": "Button text for the bottom sheet when a premium user hits a content limit."
11921228
}
11931229
}

lib/l10n/arb/app_en.arb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,5 +1189,41 @@
11891189
"example": "1.1.0"
11901190
}
11911191
}
1192+
},
1193+
"anonymousLimitTitle": "Sign in to Do More",
1194+
"@anonymousLimitTitle": {
1195+
"description": "Title for the bottom sheet when an anonymous user hits a content limit."
1196+
},
1197+
"anonymousLimitBody": "Create a free account to bookmark more and follow more.",
1198+
"@anonymousLimitBody": {
1199+
"description": "Body text for the bottom sheet when an anonymous user hits a content limit."
1200+
},
1201+
"anonymousLimitButton": "Sign In",
1202+
"@anonymousLimitButton": {
1203+
"description": "Button text for the bottom sheet when an anonymous user hits a content limit."
1204+
},
1205+
"standardLimitTitle": "Unlock More Access",
1206+
"@standardLimitTitle": {
1207+
"description": "Title for the bottom sheet when a standard user hits a content limit."
1208+
},
1209+
"standardLimitBody": "You've reached your limit for the free plan. Upgrade to save and follow more.",
1210+
"@standardLimitBody": {
1211+
"description": "Body text for the bottom sheet when a standard user hits a content limit."
1212+
},
1213+
"standardLimitButton": "Upgrade to Premium",
1214+
"@standardLimitButton": {
1215+
"description": "Button text for the bottom sheet when a standard user hits a content limit."
1216+
},
1217+
"premiumLimitTitle": "You've Reached the Limit",
1218+
"@premiumLimitTitle": {
1219+
"description": "Title for the bottom sheet when a premium user hits a content limit."
1220+
},
1221+
"premiumLimitBody": "To add new items, please review and manage your existing saved and followed content.",
1222+
"@premiumLimitBody": {
1223+
"description": "Body text for the bottom sheet when a premium user hits a content limit."
1224+
},
1225+
"premiumLimitButton": "Manage My Content",
1226+
"@premiumLimitButton": {
1227+
"description": "Button text for the bottom sheet when a premium user hits a content limit."
11921228
}
11931229
}

lib/router/routes.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,6 @@ abstract final class Routes {
4444
// --- Authentication Routes ---
4545
static const authentication = '/authentication';
4646
static const authenticationName = 'authentication';
47-
static const forgotPassword = 'forgot-password';
48-
static const forgotPasswordName = 'forgotPassword';
49-
static const resetPassword = 'reset-password';
50-
static const resetPasswordName = 'resetPassword';
51-
static const confirmEmail = 'confirm-email';
52-
static const confirmEmailName = 'confirmEmail';
5347

5448
// Top-level account linking route
5549
static const accountLinking = '/account-linking';

0 commit comments

Comments
 (0)