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 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; } 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 loadSponsoredPostForPost(Post post) async { return loadSponsoredPost(post.categoryId); } /// Record an impression for the current ad Future 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 { /// 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 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 = []; 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(); } } /// Extension on AsyncData to handle ad loading extension AdLoadingExtension on AsyncValue> { /// Transform posts to include sponsored content Future>> 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, ); }