Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
87affcd
feat(l10n): add community config localization keys
fulleni Dec 1, 2025
2ada8d8
build(l10n): sync
fulleni Dec 1, 2025
3ce025d
feat(l10n): create EngagementMode localization extension
fulleni Dec 1, 2025
7ffc3ba
feat(config): create app review settings form widget
fulleni Dec 1, 2025
2bc554e
feat(config): create reporting settings form widget
fulleni Dec 1, 2025
16efe2d
feat(config): create main community config form widget
fulleni Dec 1, 2025
3adde5e
feat(config): integrate community config form into features tab
fulleni Dec 1, 2025
895796c
feat(config): create engagement settings form widget
fulleni Dec 1, 2025
ccb76fd
chore(deps): update core package reference
fulleni Dec 1, 2025
ea6bef4
fix(data): update fixture data initialization with language code
fulleni Dec 1, 2025
2e8c039
feat(app_configuration): add expansion tiles for app review settings
fulleni Dec 1, 2025
f4da20f
feat(l10n): add arb entries for internal prompt logic and follow-up a…
fulleni Dec 1, 2025
2cd4585
build(l10n): sync
fulleni Dec 1, 2025
9311cf9
style(app_configuration): adjust subtitle text style for better visib…
fulleni Dec 1, 2025
9fc0361
refactor(app_configuration): conditionally show app review settings
fulleni Dec 1, 2025
df9b90a
refactor(app_configuration): improve app review settings form layout
fulleni Dec 1, 2025
154ea05
fix(layout): align app review settings to the left
fulleni Dec 1, 2025
7588290
feat(community): add global community features toggle
fulleni Dec 1, 2025
f889261
feat(app_configuration): add switch for source reporting
fulleni Dec 1, 2025
813c7e4
feat(l10n): add translations for community
fulleni Dec 1, 2025
f891710
build(l10n): sync
fulleni Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/app_configuration/view/tabs/features_configuration_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_config_form.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_platform_config_form.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/community_config_form.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/navigation_ad_settings_form.dart';
Expand Down Expand Up @@ -244,6 +245,43 @@ class _FeaturesConfigurationTabState extends State<FeaturesConfigurationTab> {
);
},
),
const SizedBox(height: AppSpacing.lg),

// Community & Engagement
ValueListenableBuilder<int?>(
valueListenable: _expandedTileIndex,
builder: (context, expandedIndex, child) {
const tileIndex = 3;
return ExpansionTile(
key: ValueKey('communityTile_$expandedIndex'),
title: Text(l10n.communityAndEngagementTitle),
subtitle: Text(
l10n.communityAndEngagementDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.7),
),
),
onExpansionChanged: (isExpanded) {
_expandedTileIndex.value = isExpanded ? tileIndex : null;
},
initiallyExpanded: expandedIndex == tileIndex,
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
CommunityConfigForm(
remoteConfig: widget.remoteConfig,
onConfigChanged: widget.onConfigChanged,
),
],
);
},
),
],
);
}
Expand Down
234 changes: 234 additions & 0 deletions lib/app_configuration/widgets/app_review_settings_form.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
import 'package:ui_kit/ui_kit.dart';

/// {@template app_review_settings_form}
/// A form widget for configuring app review funnel settings.
/// {@endtemplate}
class AppReviewSettingsForm extends StatefulWidget {
/// {@macro app_review_settings_form}
const AppReviewSettingsForm({
required this.remoteConfig,
required this.onConfigChanged,
super.key,
});

/// The current [RemoteConfig] object.
final RemoteConfig remoteConfig;

/// Callback to notify parent of changes to the [RemoteConfig].
final ValueChanged<RemoteConfig> onConfigChanged;

@override
State<AppReviewSettingsForm> createState() => _AppReviewSettingsFormState();
}

class _AppReviewSettingsFormState extends State<AppReviewSettingsForm> {
late final TextEditingController _positiveInteractionThresholdController;
late final TextEditingController _initialPromptCooldownController;

@override
void initState() {
super.initState();
_initializeControllers();
}

@override
void didUpdateWidget(covariant AppReviewSettingsForm oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.remoteConfig.features.community.appReview !=
oldWidget.remoteConfig.features.community.appReview) {
_updateControllers();
}
}

