{"version":3,"file":"skan-DO7MLv2w.mjs","names":["sharedCore: SkanCore | null"],"sources":["../src/skan.ts"],"sourcesContent":["// SKAN Module — iOS-only SKAdNetwork conversion value management.\n//\n// Thin facade over the Rust core's SKAN engine, which is the single source of\n// truth for rule evaluation, presets, the monotonic floor, and persistence. This\n// wrapper only bridges to the native iOS SKAdNetwork APIs (which the core cannot\n// call) and reports the native apply outcome back to the core via\n// `skanRecordConversionResult`. There is no parallel JS rule engine.\n\nexport interface SKANConversionRule {\n  eventName: string;\n  conditions?: Record<string, unknown>;\n  conversionValue: number; // 0-63 for SKAN 3.0, 0-7 fine values in SKAN 4.0\n  coarseValue?: 'low' | 'medium' | 'high'; // SKAN 4.0 only\n  lockWindow?: boolean; // SKAN 4.0 only\n  priority?: number;\n  description?: string;\n}\n\nexport interface SKANPresetConfig {\n  name: 'subscriptions' | 'engagement' | 'iap';\n  rules: SKANConversionRule[];\n  description: string;\n  maxValue: number;\n}\n\nexport interface SKANMetrics {\n  isSupported: boolean;\n  version: string;\n  currentValue: number | null;\n  currentPreset: string | null;\n  ruleCount: number;\n  evaluationCount: number;\n}\n\n/**\n * The subset of the Rust core (`LayersCore`) the SKAN facade delegates to. The\n * core owns all rule evaluation, presets, and the monotonic floor.\n */\nexport interface SkanCore {\n  skanSetPreset(preset: string): void;\n  skanSetRules(rulesJson: string): void;\n  skanProcessEvent(\n    eventName: string,\n    properties: Record<string, unknown> | null\n  ): { fineValue: number; coarseValue: string | null; lockWindow: boolean } | null;\n  skanRecordConversionResult(value: number, success: boolean): void;\n  skanCurrentValue(): number;\n  skanCurrentPreset(): string | null;\n  skanIsEnabled(): boolean;\n}\n\ninterface NativeSKANModule {\n  isAvailable(): Promise<boolean>;\n  getVersion(): Promise<string>;\n  registerAppForAdNetworkAttribution(): Promise<void>;\n  updateConversionValue(value: number): Promise<void>;\n  updatePostbackConversionValue(\n    value: number,\n    coarseValue?: string,\n    lockWindow?: boolean\n  ): Promise<void>;\n  getCurrentConversionValue(): Promise<number | null>;\n}\n\nfunction getNativeModule(): NativeSKANModule | null {\n  try {\n    const { NativeModules, Platform } = require('react-native');\n    if (Platform.OS === 'ios' && NativeModules.LayersSKAN) {\n      return NativeModules.LayersSKAN as NativeSKANModule;\n    }\n  } catch {\n    // Not available\n  }\n  return null;\n}\n\nlet sharedCore: SkanCore | null = null;\n\n/**\n * Wire the SKAN facade to the SDK's Rust core. Called by the SDK on init so that\n * a directly-constructed `new SKANManager()` and the SDK's own auto-config both\n * delegate to the same core engine.\n */\nexport function setSkanCore(core: SkanCore | null): void {\n  sharedCore = core;\n}\n\n/**\n * Apply a native SKAN conversion update returned by the core, then report the\n * outcome so the core commits its monotonic floor only on success. Shared by the\n * facade and the SDK's track/screen forwarding.\n */\nexport async function applySkanDecision(\n  core: SkanCore,\n  native: NativeSKANModule | null,\n  decision: { fineValue: number; coarseValue: string | null; lockWindow: boolean }\n): Promise<boolean> {\n  if (!native) {\n    // No native module (Android, or an iOS build without the LayersSKAN\n    // bridge): nothing was posted to Apple. Committing the floor here recorded\n    // conversion values that never left the device, and because the floor is\n    // monotonic it then blocked the real postback if the bridge showed up\n    // later. Report the failure so the core leaves the floor where it is.\n    core.skanRecordConversionResult(decision.fineValue, false);\n    return false;\n  }\n  try {\n    if (decision.coarseValue) {\n      await native.updatePostbackConversionValue(\n        decision.fineValue,\n        decision.coarseValue,\n        decision.lockWindow\n      );\n    } else {\n      await native.updateConversionValue(decision.fineValue);\n    }\n    core.skanRecordConversionResult(decision.fineValue, true);\n    return true;\n  } catch {\n    // Native update failed — the core leaves the floor unchanged so the next\n    // matching event re-issues it (no silently-lost value).\n    core.skanRecordConversionResult(decision.fineValue, false);\n    return false;\n  }\n}\n\nexport class SKANManager {\n  private nativeModule: NativeSKANModule | null;\n  private core: SkanCore | null;\n  private evaluationCount = 0;\n  private onConversionUpdate?: (data: {\n    event: string;\n    previousValue: number;\n    newValue: number;\n    rule: SKANConversionRule;\n  }) => void;\n\n  constructor(\n    onConversionUpdate?: (data: {\n      event: string;\n      previousValue: number;\n      newValue: number;\n      rule: SKANConversionRule;\n    }) => void,\n    core?: SkanCore | null\n  ) {\n    this.nativeModule = getNativeModule();\n    this.core = core ?? sharedCore;\n    if (onConversionUpdate) {\n      this.onConversionUpdate = onConversionUpdate;\n    }\n  }\n\n  /**\n   * Initialize the manager. `arm` controls whether Apple's postback window is\n   * registered — pass `false` on re-config so we don't re-arm mid-session\n   * (re-registering would reset the OS conversion value). Returns `true` only if\n   * the native register was attempted and succeeded (a rejection throws; an\n   * unavailable/no-module case returns false).\n   */\n  async initialize(arm = true): Promise<boolean> {\n    if (!this.nativeModule) return false;\n    const available = await this.nativeModule.isAvailable();\n    if (!available) return false;\n    if (arm) {\n      await this.nativeModule.registerAppForAdNetworkAttribution();\n      return true;\n    }\n    return false;\n  }\n\n  isSupported(): boolean {\n    return this.nativeModule !== null;\n  }\n\n  /** Load a built-in preset in the core (subscriptions/engagement/iap). */\n  setPreset(preset: string): void {\n    this.core?.skanSetPreset(preset);\n  }\n\n  /** Replace the core's active rules with a custom set. */\n  setCustomRules(rules: SKANConversionRule[]): void {\n    this.core?.skanSetRules(JSON.stringify(rules));\n  }\n\n  /**\n   * Evaluate an event via the core. If the core returns an update, apply it to\n   * native SKAdNetwork and report the result back so the core commits its floor\n   * only on success.\n   */\n  async processEvent(eventName: string, properties: Record<string, unknown>): Promise<void> {\n    this.evaluationCount += 1;\n    const core = this.core;\n    if (!core) return;\n    const previousValue = core.skanCurrentValue();\n    const decision = core.skanProcessEvent(eventName, properties ?? {});\n    if (!decision) return;\n    const applied = await applySkanDecision(core, this.nativeModule, decision);\n    // Only notify on a confirmed apply — a failed native postback leaves the\n    // core's floor un-advanced, so the consumer must not see it as posted.\n    if (!applied) return;\n    this.onConversionUpdate?.({\n      event: eventName,\n      previousValue,\n      newValue: decision.fineValue,\n      rule: {\n        eventName,\n        conversionValue: decision.fineValue,\n        lockWindow: decision.lockWindow,\n        ...(decision.coarseValue\n          ? { coarseValue: decision.coarseValue as 'low' | 'medium' | 'high' }\n          : {})\n      }\n    });\n  }\n\n  /** The highest SKAN conversion value posted so far (from the core). */\n  getCurrentValue(): number {\n    return this.core?.skanCurrentValue() ?? 0;\n  }\n\n  /** The active preset name (from the core), or null. */\n  getCurrentPreset(): string | null {\n    return this.core?.skanCurrentPreset() ?? null;\n  }\n\n  /**\n   * The active rules. The core owns the rule set and does not expose it, so this\n   * returns an empty array; use the core's metrics for introspection.\n   */\n  getCurrentRules(): SKANConversionRule[] {\n    return [];\n  }\n\n  getMetrics(): SKANMetrics {\n    return {\n      isSupported: this.isSupported(),\n      version: '4.0',\n      currentValue: this.core?.skanCurrentValue() ?? 0,\n      currentPreset: this.core?.skanCurrentPreset() ?? null,\n      ruleCount: 0,\n      evaluationCount: this.evaluationCount\n    };\n  }\n}\n"],"mappings":";;;AAgEA,SAAS,kBAA2C;AAClD,KAAI;EACF,MAAM,EAAE,eAAe,uBAAqB,eAAe;AAC3D,MAAI,SAAS,OAAO,SAAS,cAAc,WACzC,QAAO,cAAc;SAEjB;AAGR,QAAO;;AAGT,IAAIA,aAA8B;;;;;;AAOlC,SAAgB,YAAY,MAA6B;AACvD,cAAa;;;;;;;AAQf,eAAsB,kBACpB,MACA,QACA,UACkB;AAClB,KAAI,CAAC,QAAQ;AAMX,OAAK,2BAA2B,SAAS,WAAW,MAAM;AAC1D,SAAO;;AAET,KAAI;AACF,MAAI,SAAS,YACX,OAAM,OAAO,8BACX,SAAS,WACT,SAAS,aACT,SAAS,WACV;MAED,OAAM,OAAO,sBAAsB,SAAS,UAAU;AAExD,OAAK,2BAA2B,SAAS,WAAW,KAAK;AACzD,SAAO;SACD;AAGN,OAAK,2BAA2B,SAAS,WAAW,MAAM;AAC1D,SAAO;;;AAIX,IAAa,cAAb,MAAyB;CACvB,AAAQ;CACR,AAAQ;CACR,AAAQ,kBAAkB;CAC1B,AAAQ;CAOR,YACE,oBAMA,MACA;AACA,OAAK,eAAe,iBAAiB;AACrC,OAAK,OAAO,QAAQ;AACpB,MAAI,mBACF,MAAK,qBAAqB;;;;;;;;;CAW9B,MAAM,WAAW,MAAM,MAAwB;AAC7C,MAAI,CAAC,KAAK,aAAc,QAAO;AAE/B,MAAI,CADc,MAAM,KAAK,aAAa,aAAa,CACvC,QAAO;AACvB,MAAI,KAAK;AACP,SAAM,KAAK,aAAa,oCAAoC;AAC5D,UAAO;;AAET,SAAO;;CAGT,cAAuB;AACrB,SAAO,KAAK,iBAAiB;;;CAI/B,UAAU,QAAsB;AAC9B,OAAK,MAAM,cAAc,OAAO;;;CAIlC,eAAe,OAAmC;AAChD,OAAK,MAAM,aAAa,KAAK,UAAU,MAAM,CAAC;;;;;;;CAQhD,MAAM,aAAa,WAAmB,YAAoD;AACxF,OAAK,mBAAmB;EACxB,MAAM,OAAO,KAAK;AAClB,MAAI,CAAC,KAAM;EACX,MAAM,gBAAgB,KAAK,kBAAkB;EAC7C,MAAM,WAAW,KAAK,iBAAiB,WAAW,cAAc,EAAE,CAAC;AACnE,MAAI,CAAC,SAAU;AAIf,MAAI,CAHY,MAAM,kBAAkB,MAAM,KAAK,cAAc,SAAS,CAG5D;AACd,OAAK,qBAAqB;GACxB,OAAO;GACP;GACA,UAAU,SAAS;GACnB,MAAM;IACJ;IACA,iBAAiB,SAAS;IAC1B,YAAY,SAAS;IACrB,GAAI,SAAS,cACT,EAAE,aAAa,SAAS,aAA0C,GAClE,EAAE;IACP;GACF,CAAC;;;CAIJ,kBAA0B;AACxB,SAAO,KAAK,MAAM,kBAAkB,IAAI;;;CAI1C,mBAAkC;AAChC,SAAO,KAAK,MAAM,mBAAmB,IAAI;;;;;;CAO3C,kBAAwC;AACtC,SAAO,EAAE;;CAGX,aAA0B;AACxB,SAAO;GACL,aAAa,KAAK,aAAa;GAC/B,SAAS;GACT,cAAc,KAAK,MAAM,kBAAkB,IAAI;GAC/C,eAAe,KAAK,MAAM,mBAAmB,IAAI;GACjD,WAAW;GACX,iBAAiB,KAAK;GACvB"}