From 6e89b70c33950f61de68431f090b060b4bd82070 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:16:14 +0100 Subject: [PATCH 1/6] feat(AppConfigIntField): add enabled parameter to TextFormField - Add 'enabled' parameter to AppConfigIntField widget - Allow control over the input field's editability - Update TextFormField to use the new 'enabled' parameter --- lib/app_configuration/widgets/app_config_form_fields.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/app_configuration/widgets/app_config_form_fields.dart b/lib/app_configuration/widgets/app_config_form_fields.dart index d6d03b39..50ebb7fd 100644 --- a/lib/app_configuration/widgets/app_config_form_fields.dart +++ b/lib/app_configuration/widgets/app_config_form_fields.dart @@ -13,6 +13,7 @@ class AppConfigIntField extends StatelessWidget { required this.value, required this.onChanged, this.controller, + this.enabled = true, super.key, }); @@ -31,6 +32,9 @@ class AppConfigIntField extends StatelessWidget { /// Optional text editing controller for more control. final TextEditingController? controller; + /// Whether the input field is enabled. Defaults to true. + final bool enabled; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -50,6 +54,7 @@ class AppConfigIntField extends StatelessWidget { ), const SizedBox(height: AppSpacing.xs), TextFormField( + enabled: enabled, controller: controller, initialValue: controller == null ? value.toString() : null, keyboardType: TextInputType.number, From c48795da83ee02d7edac75b591026c8e5e37e397 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:16:29 +0100 Subject: [PATCH 2/6] fix(app_configuration): disable ad settings for premium users - Add listener to tab controller to enforce business rule for premium users - Disable ad frequency and placement interval fields for premium users - Ensure premium user ad frequency and placement interval are always set to 0 - Update comments to clarify reasoning for disabled fields --- .../widgets/feed_ad_settings_form.dart | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/app_configuration/widgets/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 49f52b60..05e6a790 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -43,6 +43,36 @@ class _FeedAdSettingsFormState extends State vsync: this, ); _initializeControllers(); + _tabController.addListener(_onTabChanged); + } + + void _onTabChanged() { + if (_tabController.indexIsChanging) return; + + final selectedRole = AppUserRole.values[_tabController.index]; + if (selectedRole == AppUserRole.premiumUser) { + final adConfig = widget.remoteConfig.adConfig; + final feedAdConfig = adConfig.feedAdConfiguration; + + // If the values for premium are not 0, update the config. + // This enforces the business rule that premium users do not see ads. + if (feedAdConfig.frequencyConfig.premiumAdFrequency != 0 || + feedAdConfig.frequencyConfig.premiumAdPlacementInterval != 0) { + final updatedFrequencyConfig = feedAdConfig.frequencyConfig.copyWith( + premiumAdFrequency: 0, + premiumAdPlacementInterval: 0, + ); + final updatedFeedAdConfig = + feedAdConfig.copyWith(frequencyConfig: updatedFrequencyConfig); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + feedAdConfiguration: updatedFeedAdConfig, + ), + ), + ); + } + } } @override @@ -109,7 +139,9 @@ class _FeedAdSettingsFormState extends State @override void dispose() { - _tabController.dispose(); + _tabController + ..removeListener(_onTabChanged) + ..dispose(); for (final controller in _adFrequencyControllers.values) { controller.dispose(); } @@ -257,6 +289,9 @@ class _FeedAdSettingsFormState extends State AppUserRole role, FeedAdConfiguration config, ) { + // Premium users do not see ads, so their settings are disabled. + final isEnabled = role != AppUserRole.premiumUser; + return Column( children: [ AppConfigIntField( @@ -273,6 +308,7 @@ class _FeedAdSettingsFormState extends State ); }, controller: _adFrequencyControllers[role], + enabled: isEnabled, ), AppConfigIntField( label: l10n.adPlacementIntervalLabel, @@ -292,6 +328,7 @@ class _FeedAdSettingsFormState extends State ); }, controller: _adPlacementIntervalControllers[role], + enabled: isEnabled, ), ], ); @@ -338,9 +375,11 @@ class _FeedAdSettingsFormState extends State ), ); case AppUserRole.premiumUser: + // Premium users should not see ads, so their frequency is always 0. + // The UI field is disabled, but this is a safeguard. return config.copyWith( frequencyConfig: config.frequencyConfig.copyWith( - premiumAdFrequency: value, + premiumAdFrequency: 0, ), ); } @@ -365,9 +404,11 @@ class _FeedAdSettingsFormState extends State ), ); case AppUserRole.premiumUser: + // Premium users should not see ads, so their interval is always 0. + // The UI field is disabled, but this is a safeguard. return config.copyWith( frequencyConfig: config.frequencyConfig.copyWith( - premiumAdPlacementInterval: value, + premiumAdPlacementInterval: 0, ), ); } From 9df1464db28b608ee13c6c5045bfc2567ba5e148 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:16:39 +0100 Subject: [PATCH 3/6] fix(app_configuration): enforce premium users never see interstitial ads - Disable and safeguard interstitial ad frequency settings for premium users - Automatically set frequency to 0 when premium user tab is selected - Add listener to tab controller for real-time updates --- .../interstitial_ad_settings_form.dart | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart index a4f252b7..16eda583 100644 --- a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart +++ b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart @@ -42,6 +42,39 @@ class _InterstitialAdSettingsFormState extends State vsync: this, ); _initializeControllers(); + _tabController.addListener(_onTabChanged); + } + + void _onTabChanged() { + if (_tabController.indexIsChanging) return; + + final selectedRole = AppUserRole.values[_tabController.index]; + if (selectedRole == AppUserRole.premiumUser) { + final adConfig = widget.remoteConfig.adConfig; + final interstitialAdConfig = adConfig.interstitialAdConfiguration; + + // If the value for premium is not 0, update the config. + // This enforces the business rule that premium users do not see ads. + if (interstitialAdConfig.feedInterstitialAdFrequencyConfig + .premiumUserTransitionsBeforeShowingInterstitialAds != + 0) { + final updatedFrequencyConfig = interstitialAdConfig + .feedInterstitialAdFrequencyConfig + .copyWith( + premiumUserTransitionsBeforeShowingInterstitialAds: 0, + ); + final updatedInterstitialAdConfig = interstitialAdConfig.copyWith( + feedInterstitialAdFrequencyConfig: updatedFrequencyConfig, + ); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + interstitialAdConfiguration: updatedInterstitialAdConfig, + ), + ), + ); + } + } } @override @@ -96,7 +129,9 @@ class _InterstitialAdSettingsFormState extends State @override void dispose() { - _tabController.dispose(); + _tabController + ..removeListener(_onTabChanged) + ..dispose(); for (final controller in _transitionsBeforeShowingInterstitialAdsControllers.values) { controller.dispose(); @@ -190,6 +225,9 @@ class _InterstitialAdSettingsFormState extends State AppUserRole role, InterstitialAdConfiguration config, ) { + // Premium users do not see ads, so their settings are disabled. + final isEnabled = role != AppUserRole.premiumUser; + return Column( children: [ AppConfigIntField( @@ -211,6 +249,7 @@ class _InterstitialAdSettingsFormState extends State ); }, controller: _transitionsBeforeShowingInterstitialAdsControllers[role], + enabled: isEnabled, ), ], ); @@ -255,8 +294,10 @@ class _InterstitialAdSettingsFormState extends State standardUserTransitionsBeforeShowingInterstitialAds: value, ); case AppUserRole.premiumUser: + // Premium users should not see ads, so their frequency is always 0. + // The UI field is disabled, but this is a safeguard. newFrequencyConfig = currentFrequencyConfig.copyWith( - premiumUserTransitionsBeforeShowingInterstitialAds: value, + premiumUserTransitionsBeforeShowingInterstitialAds: 0, ); } From 5911cb4bca9844bc00cfa165c884fd1671e333f0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:16:59 +0100 Subject: [PATCH 4/6] refactor(local_ads_management): statically define ad type tab order - Introduce a static list `_tabs` to define the order of ad types - Update `TabController` length and `TabBar` tabs to use `_tabs` instead of `AdType.values` - Modify `TabBarView` children to use `_tabs` for consistent tab order --- .../view/archived_local_ads_page.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 94e29789..e94e6e2b 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -51,11 +51,19 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> with SingleTickerProviderStateMixin { late TabController _tabController; + // Statically define the tab order to ensure consistency. + final List _tabs = [ + AdType.native, + AdType.banner, + AdType.interstitial, + AdType.video, + ]; + @override void initState() { super.initState(); _tabController = TabController( - length: AdType.values.length, + length: _tabs.length, vsync: this, ); } @@ -76,9 +84,7 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> controller: _tabController, tabAlignment: TabAlignment.start, isScrollable: true, - tabs: AdType.values - .map((type) => Tab(text: type.l10n(context))) - .toList(), + tabs: _tabs.map((type) => Tab(text: type.l10n(context))).toList(), ), ), body: BlocListener( @@ -149,9 +155,8 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> }, child: TabBarView( controller: _tabController, - children: AdType.values.map((type) { - return _ArchivedLocalAdsDataTable(adType: type); - }).toList(), + children: + _tabs.map((type) => _ArchivedLocalAdsDataTable(adType: type)).toList(), ), ), ); From bd7680231b7535f423f78e06c1a3d6f7b860979e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:17:12 +0100 Subject: [PATCH 5/6] style: format --- lib/app_configuration/widgets/feed_ad_settings_form.dart | 5 +++-- .../widgets/interstitial_ad_settings_form.dart | 7 ++++--- lib/local_ads_management/view/archived_local_ads_page.dart | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/app_configuration/widgets/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 05e6a790..211dd5d9 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -62,8 +62,9 @@ class _FeedAdSettingsFormState extends State premiumAdFrequency: 0, premiumAdPlacementInterval: 0, ); - final updatedFeedAdConfig = - feedAdConfig.copyWith(frequencyConfig: updatedFrequencyConfig); + final updatedFeedAdConfig = feedAdConfig.copyWith( + frequencyConfig: updatedFrequencyConfig, + ); widget.onConfigChanged( widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart index 16eda583..24c57acc 100644 --- a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart +++ b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart @@ -55,14 +55,15 @@ class _InterstitialAdSettingsFormState extends State // If the value for premium is not 0, update the config. // This enforces the business rule that premium users do not see ads. - if (interstitialAdConfig.feedInterstitialAdFrequencyConfig + if (interstitialAdConfig + .feedInterstitialAdFrequencyConfig .premiumUserTransitionsBeforeShowingInterstitialAds != 0) { final updatedFrequencyConfig = interstitialAdConfig .feedInterstitialAdFrequencyConfig .copyWith( - premiumUserTransitionsBeforeShowingInterstitialAds: 0, - ); + premiumUserTransitionsBeforeShowingInterstitialAds: 0, + ); final updatedInterstitialAdConfig = interstitialAdConfig.copyWith( feedInterstitialAdFrequencyConfig: updatedFrequencyConfig, ); diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index e94e6e2b..029b66cb 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -155,8 +155,9 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> }, child: TabBarView( controller: _tabController, - children: - _tabs.map((type) => _ArchivedLocalAdsDataTable(adType: type)).toList(), + children: _tabs + .map((type) => _ArchivedLocalAdsDataTable(adType: type)) + .toList(), ), ), ); From 809a2ab07ac8a738cbbc178e14371c23ba598c67 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 23 Sep 2025 07:24:56 +0100 Subject: [PATCH 6/6] build(deps): update dependencies - Update core.git resolved-ref from dd37d11 to b6a3fb4 - Upgrade go_router from 16.2.1 to 16.2.2 - Upgrade shared_preferences_android from 2.4.12 to 2.4.13 --- pubspec.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1eb68cd4..3987dfff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,7 +90,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: dd37d11352124113978ebeda3b509d932db61cf5 + resolved-ref: b6a3fb4ad862901a56b424fd92a9702f30513404 url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "0.0.0" @@ -269,10 +269,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227 + sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3 url: "https://pub.dev" source: hosted - version: "16.2.1" + version: "16.2.2" google_fonts: dependency: "direct main" description: @@ -488,10 +488,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.13" shared_preferences_foundation: dependency: transitive description: