import 'dart:convert'; import 'dart:io' show File; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/storage/preferences_storage.dart'; import '../data/ads_api_client.dart'; class SplashAdEntry { const SplashAdEntry(this.ad, this.dailyCap, this.assetPath, this.cachedAtMs); final AdCreative ad; final int dailyCap; final String? assetPath; final int cachedAtMs; bool get valid => ad.isRenderable && DateTime.now().millisecondsSinceEpoch < cachedAtMs + const Duration(hours: 24).inMilliseconds && (ad.endAtMs <= 0 || ad.endAtMs > DateTime.now().millisecondsSinceEpoch); Map toJson() => { 'ad': ad.toJson(), 'dailyCap': dailyCap, 'assetPath': assetPath, 'cachedAtMs': cachedAtMs, }; factory SplashAdEntry.fromJson(Map map) => SplashAdEntry( AdCreative.fromJson(Map.from(map['ad'] as Map)), (map['dailyCap'] as num?)?.toInt() ?? 0, map['assetPath'] as String?, (map['cachedAtMs'] as num?)?.toInt() ?? 0, ); } class SplashAdController { SplashAdController(this._api, this._preferences); final AdsApiClient _api; final PreferencesStorage _preferences; static const _entryKey = 'ads.splash.entry'; static const _dailyKey = 'ads.splash.daily'; Future coldLaunchDecision() async { final raw = _preferences.getString(_entryKey); if (raw == null) return null; try { final entry = SplashAdEntry.fromJson( Map.from(jsonDecode(raw) as Map), ); if (!entry.valid || !_underCap(entry) || !await _ready(entry)) { return null; } return entry; } catch (_) { await clearCache(); return null; } } bool _underCap(SplashAdEntry entry) { final value = _preferences.getString(_dailyKey); final today = DateTime.now().toIso8601String().substring(0, 10); if (value == null) return true; final map = Map.from(jsonDecode(value) as Map); return map['date'] != today || entry.dailyCap <= 0 || ((map['count'] as num?)?.toInt() ?? 0) < entry.dailyCap; } Future _ready(SplashAdEntry entry) async => entry.assetPath != null && await File(entry.assetPath!).exists(); Future markShown(SplashAdEntry entry) async { final today = DateTime.now().toIso8601String().substring(0, 10); final raw = _preferences.getString(_dailyKey); var count = 0; if (raw != null) { final m = Map.from(jsonDecode(raw) as Map); if (m['date'] == today) count = (m['count'] as num?)?.toInt() ?? 0; } await _preferences.setString( _dailyKey, jsonEncode({'date': today, 'count': count + 1}), ); await _report(entry.ad, 'ad_impression'); } Future refresh() async { try { final result = await _api.getAdResult(splashAdPlacement); if (!result.enabled || result.ad == null) { await _clearEntry(); return; } final file = await DefaultCacheManager().getSingleFile( result.ad!.imageUrl, ); if (!await file.exists()) return; await _preferences.setString( _entryKey, jsonEncode( SplashAdEntry( result.ad!, result.dailyCap, file.path, DateTime.now().millisecondsSinceEpoch, ).toJson(), ), ); } catch (_) {} } Future _report(AdCreative ad, String event, {String? clickId}) async { try { await _api.report(ad, event, clickId: clickId); } catch (_) {} } Future skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip'); Future clearCache() async { await _clearEntry(); await _preferences.remove(_dailyKey); } Future _clearEntry() async { await _preferences.remove(_entryKey); } } final splashAdControllerProvider = Provider( (ref) => SplashAdController( ref.watch(adsApiClientProvider), ref.watch(preferencesStorageProvider), ), );