import { Platform } from 'react-native'; import type { MCCampaign, MCCampaignResponse, MCMeta, } from '../domain'; import { UserService } from '../../offerwall/services/UserService'; // NATIVE CAMPAIGNS: ADDED - same endpoint/query contract as Android and iOS. const API_URL = 'https://native-api.mychips.io/v1.6/native/campaigns'; const REQUEST_TIMEOUT_MS = 15_000; type JsonObject = Record; const stringValue = (value: unknown, fallback = ''): string => typeof value === 'string' ? value : fallback; const numberValue = (value: unknown, fallback = 0): number => typeof value === 'number' && Number.isFinite(value) ? value : fallback; const objectValue = (value: unknown): JsonObject => value != null && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}; const nullableString = (value: unknown): string | null => typeof value === 'string' && value.length > 0 ? value : null; const parseCampaign = (value: unknown): MCCampaign | null => { if (value == null || typeof value !== 'object' || Array.isArray(value)) { return null; } const json = value as JsonObject; const creatives = objectValue(json.creatives); const links = objectValue(json.links); const progressJson = json.progress == null ? null : objectValue(json.progress); return { id: stringValue(json.id), name: stringValue(json.name), type: stringValue(json.type), creatives: { thumbnail: nullableString(creatives.thumbnail), cover: nullableString(creatives.cover), }, links: { trackingUrl: nullableString(links.trackingUrl), trackingPixelUrl: nullableString(links.trackingPixelUrl), detailUrl: nullableString(links.detailUrl), }, totalConvertedValue: numberValue(json.totalConvertedValue), remainingConvertedValue: numberValue(json.remainingConvertedValue), promoRatio: numberValue(json.promoRatio, 1), progress: progressJson == null ? null : { status: stringValue(progressJson.status), eventsCompleted: numberValue(progressJson.eventsCompleted), totalEvents: numberValue(progressJson.totalEvents), valueEarned: numberValue(progressJson.valueEarned), progressValue: numberValue(progressJson.progressValue), }, }; }; export class CampaignService { // Cache belongs to the reused service instance. private cache: MCCampaignResponse | null = null; // Clear the cache on that same instance. clearCache(): void { this.cache = null; } async getCampaigns(useCache = false): Promise { if (useCache && this.cache) { return this.cache; } const user = new UserService(); const [userId, adunitId, limit, advertisingId, gender, age, ...affSubs] = await Promise.all([ user.getOrCreateId(), user.getAdunitId(), user.getLimit(), Platform.OS === 'ios' ? user.getIdfa() : user.getGaid(), user.getGender(), user.getAge(), user.getAffSub1(), user.getAffSub2(), user.getAffSub3(), user.getAffSub4(), user.getAffSub5(), ]); if (!userId) { throw new Error('UserId is required. Call MCOfferwallSDK.setUserId() first.'); } if (!adunitId) { throw new Error( 'AdunitId is required. Call MCOfferwallSDK.setAdunitId() first.' ); } const params: Record = { content_id: adunitId, user_id: userId, limit: String(limit > 0 ? limit : 10), }; const language = Intl.DateTimeFormat().resolvedOptions().locale.split(/[-_]/)[0]; if (language) params.language = language; // Keep the backend's existing (misspelled) contract for native SDK parity. if (advertisingId) params.adverstising_id = advertisingId; if (gender) params.gender = gender; if (age >= 0) params.age = String(age); affSubs.forEach((value, index) => { if (value) params[`aff_sub${index + 1}`] = value; }); const query = Object.entries(params) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); //Use the actual OS release and device model. // Platform.Version is an Android API level (for example 34), not Android 14. const constants = Platform.constants as typeof Platform.constants & { Release?: string; Model?: string; interfaceIdiom?: string; }; const isIOS = Platform.OS === 'ios'; const platformName = isIOS ? 'IOS' : 'ANDROID'; const clientPlatform = isIOS ? 'iOS' : 'Android'; const platformVersion = isIOS ? String(Platform.Version) : String(constants.Release ?? Platform.Version); const deviceModel = isIOS ? constants.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone' : String(constants.Model ?? 'Android'); const userAgent = isIOS ? `MyChipsSDK/iOS (iOS ${platformVersion}; ${deviceModel})` : `MyChipsSDK/Android (Linux; Android ${platformVersion}; ${deviceModel})`; //Both native SDKs time out after 15 seconds. const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let response: Response; try { response = await fetch(`${API_URL}?${query}`, { method: 'GET', signal: controller.signal, headers: { 'User-Agent': userAgent, 'X-Client-Platform': platformName, 'X-Client-Platform-Version': platformVersion, 'Sec-CH-UA-Platform': `"${clientPlatform}"`, 'Sec-CH-UA-Platform-Version': `"${platformVersion}"`, // Avoid a stale native HTTP response after progress/promo changes. 'Cache-Control': 'no-cache, no-store', 'Pragma': 'no-cache', }, }); } finally { clearTimeout(timeout); } if (!response.ok) { const body = await response.text(); throw new Error(`HTTP ${response.status}: ${body}`); } const json = (await response.json()) as JsonObject; const campaigns = Array.isArray(json.campaigns) ? json.campaigns .map(parseCampaign) .filter((campaign): campaign is MCCampaign => campaign !== null) : []; const metaJson = objectValue(json.meta); const meta: MCMeta = { version: stringValue(metaJson.version, '1.0'), count: numberValue(metaJson.count, campaigns.length), }; const result = { campaigns, meta }; this.cache = result; return result; } }