import { Platform, Dimensions, NativeModules } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; // ── Constants ────────────────────────────────────────────────────────────── const ENDPOINT = 'https://uqvknwgcpptxnbmsubkc.supabase.co/functions/v1/track-mobile'; const SDK_VERSION = '1.0.0'; const DEVICE_ID_KEY = 'am_device_id'; const LAUNCHED_KEY = 'am_launched'; const QUEUE_KEY = 'am_event_queue'; const MAX_QUEUE_SIZE = 100; const MAX_RETRIES = 3; const RETRY_DELAY = 2000; const FLUSH_INTERVAL = 30000; // 30 seconds // ── State ────────────────────────────────────────────────────────────────── let appKey = null; let debugMode = false; let queue = []; let sending = false; let flushTimer = null; let userProperties = {}; let sessionId = null; let sessionStart = null; let initialized = false; // ── Utilities ────────────────────────────────────────────────────────────── function log(msg) { if (debugMode) console.log('[AppMeasurely]', msg); } function generateId() { return 'am_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now().toString(36); } async function getDeviceId() { try { let id = await AsyncStorage.getItem(DEVICE_ID_KEY); if (id) return id; id = generateId(); await AsyncStorage.setItem(DEVICE_ID_KEY, id); return id; } catch (e) { return generateId(); } } async function isFirstLaunch() { try { const launched = await AsyncStorage.getItem(LAUNCHED_KEY); return launched === null; } catch (e) { return false; } } async function markLaunched() { try { await AsyncStorage.setItem(LAUNCHED_KEY, 'true'); } catch (e) {} } function getDeviceInfo() { const { width, height } = Dimensions.get('window'); return { device_type: Platform.OS, os_version: Platform.Version ? String(Platform.Version) : 'unknown', screen_width: Math.round(width), screen_height: Math.round(height), language: 'en', // Override with react-native-localize if needed }; } async function buildBasePayload(eventName) { const deviceId = await getDeviceId(); const deviceInfo = getDeviceInfo(); return { app_key: appKey, event_name: eventName, device_id: deviceId, ...deviceInfo, timestamp: new Date().toISOString(), sdk_version: SDK_VERSION, }; } // ── Queue & Sending ──────────────────────────────────────────────────────── async function saveQueue() { try { await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue)); } catch (e) {} } async function loadQueue() { try { const stored = await AsyncStorage.getItem(QUEUE_KEY); if (stored) queue = JSON.parse(stored); } catch (e) {} } async function enqueue(payload) { if (queue.length >= MAX_QUEUE_SIZE) queue.shift(); queue.push(payload); await saveQueue(); log('Queued: ' + payload.event_name); flush(); } async function flush() { if (sending || queue.length === 0) return; sending = true; const payload = queue[0]; const success = await sendWithRetry(payload, MAX_RETRIES); if (success) { queue.shift(); await saveQueue(); sending = false; if (queue.length > 0) flush(); } else { sending = false; setTimeout(flush, RETRY_DELAY); } } async function sendWithRetry(payload, retriesLeft) { try { const response = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': appKey, }, body: JSON.stringify(payload), }); log('Sent: ' + payload.event_name + ' → ' + response.status); if (response.status === 429) return false; // Rate limited return response.ok; } catch (e) { log('Send error: ' + e.message); if (retriesLeft > 0) { await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); return sendWithRetry(payload, retriesLeft - 1); } return false; } } // ── Session Management ───────────────────────────────────────────────────── async function startSession() { sessionId = generateId(); sessionStart = Date.now(); const payload = await buildBasePayload('session_start'); payload.session_id = sessionId; await enqueue(payload); } async function endSession() { if (!sessionId || !sessionStart) return; const duration = Math.round((Date.now() - sessionStart) / 1000); const payload = await buildBasePayload('session_end'); payload.session_id = sessionId; payload.session_duration = duration; await enqueue(payload); sessionId = null; sessionStart = null; } // ── Public API ───────────────────────────────────────────────────────────── const AppMeasurely = { /** * Initialize the SDK * Call this in your App.js or index.js */ async init(key, options = {}) { if (!key) { console.error('[AppMeasurely] App key is required'); return; } appKey = key; debugMode = options.debug || false; initialized = true; // Load persisted queue await loadQueue(); // Track install or app open const isFirst = await isFirstLaunch(); const payload = await buildBasePayload(isFirst ? 'install' : 'app_open'); payload.is_first_launch = isFirst; if (options.mediaSource) payload.media_source = options.mediaSource; if (options.campaign) payload.campaign = options.campaign; await enqueue(payload); if (isFirst) await markLaunched(); // Start session await startSession(); // Start periodic flush flushTimer = setInterval(flush, FLUSH_INTERVAL); log('Initialized. App key: ' + key.substr(0, 8) + '...'); }, /** * Track a custom event */ async trackEvent(eventName, properties = {}) { if (!initialized) return; const payload = await buildBasePayload(eventName); const mergedProps = { ...userProperties, ...properties }; if (Object.keys(mergedProps).length > 0) { payload.properties = mergedProps; } await enqueue(payload); }, /** * Track revenue */ async trackRevenue(amount, currency = 'USD', eventName = 'purchase', properties = {}) { if (!initialized) return; const payload = await buildBasePayload(eventName); payload.revenue = amount; payload.currency = currency; if (Object.keys(properties).length > 0) { payload.properties = properties; } await enqueue(payload); }, /** * Set user property */ setUserProperty(key, value) { userProperties[key] = value; }, /** * Handle app state changes — call from your AppState listener */ async onAppStateChange(nextAppState) { if (nextAppState === 'active') { await startSession(); } else if (nextAppState === 'background' || nextAppState === 'inactive') { await endSession(); } }, /** * Manually flush the event queue */ flush() { flush(); }, /** * Stop tracking */ stop() { initialized = false; if (flushTimer) { clearInterval(flushTimer); flushTimer = null; } }, }; export default AppMeasurely;