UNPKG

@layers/react-native

Version:

React Native SDK for Layers Analytics with SKAN and ATT support

992 lines (982 loc) 29.2 kB
import { LayersClient } from "@layers/client"; import { NativeModules, Platform } from "react-native"; import { useEffect } from "react"; export * from "@layers/client" //#region src/native/SKANModule.ts /** * Mock SKAN implementation */ var MockSKANModule = class { conversionValue = 0; isRegistered = false; attributionData = null; async isAvailable() { return Platform.OS === "ios"; } async getVersion() { return "4.0"; } async registerAppForAdNetworkAttribution() { this.isRegistered = true; this.attributionData = { adNetworkId: "mock.network.com", campaignId: "12345", sourceAppId: "123456789", timestamp: (/* @__PURE__ */ new Date()).toISOString() }; console.log("[Mock SKAN] App registered for attribution"); } async updateConversionValue(value) { if (value >= 0 && value <= 63) { this.conversionValue = Math.max(this.conversionValue, value); console.log(`[Mock SKAN] Conversion value updated to: ${value}`); } else console.warn(`[Mock SKAN] Invalid conversion value: ${value}. Must be 0-63.`); } async updatePostbackConversionValue(value, coarseValue, lockWindow) { this.conversionValue = Math.max(this.conversionValue, value); console.log(`[Mock SKAN] Postback value updated: ${value}, coarse: ${coarseValue}, lock: ${lockWindow}`); } async startImpression(signature) { console.log(`[Mock SKAN] Started impression: ${signature}`); } async endImpression(signature) { console.log(`[Mock SKAN] Ended impression: ${signature}`); } async getCurrentConversionValue() { return this.conversionValue; } async getAttributionData() { return this.attributionData; } }; /** * Get SKAN module instance */ function getSKANModule() { if (Platform.OS === "ios") { const nativeModule = NativeModules.LayersSKAN; if (nativeModule) return nativeModule; } return new MockSKANModule(); } /** * SKAN Manager - High-level wrapper for SKAdNetwork functionality with rule engine */ var SKANManager = class { module; onConversionUpdate; currentPreset; rules = []; ruleEvaluationHistory = []; constructor(onConversionUpdate) { this.module = getSKANModule(); if (onConversionUpdate) this.onConversionUpdate = onConversionUpdate; } /** * Initialize SKAN */ async initialize() { if (await this.module.isAvailable()) await this.module.registerAppForAdNetworkAttribution(); } /** * Check if SKAN is supported */ async isSupported() { return this.module.isAvailable(); } /** * Get SKAN version */ async getVersion() { return this.module.getVersion(); } /** * Set conversion rules preset */ setPreset(preset) { this.rules = this.getPresetConfig(preset).rules.sort((a, b) => (b.priority || 0) - (a.priority || 0)); this.currentPreset = preset; console.log(`SKAN preset set to: ${preset} (${this.rules.length} rules)`); } /** * Set custom conversion rules */ setCustomRules(rules) { this.rules = rules.sort((a, b) => (b.priority || 0) - (a.priority || 0)); this.currentPreset = "custom"; console.log(`SKAN custom rules set: ${rules.length} rules`); } /** * Process an event against conversion rules */ async processEvent(eventName, properties) { if (!await this.isSupported()) return; const ruleEvaluations = []; let matchedRule; for (const rule of this.rules) { const evaluation = this.evaluateRule(rule, eventName, properties); const ruleEvaluation = { rule: rule.description || `${rule.eventName} (${rule.conversionValue})`, matched: evaluation.matched }; if (evaluation.reason) ruleEvaluation.reason = evaluation.reason; ruleEvaluations.push(ruleEvaluation); if (evaluation.matched && !matchedRule) { matchedRule = rule; break; } } const historyEntry = { event: eventName, timestamp: (/* @__PURE__ */ new Date()).toISOString() }; if (matchedRule?.description || matchedRule?.eventName) historyEntry.rule = matchedRule.description || matchedRule.eventName; this.ruleEvaluationHistory.push(historyEntry); if (this.ruleEvaluationHistory.length > 100) this.ruleEvaluationHistory = this.ruleEvaluationHistory.slice(-100); if (matchedRule) try { const previousValue = await this.module.getCurrentConversionValue() || 0; if (matchedRule.conversionValue > previousValue) { if (await this.getVersion() === "4.0" && matchedRule.coarseValue) await this.module.updatePostbackConversionValue(matchedRule.conversionValue, matchedRule.coarseValue, matchedRule.lockWindow); else await this.module.updateConversionValue(matchedRule.conversionValue); if (this.onConversionUpdate) this.onConversionUpdate({ event: eventName, properties, matchedRule, previousValue, newValue: matchedRule.conversionValue, timestamp: (/* @__PURE__ */ new Date()).toISOString(), ruleEvaluations }); console.log(`SKAN: Event ${eventName} matched rule '${matchedRule.description || matchedRule.eventName}', value: ${previousValue} → ${matchedRule.conversionValue}`); } else console.log(`SKAN: Event ${eventName} matched rule but value ${matchedRule.conversionValue} not higher than current ${previousValue}`); } catch (error) { console.error("SKAN conversion value update failed:", error); } } /** * Evaluate if a rule matches an event */ evaluateRule(rule, eventName, properties) { if (rule.eventName !== eventName) return { matched: false, reason: `Event name '${eventName}' != '${rule.eventName}'` }; if (rule.conditions) for (const [key, expectedValue] of Object.entries(rule.conditions)) { const actualValue = properties[key]; if (typeof expectedValue === "object" && expectedValue !== null) { for (const [operator, comparisonValue] of Object.entries(expectedValue)) if (!this.evaluateCondition(actualValue, operator, comparisonValue).matched) return { matched: false, reason: `Condition ${key} ${operator} ${comparisonValue} failed (actual: ${actualValue})` }; } else if (actualValue !== expectedValue) return { matched: false, reason: `Property ${key}: expected ${expectedValue}, got ${actualValue}` }; } return { matched: true }; } /** * Evaluate a single condition with operator */ evaluateCondition(actualValue, operator, expectedValue) { const numericActual = Number(actualValue); const numericExpected = Number(expectedValue); switch (operator) { case ">": return { matched: numericActual > numericExpected }; case ">=": return { matched: numericActual >= numericExpected }; case "<": return { matched: numericActual < numericExpected }; case "<=": return { matched: numericActual <= numericExpected }; case "==": case "=": return { matched: actualValue === expectedValue }; case "!=": return { matched: actualValue !== expectedValue }; default: console.warn(`Unknown condition operator: ${operator}`); return { matched: false }; } } /** * Get current conversion value */ async getCurrentValue() { return this.module.getCurrentConversionValue(); } /** * Get attribution data */ async getAttributionData() { return this.module.getAttributionData(); } /** * Get preset configuration */ getPresetConfig(preset) { switch (preset) { case "subscriptions": return { name: "subscriptions", description: "Optimized for subscription apps with trial-to-paid conversion tracking", maxValue: 63, rules: [ { eventName: "app_open", conversionValue: 1, priority: 1, description: "App opened" }, { eventName: "screen_view", conditions: { screen_name: "onboarding_complete" }, conversionValue: 8, priority: 3, description: "Onboarding completed" }, { eventName: "trial_start", conversionValue: 20, priority: 5, description: "Free trial started" }, { eventName: "purchase_success", conditions: { revenue: { "<": 5 } }, conversionValue: 35, priority: 7, description: "Small purchase (<$5)" }, { eventName: "subscription_start", conversionValue: 50, priority: 10, description: "Subscription started" }, { eventName: "subscription_renew", conversionValue: 63, priority: 15, description: "Subscription renewed" } ] }; case "engagement": return { name: "engagement", description: "Optimized for engagement-driven apps focusing on user activity", maxValue: 63, rules: [ { eventName: "app_open", conversionValue: 1, priority: 1, description: "App opened" }, { eventName: "content_open", conversionValue: 5, priority: 2, description: "Content viewed" }, { eventName: "search", conversionValue: 12, priority: 4, description: "Search performed" }, { eventName: "bookmark_add", conversionValue: 25, priority: 6, description: "Content bookmarked" }, { eventName: "app_open", conditions: { session_count: { ">": 3 } }, conversionValue: 40, priority: 8, description: "Regular user (3+ sessions)" }, { eventName: "app_open", conditions: { session_count: { ">": 10 } }, conversionValue: 63, priority: 12, description: "Power user (10+ sessions)" } ] }; case "iap": return { name: "iap", description: "Optimized for in-app purchase revenue tracking", maxValue: 63, rules: [ { eventName: "app_open", conversionValue: 1, priority: 1, description: "App opened" }, { eventName: "paywall_show", conversionValue: 8, priority: 2, description: "Paywall shown" }, { eventName: "purchase_attempt", conversionValue: 15, priority: 3, description: "Purchase attempted" }, { eventName: "purchase_success", conditions: { revenue: { "<": 1 } }, conversionValue: 25, priority: 5, description: "Small purchase (<$1)" }, { eventName: "purchase_success", conditions: { revenue: { ">=": 1, "<": 10 } }, conversionValue: 40, priority: 7, description: "Medium purchase ($1-$10)" }, { eventName: "purchase_success", conditions: { revenue: { ">=": 10 } }, conversionValue: 63, priority: 10, description: "Large purchase ($10+)" } ] }; default: throw new Error(`Unknown preset: ${preset}`); } } /** * Get current preset name */ getCurrentPreset() { return this.currentPreset || null; } /** * Get current rules */ getCurrentRules() { return [...this.rules]; } /** * Get rule evaluation history */ getEvaluationHistory() { return [...this.ruleEvaluationHistory]; } /** * Check if SKAN 4.0 features are available */ async supportsSKAN4() { return await this.getVersion() === "4.0"; } /** * Get SKAN metrics for debugging */ async getSKANMetrics() { const isSupported = await this.isSupported(); const version = await this.getVersion(); const currentValue = await this.getCurrentValue(); const attributionData = await this.getAttributionData(); return { isSupported, version, currentValue, currentPreset: this.currentPreset || null, ruleCount: this.rules.length, evaluationCount: this.ruleEvaluationHistory.length, attributionData, lastEvaluations: this.ruleEvaluationHistory.slice(-5) }; } }; const SKAN = new SKANManager(); //#endregion //#region src/storage/identity.ts const INSTALL_ID_KEY = "layers_install_id"; let asyncStorageInstance = null; let asyncStorageLoadPromise = null; let _pendingPromise = null; async function getAsyncStorage$1() { if (asyncStorageInstance) return asyncStorageInstance; if (!asyncStorageLoadPromise) asyncStorageLoadPromise = import("@react-native-async-storage/async-storage").then((mod) => { asyncStorageInstance = mod.default ?? mod; return asyncStorageInstance; }).catch(() => null); return asyncStorageLoadPromise; } function getOrSetInstallId() { if (_pendingPromise) return _pendingPromise; _pendingPromise = (async () => { try { const AsyncStorage = await getAsyncStorage$1(); if (!AsyncStorage) { console.warn("[Layers] AsyncStorage not found. Using temporary install_id. Identity will not persist across restarts."); return generateUUID(); } const existingId = await AsyncStorage.getItem(INSTALL_ID_KEY); if (existingId) return existingId; const newId = generateUUID(); await AsyncStorage.setItem(INSTALL_ID_KEY, newId); return newId; } catch (error) { console.warn("[Layers] Failed to access storage for install_id", error); return generateUUID(); } })(); return _pendingPromise; } function generateUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; return (c === "x" ? r : r & 3 | 8).toString(16); }); } //#endregion //#region src/storage/nativeQueueStorage.ts const STORAGE_KEY_PREFIX = "layers_queue_"; let _AsyncStorage = void 0; let _initPromise = null; function getAsyncStorage() { if (_AsyncStorage) return Promise.resolve(_AsyncStorage); if (!_initPromise) _initPromise = import("@react-native-async-storage/async-storage").then((module) => { _AsyncStorage = module.default || module; return _AsyncStorage; }).catch(() => { return null; }); return _initPromise; } const createMemoryQueueStorage = () => { let snapshot = null; return { async load() { return snapshot; }, async save(nextSnapshot) { snapshot = nextSnapshot; }, async clear() { snapshot = null; } }; }; function createReactNativeQueueStorage(appId) { const storageKey = `${STORAGE_KEY_PREFIX}${appId}`; const memoryFallback = createMemoryQueueStorage(); let useMemory = false; let hasWarned = false; const getStorageOrFallback = async () => { if (useMemory) return null; const AsyncStorage = await getAsyncStorage(); if (!AsyncStorage) { useMemory = true; if (!hasWarned) { console.warn("[Layers] AsyncStorage not found. Falling back to in-memory storage. Events will not persist across restarts."); hasWarned = true; } return null; } return AsyncStorage; }; return { async load() { const AsyncStorage = await getStorageOrFallback(); if (!AsyncStorage) return memoryFallback.load(); try { const value = await AsyncStorage.getItem(storageKey); if (!value) return null; return JSON.parse(value); } catch (error) { console.warn("[Layers] Failed to load queue from AsyncStorage", error); return null; } }, async save(snapshot) { const AsyncStorage = await getStorageOrFallback(); if (!AsyncStorage) return memoryFallback.save(snapshot); try { await AsyncStorage.setItem(storageKey, JSON.stringify(snapshot)); } catch (error) { console.warn("[Layers] Failed to persist queue to AsyncStorage", error); } }, async clear() { const AsyncStorage = await getStorageOrFallback(); if (!AsyncStorage) return memoryFallback.clear(); try { await AsyncStorage.removeItem(storageKey); } catch (error) { console.warn("[Layers] Failed to clear queue storage", error); } } }; } //#endregion //#region src/ExpoRouterIntegration.ts /** * Hook for tracking screen views with Expo Router. * * @param usePathname - The `usePathname` hook from `expo-router`. * @param useGlobalSearchParams - The `useGlobalSearchParams` hook from `expo-router`. */ function useLayersExpoRouterTracking(usePathname, useGlobalSearchParams) { const pathname = usePathname(); const params = useGlobalSearchParams(); useEffect(() => { if (pathname) { const properties = {}; Object.entries(params).forEach(([key, value]) => { if (Array.isArray(value)) properties[key] = value.join(","); else properties[key] = value; }); Layers.screen(pathname, properties).catch((err) => { console.warn("[Layers] Failed to track screen view:", err); }); } }, [pathname, JSON.stringify(params)]); } /** * Component for tracking screen views with Expo Router. * Render this in your root `_layout.tsx`. */ function LayersExpoRouter({ usePathname, useGlobalSearchParams }) { useLayersExpoRouterTracking(usePathname, useGlobalSearchParams); return null; } //#endregion //#region src/integrations/Commerce.ts const Commerce = { trackPurchase: async (transaction) => { try { await Layers.track("purchase_success", { product_id: transaction.products.map((p) => p.productId).join(","), transaction_id: transaction.transactionId, store: transaction.store, revenue: transaction.revenue, currency: transaction.currency, quantity: transaction.products.reduce((acc, p) => acc + (p.quantity ?? 1), 0), items: transaction.products }); } catch (e) { console.warn("[Layers] Failed to track purchase:", e); } } }; //#endregion //#region src/integrations/RevenueCat.ts /** * Safe wrapper for RevenueCat integration. * Does not import 'react-native-purchases' to avoid runtime crashes. */ const RevenueCat = { connect: (purchasesInstance) => { if (_isConnected) return; try { if (!purchasesInstance || typeof purchasesInstance.addCustomerInfoUpdateListener !== "function" || typeof purchasesInstance.getCustomerInfo !== "function") { console.warn("[Layers] RevenueCat.connect() was passed an invalid object. Expected the default export from \"react-native-purchases\"."); return; } const purchases = purchasesInstance; purchases.addCustomerInfoUpdateListener((info) => { handleCustomerInfoUpdate(info, false); }); purchases.getCustomerInfo().then((info) => handleCustomerInfoUpdate(info, true)).catch(() => {}); _isConnected = true; console.log("[Layers] RevenueCat integration connected."); } catch (e) { console.warn("[Layers] Failed to connect RevenueCat:", e); } }, trackPurchase: async (rcPackage) => { try { if (!rcPackage || !rcPackage.product) return; const { product } = rcPackage; const store = Platform.OS === "ios" ? "app_store" : Platform.OS === "android" ? "play_store" : "custom"; await Layers.track("purchase_success", { product_id: product.identifier, price: product.price, currency: product.currencyCode, store, transaction_id: "", source: "revenuecat" }); } catch (e) {} } }; let _activeSubscriptionsSet = /* @__PURE__ */ new Set(); let _isInitialized = false; let _isConnected = false; function handleCustomerInfoUpdate(info, isInitialLoad) { try { const currentSubscriptions = new Set(info.activeSubscriptions || []); if (isInitialLoad && _isInitialized) return; if (!isInitialLoad && _isInitialized) { for (const subId of currentSubscriptions) if (!_activeSubscriptionsSet.has(subId)) Layers.track("subscription_start", { product_id: subId, platform: "revenuecat" }).catch((err) => { console.warn("[Layers] Failed to track subscription start:", err); }); } _activeSubscriptionsSet = currentSubscriptions; _isInitialized = true; const userProps = { is_subscriber: currentSubscriptions.size > 0 }; if (info.originalAppUserId) userProps.revenuecat_original_app_user_id = info.originalAppUserId; Layers.setUserProperties(userProps).catch((err) => { console.warn("[Layers] Failed to sync user properties:", err); }); } catch (e) { console.warn("[Layers] Error processing RevenueCat update:", e); } } //#endregion //#region src/integrations/Superwall.ts const Superwall = { trackPresentation: async (paywallInfo) => { try { if (!paywallInfo) return; await Layers.track("paywall_show", { paywall_id: paywallInfo.identifier, placement: paywallInfo.name || "unknown", ab_test: paywallInfo.experiment ? { id: paywallInfo.experiment.id, variant: paywallInfo.experiment.variantId } : void 0, url: paywallInfo.url }); } catch (e) {} }, trackDismiss: async (paywallInfo) => { try { if (!paywallInfo) return; await Layers.track("paywall_dismiss", { paywall_id: paywallInfo.identifier, reason: "user_close" }); } catch (e) {} } }; //#endregion //#region src/index.ts const SUPPORTED_NETWORKS = [ "meta", "tiktok", "snap", "google" ]; const DEFAULT_DEEP_LINK_CONFIG = { urlSchemes: [], universalLinkDomains: [], deferredDeepLinkTimeout: 0, attributionWindowDays: 0, trackOrganicLinks: false }; const EMPTY_CONNECTOR_STATUS = { network: "meta", enabled: false, initialized: false, hasSDK: false, eventsSent: 0, errors: 0 }; const EMPTY_CONNECTOR_METRICS = { network: "meta", totalEvents: 0, successfulEvents: 0, failedEvents: 0 }; const EMPTY_DEEP_LINK_METRICS = { totalLinks: 0, uniqueLinks: 0, attributedInstalls: 0, attributedSessions: 0, deferredLinks: 0, sourceBreakdown: {}, conversionRate: 0 }; const NOT_SUPPORTED_ATT_STATUS = "not_determined"; const NOT_SUPPORTED_SKAN_METRICS = { isSupported: false, version: "4.0", currentValue: null, currentPreset: null, ruleCount: 0, evaluationCount: 0, attributionData: null, lastEvaluations: [] }; var LayersRN = class { client = null; isInitialized = false; deepLinkConfig = null; skanManager = null; async init(config) { const installId = await getOrSetInstallId(); this.client = new LayersClient({ ...config, queueStorage: createReactNativeQueueStorage(config.appId), queueOptions: { flushIntervalMs: 1e3, maxQueueSize: 50, maxItemAgeMs: 6e4, requestTimeoutMs: 1e4, maxRetries: 1, baseRetryDelayMs: 500, maxRetryDelayMs: 1e3, ...config.queueOptions ?? {} } }); this.client.setDeviceInfo({ platform: Platform.OS === "ios" ? "ios" : Platform.OS === "android" ? "android" : "react-native", os_version: typeof Platform.Version === "string" ? Platform.Version : String(Platform.Version), install_id: installId }); await this.client.init(); if (Platform.OS === "ios") await this.initializeSKAN(); this.isInitialized = true; if (config.enableDebug) console.log("Layers initialized with SKAN support."); } /** * Initialize SKAN and apply config from server */ async initializeSKAN() { try { this.skanManager = new SKANManager((data) => { if (this.client?.getConfig().enableDebug) console.log("[Layers SKAN] Conversion updated:", { event: data.event, previousValue: data.previousValue, newValue: data.newValue, rule: data.matchedRule?.description }); }); await this.skanManager.initialize(); const remoteConfig = await this.getRemoteConfig(); if (remoteConfig?.skan) await this.applySKANConfig(remoteConfig.skan); } catch (error) { console.warn("[Layers] SKAN initialization failed:", error); } } /** * Get remote config from client */ async getRemoteConfig() { if (!this.client) return null; try { return this.client.remoteConfig ?? null; } catch { return null; } } /** * Apply SKAN configuration from server */ async applySKANConfig(skanConfig) { if (!this.skanManager || !skanConfig.enabled) return; try { if (skanConfig.preset && [ "subscriptions", "engagement", "iap" ].includes(skanConfig.preset)) { this.skanManager.setPreset(skanConfig.preset); console.log(`[Layers SKAN] Preset applied: ${skanConfig.preset}`); } else if (Array.isArray(skanConfig.customRules) && skanConfig.customRules.length > 0) { this.skanManager.setCustomRules(skanConfig.customRules); console.log(`[Layers SKAN] Custom rules applied: ${skanConfig.customRules.length} rules`); } } catch (error) { console.warn("[Layers] Failed to apply SKAN config:", error); } } getClient() { return this.client; } isReady() { return this.isInitialized; } async destroy() { if (this.client) { await this.client.flush(); this.client = null; } this.isInitialized = false; } async track(eventName, properties = {}) { if (!this.client) throw new Error("Layers must be initialized before tracking events"); const eventProperties = { platform: Platform.OS, ...properties }; await this.client.track(eventName, eventProperties); if (Platform.OS === "ios" && this.skanManager) this.skanManager.processEvent(eventName, eventProperties).catch((error) => { if (this.client?.getConfig().enableDebug) console.warn("[Layers SKAN] Event processing failed:", error); }); } async screen(screenName, properties = {}) { await this.track("screen_view", { screen_name: screenName, ...properties }); } async setUserProperties(properties) { if (!this.client) throw new Error("Layers must be initialized before setting user properties"); await this.client.setUserProperties(properties); } setAppUserId(appUserId) { if (!this.client) throw new Error("Layers must be initialized before setting app user ID"); this.client.setAppUserId(appUserId); } clearAppUserId() { if (!this.client) throw new Error("Layers must be initialized before clearing app user ID"); this.client.setAppUserId(void 0); } async setConsent(consent) { if (!this.client) throw new Error("Layers must be initialized before setting consent"); await this.client.setConsent(consent); } async requestTracking(_options) { console.warn("Layers ATT support is disabled in simplified mode."); } debug = { showOverlay: (_options) => { console.warn("Debug overlay is not available in simplified mode."); }, hideOverlay: () => {}, isOverlayVisible: () => false, getATTStatus: async () => NOT_SUPPORTED_ATT_STATUS, getSKANMetrics: async () => { if (Platform.OS !== "ios" || !this.skanManager) return NOT_SUPPORTED_SKAN_METRICS; return this.skanManager.getSKANMetrics(); }, getDeepLinkMetrics: async () => EMPTY_DEEP_LINK_METRICS, getConnectorStatuses: async () => this.createConnectorStatusMap(), testConnectorConnection: async (_network) => false, flushAllEvents: async () => { if (this.client) await this.client.flush(); return this.createZeroCountMap(); } }; connectors = { configure: async (_network, _config) => false, enable: async (_network) => false, disable: async (_network) => false, getStatus: async (network) => ({ ...EMPTY_CONNECTOR_STATUS, network }), getAllStatuses: async () => this.createConnectorStatusMap(), getMetrics: async (network) => ({ ...EMPTY_CONNECTOR_METRICS, network }), testConnection: async (_network) => false, flush: async () => this.createZeroCountMap() }; deepLinks = { configure: async (config) => { this.deepLinkConfig = { ...DEFAULT_DEEP_LINK_CONFIG, ...this.deepLinkConfig ?? {}, ...config }; }, addListener: (_listener) => { console.warn("Deep link listeners are not available in simplified mode."); return () => void 0; }, getMetrics: async () => EMPTY_DEEP_LINK_METRICS, parseUrl: (url) => { try { const parsed = new URL(url); const params = {}; parsed.searchParams.forEach((value, key) => { params[key] = value; }); const data = { url, scheme: parsed.protocol.replace(":", ""), host: parsed.hostname, path: parsed.pathname, queryParams: params, timestamp: Date.now() }; if (parsed.hash) data.fragment = parsed.hash.slice(1); return data; } catch { throw new Error(`Invalid deep link URL: ${url}`); } }, clearAttribution: async () => {} }; att = { request: async (_strategy) => { console.warn("ATT request is not supported in simplified mode."); return NOT_SUPPORTED_ATT_STATUS; }, getStatus: async () => NOT_SUPPORTED_ATT_STATUS, isSupported: async () => false, setCustomPrompt: (_title, _message) => { console.warn("ATT custom prompts are not supported in simplified mode."); } }; skan = { setPreset: async (preset) => { if (Platform.OS !== "ios") { console.warn("[Layers SKAN] Only supported on iOS"); return; } if (!this.skanManager) { console.warn("[Layers SKAN] Not initialized"); return; } this.skanManager.setPreset(preset); }, setRules: async (rules) => { if (Platform.OS !== "ios") { console.warn("[Layers SKAN] Only supported on iOS"); return; } if (!this.skanManager) { console.warn("[Layers SKAN] Not initialized"); return; } this.skanManager.setCustomRules(rules); }, getCurrentConversionValue: async () => { if (Platform.OS !== "ios" || !this.skanManager) return null; return this.skanManager.getCurrentValue(); }, getMetrics: async () => { if (Platform.OS !== "ios" || !this.skanManager) return NOT_SUPPORTED_SKAN_METRICS; return this.skanManager.getSKANMetrics(); } }; createConnectorStatusMap() { const statuses = {}; for (const network of SUPPORTED_NETWORKS) statuses[network] = { ...EMPTY_CONNECTOR_STATUS, network }; return statuses; } createZeroCountMap() { return { meta: 0, tiktok: 0, snap: 0, google: 0 }; } }; const Layers = new LayersRN(); var src_default = Layers; const DebugOverlay = (_props) => null; //#endregion export { Commerce, DebugOverlay, Layers, LayersExpoRouter, RevenueCat, Superwall, src_default as default, useLayersExpoRouterTracking }; //# sourceMappingURL=index.js.map