sojorn/sojorn_app/lib/services/ad_integration_service.dart

167 lines
4.6 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/sponsored_post.dart';
import '../models/post.dart';
import '../providers/api_provider.dart';
/// Helper class for integrating sponsored content into feeds
///
/// Usage:
/// 1. Add a SponsoredPost? _currentAd field to your screen
/// 2. Call loadSponsoredPost(categoryId) when loading posts
/// 3. Insert the SponsoredPostCard widget at the desired position in the list
/// 4. Call recordAdImpression() when the ad becomes visible
class AdIntegrationService {
final Function _read;
AdIntegrationService(this._read);
/// Currently loaded sponsored post (null if none available)
SponsoredPost? get currentAd => _currentAd;
SponsoredPost? _currentAd;
/// Load a sponsored post for the given category
Future<SponsoredPost?> loadSponsoredPost(String? categoryId) async {
if (categoryId == null || categoryId.isEmpty) {
return _currentAd;
}
try {
final apiService = _read(apiServiceProvider);
final ad = await apiService.getSponsoredPost(categoryId: categoryId);
if (ad != null) {
_currentAd = ad;
return ad;
}
if (_currentAd != null && _currentAd!.matchesCategory(categoryId)) {
return _currentAd;
}
'AdIntegrationService: no sponsored post available for $categoryId');
return null;
} catch (e) {
if (_currentAd != null && _currentAd!.matchesCategory(categoryId)) {
return _currentAd;
}
return null;
}
}
/// Load a sponsored post based on an existing post's category
Future<SponsoredPost?> loadSponsoredPostForPost(Post post) async {
return loadSponsoredPost(post.categoryId);
}
/// Record an impression for the current ad
Future<void> recordAdImpression() async {
if (_currentAd != null) {
try {
final apiService = _read(apiServiceProvider);
await apiService.recordAdImpression(_currentAd!.id);
} catch (e) {
// Silently fail - impression tracking is not critical
}
}
}
/// Clear the current ad (e.g., on refresh)
void clearAd() {
_currentAd = null;
}
/// Check if an ad is currently loaded
bool get hasAd => _currentAd != null;
/// Get the current ad for display
SponsoredPost? getAd() => _currentAd;
}
/// Extension on List to interleave sponsored posts
extension ListAdExtension on List<Post> {
/// Insert sponsored content at regular intervals
///
/// [ad] - The sponsored post to insert
/// [interval] - Insert after every N posts (default: 10)
/// [maxAds] - Maximum number of ads to insert (default: 1)
List<Post> interleaveWithAd(
SponsoredPost? ad, {
int interval = 10,
int maxAds = 1,
SponsoredPost? fallbackAd,
}) {
if (isEmpty) {
return [...this];
}
final activeAd = ad ?? fallbackAd;
if (activeAd == null) {
return [...this];
}
final result = <Object>[];
int adCount = 0;
final safeInterval = interval <= 0 ? length : interval;
final maxPossibleAds = length ~/ safeInterval;
final effectiveMaxAds = maxAds.clamp(0, maxPossibleAds);
final adPost = _sponsoredPostToPost(activeAd);
for (int i = 0; i < length; i++) {
result.add(this[i]);
// Insert ad after every N posts, up to maxAds
if ((i + 1) % safeInterval == 0 && adCount < effectiveMaxAds) {
result.add(adPost);
adCount++;
}
}
return result.cast<Post>();
}
}
/// Extension on AsyncData to handle ad loading
extension AdLoadingExtension on AsyncValue<List<Post>> {
/// Transform posts to include sponsored content
Future<AsyncValue<List<Post>>> withSponsoredContent(
String? categoryId,
Ref ref,
) async {
final data = asData;
if (data == null) {
return this;
}
final posts = data.value;
final adService = AdIntegrationService(ref.read);
await adService.loadSponsoredPost(categoryId);
final ad = adService.getAd();
final postsWithAds = posts.interleaveWithAd(
ad,
interval: 10,
maxAds: 2,
fallbackAd: adService.getAd(),
);
return AsyncData(postsWithAds);
}
}
Post _sponsoredPostToPost(SponsoredPost ad) {
return Post(
id: ad.id,
authorId: 'sponsored',
categoryId:
ad.targetCategories.isNotEmpty ? ad.targetCategories.first : null,
body: ad.body,
status: PostStatus.active,
detectedTone: ToneLabel.neutral,
contentIntegrityScore: 1.0,
createdAt: DateTime.now(),
allowChain: false,
imageUrl: ad.imageUrl,
isSponsored: true,
advertiserName: ad.advertiserName,
ctaLink: ad.ctaLink,
ctaText: ad.ctaText,
);
}