void _initializeControllers() {
final appReviewConfig = widget.remoteConfig.features.community.appReview;
_positiveInteractionThresholdController = TextEditingController(
text: appReviewConfig.positiveInteractionThreshold.toString(),
);
_initialPromptCooldownController = TextEditingController(
text: appReviewConfig.initialPromptCooldownDays.toString(),
);
}

void _updateControllers() {
final appReviewConfig = widget.remoteConfig.features.community.appReview;
_positiveInteractionThresholdController.text = appReviewConfig
.positiveInteractionThreshold
.toString();
_initialPromptCooldownController.text = appReviewConfig
.initialPromptCooldownDays
.toString();
}

@override
void dispose() {
_positiveInteractionThresholdController.dispose();
_initialPromptCooldownController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final l10n = AppLocalizationsX(context).l10n;
final communityConfig = widget.remoteConfig.features.community;
final appReviewConfig = communityConfig.appReview;

return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text(l10n.enableAppFeedbackSystemLabel),
subtitle: Text(l10n.enableAppFeedbackSystemDescription),
value: appReviewConfig.enabled,
onChanged: (value) {
final newConfig = communityConfig.copyWith(
appReview: appReviewConfig.copyWith(enabled: value),
);
widget.onConfigChanged(
widget.remoteConfig.copyWith(
features: widget.remoteConfig.features.copyWith(
community: newConfig,
),
),
);
},
),
if (appReviewConfig.enabled) ...[
const SizedBox(height: AppSpacing.lg),
Padding(
padding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg,
),
child: Column(
children: [
ExpansionTile(
title: Text(l10n.internalPromptLogicTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
AppConfigIntField(
label: l10n.positiveInteractionThresholdLabel,
description:
l10n.positiveInteractionThresholdDescription,
value: appReviewConfig.positiveInteractionThreshold,
onChanged: (value) {
final newConfig = communityConfig.copyWith(
appReview: appReviewConfig.copyWith(
positiveInteractionThreshold: value,
),
);
widget.onConfigChanged(
widget.remoteConfig.copyWith(
features: widget.remoteConfig.features
.copyWith(
community: newConfig,
),
),
);
},
controller: _positiveInteractionThresholdController,
),
AppConfigIntField(
label: l10n.initialPromptCooldownLabel,
description: l10n.initialPromptCooldownDescription,
value: appReviewConfig.initialPromptCooldownDays,
onChanged: (value) {
final newConfig = communityConfig.copyWith(
appReview: appReviewConfig.copyWith(
initialPromptCooldownDays: value,
),
);
widget.onConfigChanged(
widget.remoteConfig.copyWith(
features: widget.remoteConfig.features
.copyWith(
community: newConfig,
),
),
);
},
controller: _initialPromptCooldownController,
),
],
),
const SizedBox(height: AppSpacing.lg),
ExpansionTile(
title: Text(l10n.followUpActionsTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text(l10n.requestStoreReviewLabel),
subtitle: Text(l10n.requestStoreReviewDescription),
value: appReviewConfig
.isPositiveFeedbackFollowUpEnabled,
onChanged: (value) {
final newAppReviewConfig = appReviewConfig
.copyWith(
isPositiveFeedbackFollowUpEnabled: value,
);
widget.onConfigChanged(
widget.remoteConfig.copyWith(
features: widget.remoteConfig.features
.copyWith(
community: communityConfig.copyWith(
appReview: newAppReviewConfig,
),
),
),
);
},
),
SwitchListTile(
title: Text(l10n.requestWrittenFeedbackLabel),
subtitle: Text(
l10n.requestWrittenFeedbackDescription,
),
value: appReviewConfig
.isNegativeFeedbackFollowUpEnabled,
onChanged: (value) {
final newAppReviewConfig = appReviewConfig
.copyWith(
isNegativeFeedbackFollowUpEnabled: value,
);
widget.onConfigChanged(
widget.remoteConfig.copyWith(
features: widget.remoteConfig.features
.copyWith(
community: communityConfig.copyWith(
appReview: newAppReviewConfig,
),
),
),
);
},
),
],
),
],
),
),
],
],
),
),
],
);
}
}
Loading
Loading