1- import 'dart:math' ;
2-
31import '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).
76class 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 }
0 commit comments