mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-16 07:46:29 +00:00
201 lines
6.3 KiB
TypeScript
201 lines
6.3 KiB
TypeScript
import { mmkvStorage } from './mmkvStorage';
|
|
import { Platform } from 'react-native';
|
|
|
|
// Campaign API URL - use env variable, fallback to local dev server
|
|
const CAMPAIGN_API_URL = process.env.EXPO_PUBLIC_CAMPAIGN_API_URL || 'http://localhost:3000';
|
|
|
|
export type CampaignAction = {
|
|
type: 'link' | 'navigate' | 'dismiss';
|
|
value?: string;
|
|
label: string;
|
|
style?: 'primary' | 'secondary' | 'outline';
|
|
};
|
|
|
|
export type CampaignContent = {
|
|
title?: string;
|
|
message?: string;
|
|
mediaType?: 'image' | 'video';
|
|
imageUrl?: string;
|
|
videoUrl?: string;
|
|
backgroundColor?: string;
|
|
textColor?: string;
|
|
closeButtonColor?: string;
|
|
primaryAction?: CampaignAction | null;
|
|
secondaryAction?: CampaignAction | null;
|
|
aspectRatio?: number;
|
|
};
|
|
|
|
export type CampaignRules = {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
maxImpressions?: number | null;
|
|
minVersion?: string;
|
|
maxVersion?: string;
|
|
platforms?: string[];
|
|
priority: number;
|
|
showOncePerSession?: boolean;
|
|
showOncePerUser?: boolean;
|
|
};
|
|
|
|
export type Campaign = {
|
|
id: string;
|
|
type: 'poster_modal' | 'banner' | 'bottom_sheet';
|
|
content: CampaignContent;
|
|
rules: CampaignRules;
|
|
};
|
|
|
|
class CampaignService {
|
|
private sessionImpressions: Set<string>;
|
|
private campaignQueue: Campaign[] = [];
|
|
private currentIndex: number = 0;
|
|
private lastFetch: number = 0;
|
|
private readonly CACHE_TTL = 5 * 60 * 1000;
|
|
|
|
constructor() {
|
|
this.sessionImpressions = new Set();
|
|
}
|
|
|
|
async getActiveCampaign(): Promise<Campaign | null> {
|
|
try {
|
|
const now = Date.now();
|
|
|
|
console.log('[CampaignService] getActiveCampaign called, API URL:', CAMPAIGN_API_URL);
|
|
|
|
if (this.campaignQueue.length > 0 && (now - this.lastFetch) < this.CACHE_TTL) {
|
|
console.log('[CampaignService] Using cached campaigns');
|
|
return this.getNextValidCampaign();
|
|
}
|
|
|
|
const platform = Platform.OS;
|
|
const url = `${CAMPAIGN_API_URL}/api/campaigns/queue?platform=${platform}`;
|
|
console.log('[CampaignService] Fetching from:', url);
|
|
const response = await fetch(
|
|
`${CAMPAIGN_API_URL}/api/campaigns/queue?platform=${platform}`,
|
|
{
|
|
method: 'GET',
|
|
headers: { 'Accept': 'application/json' },
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.warn('[CampaignService] Failed to fetch campaigns:', response.status);
|
|
return null;
|
|
}
|
|
|
|
const campaigns = await response.json();
|
|
|
|
if (!campaigns || !Array.isArray(campaigns) || campaigns.length === 0) {
|
|
this.campaignQueue = [];
|
|
this.currentIndex = 0;
|
|
this.lastFetch = now;
|
|
return null;
|
|
}
|
|
|
|
campaigns.forEach((campaign: Campaign) => {
|
|
if (campaign.content?.imageUrl && campaign.content.imageUrl.startsWith('/')) {
|
|
campaign.content.imageUrl = `${CAMPAIGN_API_URL}${campaign.content.imageUrl}`;
|
|
}
|
|
if (campaign.content?.videoUrl && campaign.content.videoUrl.startsWith('/')) {
|
|
campaign.content.videoUrl = `${CAMPAIGN_API_URL}${campaign.content.videoUrl}`;
|
|
}
|
|
});
|
|
|
|
console.log('[CampaignService] Fetched campaigns:', campaigns.length, 'CAMPAIGN_API_URL:', CAMPAIGN_API_URL);
|
|
|
|
this.campaignQueue = campaigns;
|
|
this.currentIndex = 0;
|
|
this.lastFetch = now;
|
|
|
|
const result = this.getNextValidCampaign();
|
|
console.log('[CampaignService] Next valid campaign:', result?.id, result?.type);
|
|
return result;
|
|
} catch (error) {
|
|
console.warn('[CampaignService] Error fetching campaigns:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private getNextValidCampaign(): Campaign | null {
|
|
while (this.currentIndex < this.campaignQueue.length) {
|
|
const campaign = this.campaignQueue[this.currentIndex];
|
|
if (this.isLocallyValid(campaign)) {
|
|
return campaign;
|
|
}
|
|
this.currentIndex++;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getNextCampaign(): Campaign | null {
|
|
this.currentIndex++;
|
|
return this.getNextValidCampaign();
|
|
}
|
|
|
|
private isLocallyValid(campaign: Campaign): boolean {
|
|
const { rules } = campaign;
|
|
|
|
if (rules.showOncePerUser && this.hasSeenCampaign(campaign.id)) {
|
|
return false;
|
|
}
|
|
|
|
if (rules.maxImpressions) {
|
|
const impressionCount = this.getImpressionCount(campaign.id);
|
|
if (impressionCount >= rules.maxImpressions) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (rules.showOncePerSession && this.sessionImpressions.has(campaign.id)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private hasSeenCampaign(campaignId: string): boolean {
|
|
return mmkvStorage.getBoolean(`campaign_seen_${campaignId}`) || false;
|
|
}
|
|
|
|
private markCampaignSeen(campaignId: string) {
|
|
mmkvStorage.setBoolean(`campaign_seen_${campaignId}`, true);
|
|
}
|
|
|
|
private getImpressionCount(campaignId: string): number {
|
|
return mmkvStorage.getNumber(`campaign_impression_${campaignId}`) || 0;
|
|
}
|
|
|
|
recordImpression(campaignId: string, showOncePerUser?: boolean) {
|
|
const current = this.getImpressionCount(campaignId);
|
|
mmkvStorage.setNumber(`campaign_impression_${campaignId}`, current + 1);
|
|
this.sessionImpressions.add(campaignId);
|
|
|
|
if (showOncePerUser) {
|
|
this.markCampaignSeen(campaignId);
|
|
}
|
|
}
|
|
|
|
async resetCampaigns() {
|
|
this.sessionImpressions.clear();
|
|
this.campaignQueue = [];
|
|
this.currentIndex = 0;
|
|
this.lastFetch = 0;
|
|
}
|
|
|
|
clearCache() {
|
|
this.campaignQueue = [];
|
|
this.currentIndex = 0;
|
|
this.lastFetch = 0;
|
|
}
|
|
|
|
getRemainingCount(): number {
|
|
let count = 0;
|
|
for (let i = this.currentIndex; i < this.campaignQueue.length; i++) {
|
|
if (this.isLocallyValid(this.campaignQueue[i])) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
export const campaignService = new CampaignService();
|