Skip to content

Commit b98914e

Browse files
committed
refactor(feed): redesign FeedInjectorService for enhanced flexibility
- Replace AccountAction with more generic FeedAction - Introduce support for multiple action types - Enhance user targeting and frequency control - Improve ad injection logic and naming - Update dependencies and class internals
1 parent b58f5d2 commit b98914e

File tree

3 files changed

+97
-132
lines changed

3 files changed

+97
-132
lines changed

lib/shared/services/feed_injector_service.dart

Lines changed: 95 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,59 @@
1-
import 'dart:math';
2-
31
import 'package:ht_shared/ht_shared.dart';
2+
import 'package:uuid/uuid.dart';
43

54
/// A service responsible for injecting various types of FeedItems (like Ads
6-
/// and AccountActions) into a list of primary content items (e.g., Headlines).
5+
/// and FeedActions) into a list of primary content items (e.g., Headlines).
76
class FeedInjectorService {
8-
final Random _random = Random();
7+
final Uuid _uuid = const Uuid();
98

109
/// Processes a list of [Headline] items and injects [Ad] and
11-
/// [AccountAction] items based on the provided configurations and user state.
10+
/// [FeedAction] items based on the provided configurations and user state.
1211
///
1312
/// Parameters:
1413
/// - `headlines`: The list of original [Headline] items.
1514
/// - `user`: The current [User] object (nullable). This is used to determine
16-
/// user role for ad frequency and account action relevance.
17-
/// - `appConfig`: The application's configuration ([AppConfig]), which contains
18-
/// [AdConfig] for ad injection rules and [AccountActionConfig] for
19-
/// account action rules.
15+
/// user role for ad frequency and feed action relevance.
16+
/// - `remoteConfig`: The application's remote configuration ([RemoteConfig]),
17+
/// which contains [AdConfig] for ad injection rules and
18+
/// [AccountActionConfig] for feed action rules.
2019
/// - `currentFeedItemCount`: The total number of items already present in the
2120
/// feed before this batch of headlines is processed. This is crucial for
2221
/// correctly applying ad frequency and placement intervals, especially
2322
/// during pagination. Defaults to 0 for the first batch.
2423
///
2524
/// Returns a new list of [FeedItem] objects, interspersed with ads and
26-
/// account actions according to the defined logic.
25+
/// feed actions according to the defined logic.
2726
List<FeedItem> injectItems({
2827
required List<Headline> headlines,
2928
required User? user,
30-
required AppConfig appConfig,
29+
required RemoteConfig remoteConfig,
3130
int currentFeedItemCount = 0,
3231
}) {
3332
final finalFeed = <FeedItem>[];
34-
var accountActionInjectedThisBatch = false;
33+
var feedActionInjectedThisBatch = false;
3534
var headlinesInThisBatchCount = 0;
36-
final adConfig = appConfig.adConfig;
37-
final userRole = user?.role ?? UserRole.guestUser;
35+
final adConfig = remoteConfig.adConfig;
36+
final userRole = user?.appRole ?? AppUserRole.guestUser;
3837

3938
int adFrequency;
4039
int adPlacementInterval;
4140

4241
switch (userRole) {
43-
case UserRole.guestUser:
42+
case AppUserRole.guestUser:
4443
adFrequency = adConfig.guestAdFrequency;
4544
adPlacementInterval = adConfig.guestAdPlacementInterval;
46-
case UserRole.standardUser: // Assuming 'authenticated' maps to standard
45+
case AppUserRole.standardUser:
4746
adFrequency = adConfig.authenticatedAdFrequency;
4847
adPlacementInterval = adConfig.authenticatedAdPlacementInterval;
49-
case UserRole.premiumUser:
48+
case AppUserRole.premiumUser:
5049
adFrequency = adConfig.premiumAdFrequency;
5150
adPlacementInterval = adConfig.premiumAdPlacementInterval;
52-
default: // For any other roles, or if UserRole enum expands
53-
adFrequency = adConfig.guestAdFrequency; // Default to guest ads
54-
adPlacementInterval = adConfig.guestAdPlacementInterval;
5551
}
5652

57-
// Determine if an AccountAction is due before iterating
58-
final accountActionToInject = _getDueAccountActionDetails(
53+
// Determine if a FeedAction is due before iterating
54+
final feedActionToInject = _getDueFeedAction(
5955
user: user,
60-
appConfig: appConfig,
56+
remoteConfig: remoteConfig,
6157
);
6258

6359
for (var i = 0; i < headlines.length; i++) {
@@ -67,20 +63,17 @@ class FeedInjectorService {
6763

6864
final totalItemsSoFar = currentFeedItemCount + finalFeed.length;
6965

70-
// 1. Inject AccountAction (if due and not already injected in this batch)
66+
// 1. Inject FeedAction (if due and not already injected in this batch)
7167
// Attempt to inject after the first headline of the current batch.
7268
if (i == 0 &&
73-
accountActionToInject != null &&
74-
!accountActionInjectedThisBatch) {
75-
finalFeed.add(accountActionToInject);
76-
accountActionInjectedThisBatch = true;
77-
// Note: AccountAction also counts as an item for ad placement interval
69+
feedActionToInject != null &&
70+
!feedActionInjectedThisBatch) {
71+
finalFeed.add(feedActionToInject);
72+
feedActionInjectedThisBatch = true;
7873
}
7974

8075
// 2. Inject Ad
8176
if (adFrequency > 0 && totalItemsSoFar >= adPlacementInterval) {
82-
// Check frequency against headlines processed *in this batch* after interval met
83-
// This is a simplified local frequency. A global counter might be needed for strict global frequency.
8477
if (headlinesInThisBatchCount % adFrequency == 0) {
8578
final adToInject = _getAdToInject();
8679
if (adToInject != null) {
@@ -92,138 +85,109 @@ class FeedInjectorService {
9285
return finalFeed;
9386
}
9487

95-
AccountAction? _getDueAccountActionDetails({
88+
FeedAction? _getDueFeedAction({
9689
required User? user,
97-
required AppConfig appConfig,
90+
required RemoteConfig remoteConfig,
9891
}) {
99-
final userRole =
100-
user?.role ?? UserRole.guestUser; // Default to guest if user is null
92+
final userRole = user?.appRole ?? AppUserRole.guestUser;
10193
final now = DateTime.now();
102-
final lastActionShown = user?.lastAccountActionShownAt;
103-
final daysBetweenActionsConfig = appConfig.accountActionConfig;
104-
105-
int daysThreshold;
106-
AccountActionType? actionType;
107-
108-
if (userRole == UserRole.guestUser) {
109-
daysThreshold = daysBetweenActionsConfig.guestDaysBetweenAccountActions;
110-
actionType = AccountActionType.linkAccount;
111-
} else if (userRole == UserRole.standardUser) {
112-
daysThreshold =
113-
daysBetweenActionsConfig.standardUserDaysBetweenAccountActions;
114-
115-
// todo(fulleni): once account upgrade feature is implemented,
116-
// uncomment the action type line
117-
// and remove teh null return line.
118-
119-
// actionType = AccountActionType.upgrade;
120-
return null;
121-
} else {
122-
// No account actions for premium users or other roles for now
123-
return null;
124-
}
94+
final actionConfig = remoteConfig.accountActionConfig;
95+
96+
// Iterate through all possible action types to find one that is due.
97+
for (final actionType in FeedActionType.values) {
98+
final status = user?.feedActionStatus[actionType];
99+
100+
// Skip if the action has already been completed.
101+
if (status?.isCompleted ?? false) {
102+
continue;
103+
}
104+
105+
final daysBetweenActionsMap = (userRole == AppUserRole.guestUser)
106+
? actionConfig.guestDaysBetweenActions
107+
: actionConfig.standardUserDaysBetweenActions;
108+
109+
final daysThreshold = daysBetweenActionsMap[actionType];
125110

126-
if (lastActionShown == null ||
127-
now.difference(lastActionShown).inDays >= daysThreshold) {
128-
if (actionType == AccountActionType.linkAccount) {
129-
return _buildLinkAccountActionVariant(appConfig);
130-
} else if (actionType == AccountActionType.upgrade) {
131-
return _buildUpgradeAccountActionVariant(appConfig);
111+
// Skip if there's no configuration for this action type for the user's role.
112+
if (daysThreshold == null) {
113+
continue;
114+
}
115+
116+
final lastShown = status?.lastShownAt;
117+
118+
// Check if the cooldown period has passed.
119+
if (lastShown == null ||
120+
now.difference(lastShown).inDays >= daysThreshold) {
121+
// Found a due action, build and return it.
122+
return _buildFeedActionVariant(actionType);
132123
}
133124
}
125+
126+
// No actions are due at this time.
134127
return null;
135128
}
136129

137-
AccountAction _buildLinkAccountActionVariant(AppConfig appConfig) {
138-
final prefs = appConfig.userPreferenceLimits;
139-
// final prefs = appConfig.userPreferenceLimits; // Not using specific numbers
140-
// final ads = appConfig.adConfig; // Not using specific numbers
141-
final variant = _random.nextInt(3);
142-
130+
FeedAction _buildFeedActionVariant(FeedActionType actionType) {
143131
String title;
144132
String description;
145-
var ctaText = 'Learn More';
133+
String ctaText;
134+
String ctaUrl;
146135

147-
switch (variant) {
148-
case 0:
136+
// TODO(anyone): Use a random variant selection for more dynamic content.
137+
switch (actionType) {
138+
case FeedActionType.linkAccount:
149139
title = 'Unlock Your Full Potential!';
150140
description =
151141
'Link your account to enjoy expanded content access, keep your preferences synced, and experience a more streamlined ad display.';
152142
ctaText = 'Link Account & Explore';
153-
case 1:
154-
title = 'Personalize Your Experience!';
155-
description =
156-
'Secure your settings and reading history across all your devices by linking your account. Enjoy a tailored news journey!';
157-
ctaText = 'Secure My Preferences';
158-
default: // case 2
159-
title = 'Get More From Your News!';
160-
description =
161-
'Link your account for enhanced content limits, better ad experiences, and ensure your preferences are always with you.';
162-
ctaText = 'Get Started';
163-
}
164-
165-
return AccountAction(
166-
title: title,
167-
description: description,
168-
accountActionType: AccountActionType.linkAccount,
169-
callToActionText: ctaText,
170-
// The actual navigation for linking is typically handled by the UI
171-
// when this action item is tapped. The URL can be a deep link or a route.
172-
callToActionUrl: '/authentication?context=linking',
173-
);
174-
}
175-
176-
AccountAction _buildUpgradeAccountActionVariant(AppConfig appConfig) {
177-
final prefs = appConfig.userPreferenceLimits;
178-
// final prefs = appConfig.userPreferenceLimits; // Not using specific numbers
179-
// final ads = appConfig.adConfig; // Not using specific numbers
180-
final variant = _random.nextInt(3);
181-
182-
String title;
183-
String description;
184-
var ctaText = 'Explore Premium';
185-
186-
switch (variant) {
187-
case 0:
143+
ctaUrl = '/authentication?context=linking';
144+
case FeedActionType.upgrade:
188145
title = 'Unlock Our Best Features!';
189146
description =
190147
'Go Premium to enjoy our most comprehensive content access, the best ad experience, and many more exclusive perks.';
191148
ctaText = 'Upgrade Now';
192-
case 1:
193-
title = 'Elevate Your News Consumption!';
194-
description =
195-
'With Premium, your content limits are greatly expanded and you will enjoy our most favorable ad settings. Discover the difference!';
196-
ctaText = 'Discover Premium Benefits';
197-
default: // case 2
198-
title = 'Want More Control & Fewer Interruptions?';
199-
description =
200-
'Upgrade to Premium for a superior ad experience, massively increased content limits, and a more focused news journey.';
201-
ctaText = 'Yes, Upgrade Me!';
149+
ctaUrl = '/account/upgrade';
150+
case FeedActionType.rateApp:
151+
title = 'Enjoying the App?';
152+
description = 'A rating on the app store helps us grow.';
153+
ctaText = 'Rate Us';
154+
ctaUrl = '/app-store-rating'; // Placeholder
155+
case FeedActionType.enableNotifications:
156+
title = 'Stay Updated!';
157+
description = 'Enable notifications to get the latest news instantly.';
158+
ctaText = 'Enable Notifications';
159+
ctaUrl = '/settings/notifications';
160+
case FeedActionType.followTopics:
161+
title = 'Personalize Your Feed';
162+
description = 'Follow topics to see more of what you love.';
163+
ctaText = 'Follow Topics';
164+
ctaUrl = '/account/manage-followed-items/topics';
165+
case FeedActionType.followSources:
166+
title = 'Discover Your Favorite Sources';
167+
description = 'Follow sources to get news from who you trust.';
168+
ctaText = 'Follow Sources';
169+
ctaUrl = '/account/manage-followed-items/sources';
202170
}
203-
return AccountAction(
171+
172+
return FeedAction(
173+
id: _uuid.v4(),
204174
title: title,
205175
description: description,
206-
accountActionType: AccountActionType.upgrade,
176+
feedActionType: actionType,
207177
callToActionText: ctaText,
208-
// URL could point to a subscription page/flow
209-
callToActionUrl: '/account/upgrade', // Placeholder route
178+
callToActionUrl: ctaUrl,
210179
);
211180
}
212181

213-
// Placeholder for _getAdToInject
214182
Ad? _getAdToInject() {
215183
// For now, return a placeholder Ad, always native.
216184
// In a real scenario, this would fetch from an ad network or predefined list.
217-
// final adPlacements = AdPlacement.values; // Can still use for variety if needed
218-
219-
return const Ad(
220-
id: 'tmp-id',
221-
// id is generated by model if not provided
185+
return Ad(
186+
id: _uuid.v4(),
222187
imageUrl:
223-
'https://via.placeholder.com/300x100.png/000000/FFFFFF?Text=Native+Placeholder+Ad', // Adjusted placeholder
188+
'https://via.placeholder.com/300x100.png/000000/FFFFFF?Text=Native+Placeholder+Ad',
224189
targetUrl: 'https://example.com/adtarget',
225-
adType: AdType.native, // Always native
226-
// Default placement or random from native-compatible placements
190+
adType: AdType.native,
227191
placement: AdPlacement.feedInlineNativeBanner,
228192
);
229193
}

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,7 @@ packages:
10561056
source: hosted
10571057
version: "3.1.4"
10581058
uuid:
1059-
dependency: transitive
1059+
dependency: "direct main"
10601060
description:
10611061
name: uuid
10621062
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ dependencies:
6868
stream_transform: ^2.1.1
6969
timeago: ^3.7.1
7070
url_launcher: ^6.3.1
71+
uuid: ^4.4.0
7172

7273
dev_dependencies:
7374
bloc_test: ^10.0.0

0 commit comments

Comments
 (0)