@kontextso/sdk-react-native 3.4.0-rc.0 → 3.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RNKontext.podspec CHANGED
@@ -15,6 +15,7 @@ Pod::Spec.new do |s|
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
17
17
  s.private_header_files = "ios/**/*.h"
18
+ s.resources = ["ios/PrivacyInfo.xcprivacy"]
18
19
 
19
20
 
20
21
  install_modules_dependencies(s)
@@ -54,6 +54,23 @@ class RNKontextModule(reactContext: ReactApplicationContext) :
54
54
  promise?.resolve(false)
55
55
  }
56
56
 
57
+ // ---------- iOS-only: SKAdNetwork ----------
58
+ override fun initSkAdNetworkImpression(params: com.facebook.react.bridge.ReadableMap, promise: Promise?) {
59
+ promise?.resolve(false)
60
+ }
61
+
62
+ override fun startSkAdNetworkImpression(promise: Promise?) {
63
+ promise?.resolve(false)
64
+ }
65
+
66
+ override fun endSkAdNetworkImpression(promise: Promise?) {
67
+ promise?.resolve(false)
68
+ }
69
+
70
+ override fun disposeSkAdNetwork(promise: Promise?) {
71
+ promise?.resolve(false)
72
+ }
73
+
57
74
  companion object {
58
75
  const val NAME = "RNKontext"
59
76
  }
@@ -66,6 +66,27 @@ class RNKontextModule(reactContext: ReactApplicationContext) :
66
66
  impl.getTcfData(promise)
67
67
  }
68
68
 
69
+ // ---------- iOS-only: SKAdNetwork ----------
70
+ @ReactMethod
71
+ fun initSkAdNetworkImpression(params: com.facebook.react.bridge.ReadableMap, promise: Promise?) {
72
+ promise?.resolve(false)
73
+ }
74
+
75
+ @ReactMethod
76
+ fun startSkAdNetworkImpression(promise: Promise?) {
77
+ promise?.resolve(false)
78
+ }
79
+
80
+ @ReactMethod
81
+ fun endSkAdNetworkImpression(promise: Promise?) {
82
+ promise?.resolve(false)
83
+ }
84
+
85
+ @ReactMethod
86
+ fun disposeSkAdNetwork(promise: Promise?) {
87
+ promise?.resolve(false)
88
+ }
89
+
69
90
  companion object {
70
91
  const val NAME = "RNKontext"
71
92
  }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ var __export = (target, all) => {
11
11
  };
12
12
  var __copyProps = (to, from, except, desc) => {
13
13
  if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
14
+ for (const key of __getOwnPropNames(from))
15
15
  if (!__hasOwnProp.call(to, key) && key !== except)
16
16
  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
17
  }
@@ -100,6 +100,65 @@ public class KontextSDK: NSObject {
100
100
  }
101
101
  }
102
102
 
103
+ // MARK: - SKAdNetwork
104
+
105
+ @objc
106
+ public static func initSkAdNetworkImpression(
107
+ _ params: [String: Any],
108
+ resolver resolve: @escaping RCTPromiseResolveBlock,
109
+ rejecter reject: @escaping RCTPromiseRejectBlock
110
+ ) {
111
+ SKAdNetworkManager.shared.initImpression(params: params) { success, error in
112
+ if let error = error {
113
+ reject("SKAN_INIT_IMPRESSION_FAILED", error.localizedDescription, error)
114
+ } else {
115
+ resolve(success)
116
+ }
117
+ }
118
+ }
119
+
120
+ @objc
121
+ public static func startSkAdNetworkImpression(
122
+ _ resolve: @escaping RCTPromiseResolveBlock,
123
+ rejecter reject: @escaping RCTPromiseRejectBlock
124
+ ) {
125
+ SKAdNetworkManager.shared.startImpression { success, error in
126
+ if let error = error {
127
+ reject("SKAN_START_IMPRESSION_FAILED", error.localizedDescription, error)
128
+ } else {
129
+ resolve(success)
130
+ }
131
+ }
132
+ }
133
+
134
+ @objc
135
+ public static func endSkAdNetworkImpression(
136
+ _ resolve: @escaping RCTPromiseResolveBlock,
137
+ rejecter reject: @escaping RCTPromiseRejectBlock
138
+ ) {
139
+ SKAdNetworkManager.shared.endImpression { success, error in
140
+ if let error = error {
141
+ reject("SKAN_END_IMPRESSION_FAILED", error.localizedDescription, error)
142
+ } else {
143
+ resolve(success)
144
+ }
145
+ }
146
+ }
147
+
148
+ @objc
149
+ public static func disposeSkAdNetwork(
150
+ _ resolve: @escaping RCTPromiseResolveBlock,
151
+ rejecter reject: @escaping RCTPromiseRejectBlock
152
+ ) {
153
+ SKAdNetworkManager.shared.dispose { success, error in
154
+ if let error = error {
155
+ reject("SKAN_DISPOSE_FAILED", error.localizedDescription, error)
156
+ } else {
157
+ resolve(success)
158
+ }
159
+ }
160
+ }
161
+
103
162
  // MARK: - TCF (minimal: GDPR applies + TC string)
104
163
 
105
164
  /// Returns minimal TCF data needed for RTB:
package/ios/RNKontext.mm CHANGED
@@ -76,6 +76,27 @@ RCT_EXPORT_MODULE()
76
76
  resolve([KontextSDK getVendorId]);
77
77
  }
78
78
 
79
+ - (void)initSkAdNetworkImpression:(NSDictionary *)params
80
+ resolve:(RCTPromiseResolveBlock)resolve
81
+ reject:(RCTPromiseRejectBlock)reject {
82
+ [KontextSDK initSkAdNetworkImpression:params resolver:resolve rejecter:reject];
83
+ }
84
+
85
+ - (void)startSkAdNetworkImpression:(RCTPromiseResolveBlock)resolve
86
+ reject:(RCTPromiseRejectBlock)reject {
87
+ [KontextSDK startSkAdNetworkImpression:resolve rejecter:reject];
88
+ }
89
+
90
+ - (void)endSkAdNetworkImpression:(RCTPromiseResolveBlock)resolve
91
+ reject:(RCTPromiseRejectBlock)reject {
92
+ [KontextSDK endSkAdNetworkImpression:resolve rejecter:reject];
93
+ }
94
+
95
+ - (void)disposeSkAdNetwork:(RCTPromiseResolveBlock)resolve
96
+ reject:(RCTPromiseRejectBlock)reject {
97
+ [KontextSDK disposeSkAdNetwork:resolve rejecter:reject];
98
+ }
99
+
79
100
  #else
80
101
 
81
102
  RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)resolve
@@ -143,6 +164,27 @@ RCT_EXPORT_METHOD(getVendorId:(RCTPromiseResolveBlock)resolve
143
164
  resolve([KontextSDK getVendorId]);
144
165
  }
145
166
 
167
+ RCT_EXPORT_METHOD(initSkAdNetworkImpression:(NSDictionary *)params
168
+ resolver:(RCTPromiseResolveBlock)resolve
169
+ rejecter:(RCTPromiseRejectBlock)reject) {
170
+ [KontextSDK initSkAdNetworkImpression:params resolver:resolve rejecter:reject];
171
+ }
172
+
173
+ RCT_EXPORT_METHOD(startSkAdNetworkImpression:(RCTPromiseResolveBlock)resolve
174
+ rejecter:(RCTPromiseRejectBlock)reject) {
175
+ [KontextSDK startSkAdNetworkImpression:resolve rejecter:reject];
176
+ }
177
+
178
+ RCT_EXPORT_METHOD(endSkAdNetworkImpression:(RCTPromiseResolveBlock)resolve
179
+ rejecter:(RCTPromiseRejectBlock)reject) {
180
+ [KontextSDK endSkAdNetworkImpression:resolve rejecter:reject];
181
+ }
182
+
183
+ RCT_EXPORT_METHOD(disposeSkAdNetwork:(RCTPromiseResolveBlock)resolve
184
+ rejecter:(RCTPromiseRejectBlock)reject) {
185
+ [KontextSDK disposeSkAdNetwork:resolve rejecter:reject];
186
+ }
187
+
146
188
  #endif
147
189
 
148
190
  @end
@@ -0,0 +1,257 @@
1
+ import Foundation
2
+ import StoreKit
3
+
4
+ final class SKAdNetworkManager {
5
+ static let shared = SKAdNetworkManager()
6
+ private init() {}
7
+
8
+ private var skImpressionBox: Any?
9
+ private var isStarted: Bool = false
10
+
11
+ @available(iOS 14.5, *)
12
+ private var skImpression: SKAdImpression? {
13
+ get { skImpressionBox as? SKAdImpression }
14
+ set { skImpressionBox = newValue }
15
+ }
16
+
17
+ /// Required keys:
18
+ /// - version: String
19
+ /// - network: String (adNetworkIdentifier)
20
+ /// - itunesItem: String/Int (advertisedAppStoreItemIdentifier)
21
+ /// - sourceApp: String/Int (sourceAppStoreItemIdentifier, 0 if no App Store ID)
22
+ /// Optional keys:
23
+ /// - sourceIdentifier: String/Int (SKAdNetwork 4.0, iOS 16.1+)
24
+ /// - campaign: String/Int (adCampaignIdentifier)
25
+ /// - fidelities: Array (iOS 16.1+; each entry may contain nonce, timestamp, signature)
26
+ /// - nonce: String (adImpressionIdentifier; required if no fidelities)
27
+ /// - timestamp: String/Int (required if no fidelities)
28
+ /// - signature: String (required if no fidelities)
29
+ func initImpression(params: [String: Any], completion: @escaping (Bool, NSError?) -> Void) {
30
+ guard #available(iOS 14.5, *) else {
31
+ completeOnMain(completion, false, nil)
32
+ return
33
+ }
34
+
35
+ func num(_ any: Any?) -> NSNumber? {
36
+ if let n = any as? NSNumber { return n }
37
+ if let i = any as? Int { return NSNumber(value: i) }
38
+ if let d = any as? Double {
39
+ guard d == d.rounded() else { return nil }
40
+ return NSNumber(value: Int(d))
41
+ }
42
+ if let s = any as? String, let i = Int(s) { return NSNumber(value: i) }
43
+ return nil
44
+ }
45
+
46
+ // Required
47
+ let version = params["version"] as? String
48
+ let networkId = params["network"] as? String
49
+ let itunesItem = num(params["itunesItem"])
50
+ let sourceApp = num(params["sourceApp"]) ?? NSNumber(value: 0)
51
+
52
+ // Optional
53
+ let campaign = num(params["campaign"])
54
+ let sourceIdentifier = num(params["sourceIdentifier"])
55
+ let nonce = params["nonce"] as? String
56
+ let timestamp = num(params["timestamp"])
57
+ let signature = params["signature"] as? String
58
+ let fidelities = params["fidelities"] as? [[String: Any]]
59
+
60
+ let hasFidelities: Bool = {
61
+ if #available(iOS 16.1, *) {
62
+ return !(fidelities?.isEmpty ?? true)
63
+ }
64
+ return false
65
+ }()
66
+
67
+ func isBlank(_ s: String?) -> Bool {
68
+ return s?.trimmingCharacters(in: .whitespaces).isEmpty ?? true
69
+ }
70
+
71
+ var missing: [String] = []
72
+ if isBlank(version) { missing.append("version") }
73
+ if isBlank(networkId) { missing.append("network") }
74
+ if itunesItem == nil { missing.append("itunesItem") }
75
+ if !hasFidelities {
76
+ if isBlank(nonce) { missing.append("nonce") }
77
+ if timestamp == nil { missing.append("timestamp") }
78
+ if isBlank(signature) { missing.append("signature") }
79
+ }
80
+
81
+ guard missing.isEmpty else {
82
+ var message = "Missing required arguments: \(missing.joined(separator: ", "))"
83
+ if fidelities != nil && !hasFidelities {
84
+ message += " Note: fidelities array was provided but is only supported on iOS 16.1+. "
85
+ + "Top-level nonce/timestamp/signature are required on this OS version."
86
+ }
87
+ let error = NSError(
88
+ domain: "SKAdNetwork",
89
+ code: 1,
90
+ userInfo: [NSLocalizedDescriptionKey: message]
91
+ )
92
+ completeOnMain(completion, false, error)
93
+ return
94
+ }
95
+
96
+ let previousImpression = isStarted ? skImpression : nil
97
+ isStarted = false
98
+
99
+ if #available(iOS 16.0, *) {
100
+ let imp = SKAdImpression(
101
+ sourceAppStoreItemIdentifier: sourceApp,
102
+ advertisedAppStoreItemIdentifier: itunesItem!,
103
+ adNetworkIdentifier: networkId!,
104
+ adCampaignIdentifier: campaign ?? NSNumber(value: 0),
105
+ adImpressionIdentifier: nonce ?? "",
106
+ timestamp: timestamp ?? NSNumber(value: 0),
107
+ signature: signature ?? "",
108
+ version: version!
109
+ )
110
+
111
+ if #available(iOS 16.1, *) {
112
+ if let sourceIdentifier = sourceIdentifier {
113
+ imp.sourceIdentifier = sourceIdentifier
114
+ }
115
+ if hasFidelities, let fidelities = fidelities {
116
+ parseFidelities(fidelities, into: imp)
117
+ }
118
+ }
119
+
120
+ skImpression = imp
121
+ } else {
122
+ // iOS 14.5–15.x: memberwise initializer not available, use property-based init
123
+ let imp = SKAdImpression()
124
+ imp.sourceAppStoreItemIdentifier = sourceApp
125
+ imp.advertisedAppStoreItemIdentifier = itunesItem!
126
+ imp.adNetworkIdentifier = networkId!
127
+ imp.adCampaignIdentifier = campaign ?? NSNumber(value: 0)
128
+ imp.adImpressionIdentifier = nonce ?? ""
129
+ imp.timestamp = timestamp ?? NSNumber(value: 0)
130
+ imp.signature = signature ?? ""
131
+ imp.version = version!
132
+ skImpression = imp
133
+ }
134
+
135
+ // End the previous impression only after the new one is safely stored.
136
+ // Failure here is best-effort — we log it but don't block the caller.
137
+ if let old = previousImpression {
138
+ SKAdNetwork.endImpression(old) { error in
139
+ if let error = error {
140
+ print("[SKAdNetwork] Warning: failed to end previous impression: \(error)")
141
+ }
142
+ }
143
+ }
144
+
145
+ completeOnMain(completion, true, nil)
146
+ }
147
+
148
+ func startImpression(completion: @escaping (Bool, NSError?) -> Void) {
149
+ guard #available(iOS 14.5, *) else {
150
+ completeOnMain(completion, false, nil)
151
+ return
152
+ }
153
+ guard let impression = skImpression else {
154
+ let error = NSError(
155
+ domain: "SKAdNetwork",
156
+ code: 2,
157
+ userInfo: [NSLocalizedDescriptionKey: "SKAdImpression not initialized. Call initImpression first."]
158
+ )
159
+ completeOnMain(completion, false, error)
160
+ return
161
+ }
162
+ guard !isStarted else {
163
+ // Already started — ignore duplicate call
164
+ completeOnMain(completion, true, nil)
165
+ return
166
+ }
167
+
168
+ isStarted = true
169
+ SKAdNetwork.startImpression(impression) { [weak self] error in
170
+ if let error = error {
171
+ self?.isStarted = false
172
+ let nsError = NSError(
173
+ domain: "SKAdNetwork",
174
+ code: 3,
175
+ userInfo: [NSLocalizedDescriptionKey: "Failed to start SKAdImpression: \(error)"]
176
+ )
177
+ self?.completeOnMain(completion, false, nsError)
178
+ } else {
179
+ self?.completeOnMain(completion, true, nil)
180
+ }
181
+ }
182
+ }
183
+
184
+ func endImpression(completion: @escaping (Bool, NSError?) -> Void) {
185
+ guard #available(iOS 14.5, *) else {
186
+ completeOnMain(completion, false, nil)
187
+ return
188
+ }
189
+ guard let impression = skImpression else {
190
+ let error = NSError(
191
+ domain: "SKAdNetwork",
192
+ code: 2,
193
+ userInfo: [NSLocalizedDescriptionKey: "SKAdImpression not initialized. Call initImpression first."]
194
+ )
195
+ completeOnMain(completion, false, error)
196
+ return
197
+ }
198
+ guard isStarted else {
199
+ // Not started — ignore unmatched endImpression
200
+ completeOnMain(completion, true, nil)
201
+ return
202
+ }
203
+
204
+ isStarted = false
205
+ SKAdNetwork.endImpression(impression) { [weak self] error in
206
+ if let error = error {
207
+ self?.isStarted = true // roll back — end failed
208
+ let nsError = NSError(
209
+ domain: "SKAdNetwork",
210
+ code: 4,
211
+ userInfo: [NSLocalizedDescriptionKey: "Failed to end SKAdImpression: \(error)"]
212
+ )
213
+ self?.completeOnMain(completion, false, nsError)
214
+ } else {
215
+ self?.completeOnMain(completion, true, nil)
216
+ }
217
+ }
218
+ }
219
+
220
+ func dispose(completion: @escaping (Bool, NSError?) -> Void) {
221
+ if #available(iOS 14.5, *), isStarted, let impression = skImpression {
222
+ isStarted = false
223
+ SKAdNetwork.endImpression(impression) { _ in }
224
+ } else {
225
+ isStarted = false
226
+ }
227
+
228
+ skImpressionBox = nil
229
+ completeOnMain(completion, true, nil)
230
+ }
231
+
232
+ // MARK: - Private
233
+
234
+ @available(iOS 16.1, *)
235
+ private func parseFidelities(_ fidelities: [[String: Any]], into imp: SKAdImpression) {
236
+ for f in fidelities {
237
+ if imp.adImpressionIdentifier.isEmpty, let nonce = f["nonce"] as? String {
238
+ imp.adImpressionIdentifier = nonce
239
+ }
240
+ if imp.timestamp == NSNumber(value: 0) {
241
+ if let n = f["timestamp"] as? NSNumber { imp.timestamp = n }
242
+ else if let s = f["timestamp"] as? String, let i = Int(s) { imp.timestamp = NSNumber(value: i) }
243
+ }
244
+ if imp.signature.isEmpty, let sig = f["signature"] as? String {
245
+ imp.signature = sig
246
+ }
247
+ }
248
+ }
249
+
250
+ private func completeOnMain(_ completion: @escaping (Bool, NSError?) -> Void, _ success: Bool, _ error: NSError?) {
251
+ if Thread.isMainThread {
252
+ completion(success, error)
253
+ } else {
254
+ DispatchQueue.main.async { completion(success, error) }
255
+ }
256
+ }
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "3.4.0-rc.0",
3
+ "version": "3.4.0-rc.1",
4
4
  "description": "Kontext SDK for React Native",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -20,7 +20,7 @@
20
20
  "format": "biome format --write ."
21
21
  },
22
22
  "devDependencies": {
23
- "@kontextso/sdk-common": "^1.0.4",
23
+ "@kontextso/sdk-common": "^1.0.7",
24
24
  "@kontextso/typescript-config": "*",
25
25
  "@react-native-community/netinfo": "11.3.1",
26
26
  "@testing-library/dom": "^10.4.0",
@@ -47,14 +47,14 @@
47
47
  "vitest": "^2.1.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "@react-native-community/netinfo": "^11.0",
50
+ "@react-native-community/netinfo": ">=11.0",
51
51
  "react": ">=18.0.0",
52
52
  "react-native": ">=0.73.0",
53
53
  "react-native-device-info": ">=10.0.0 <15.0.0",
54
54
  "react-native-webview": "^13.10.0"
55
55
  },
56
56
  "dependencies": {
57
- "@kontextso/sdk-react": "^3.0.8-rc.0"
57
+ "@kontextso/sdk-react": "^3.0.9"
58
58
  },
59
59
  "files": [
60
60
  "dist/*",
@@ -22,6 +22,12 @@ export interface Spec extends TurboModule {
22
22
  // IFA
23
23
  getAdvertisingId(): Promise<string | null>
24
24
  getVendorId(): Promise<string | null>
25
+
26
+ // SKAdNetwork
27
+ initSkAdNetworkImpression(params: Record<string, any>): Promise<boolean>
28
+ startSkAdNetworkImpression(): Promise<boolean>
29
+ endSkAdNetworkImpression(): Promise<boolean>
30
+ disposeSkAdNetwork(): Promise<boolean>
25
31
  }
26
32
 
27
33
  export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
@@ -18,6 +18,12 @@ import { useContext, useEffect, useRef, useState } from 'react'
18
18
  import { Keyboard, Linking, Modal, useWindowDimensions, View } from 'react-native'
19
19
  import type { WebView, WebViewMessageEvent } from 'react-native-webview'
20
20
  import FrameWebView from '../frame-webview'
21
+ import {
22
+ disposeSkAdNetwork,
23
+ endSkAdNetworkImpression,
24
+ initSkAdNetworkImpression,
25
+ startSkAdNetworkImpression,
26
+ } from '../services/SkAdNetwork'
21
27
  import { dismissSKOverlay, presentSKOverlay, type SKOverlayPosition } from '../services/SkOverlay'
22
28
  import { dismissSKStoreProduct, presentSKStoreProduct } from '../services/SkStoreProduct'
23
29
 
@@ -55,6 +61,11 @@ enum MessageStatus {
55
61
  MessageReceived = 'message-received',
56
62
  }
57
63
 
64
+ enum AttributionType {
65
+ None = 'none',
66
+ Skan = 'skan',
67
+ }
68
+
58
69
  const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatProps) => {
59
70
  const context = useContext(AdsContext)
60
71
 
@@ -76,6 +87,8 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
76
87
  const [containerStyles, setContainerStyles] = useState<any>({})
77
88
  const [iframeStyles, setIframeStyles] = useState<any>({})
78
89
 
90
+ const attributionTypeRef = useRef<AttributionType>(AttributionType.None)
91
+
79
92
  const containerRef = useRef<View>(null)
80
93
  const webViewRef = useRef<WebView>(null)
81
94
  const modalWebViewRef = useRef<WebView>(null)
@@ -211,6 +224,45 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
211
224
  }
212
225
  }
213
226
 
227
+ const handleAttributionInit = async () => {
228
+ if (!bid?.skan) {
229
+ return
230
+ }
231
+ try {
232
+ const success = await initSkAdNetworkImpression(bid.skan)
233
+ if (success) {
234
+ attributionTypeRef.current = AttributionType.Skan
235
+ }
236
+ } catch (e) {
237
+ debug('error-attribution-init', { error: e })
238
+ console.error('error initializing skadnetwork impression', e)
239
+ }
240
+ }
241
+
242
+ const handleAttributionBeginView = async () => {
243
+ try {
244
+ if (attributionTypeRef.current === AttributionType.Skan) {
245
+ await startSkAdNetworkImpression()
246
+ }
247
+ } catch (e) {
248
+ debug('error-attribution-begin-view', { error: e })
249
+ console.error('error starting skadnetwork impression', e)
250
+ }
251
+ }
252
+
253
+ const cleanupAttribution = async () => {
254
+ try {
255
+ if (attributionTypeRef.current === AttributionType.Skan) {
256
+ await endSkAdNetworkImpression()
257
+ await disposeSkAdNetwork()
258
+ }
259
+ } catch (e) {
260
+ console.error('error cleaning up skadnetwork', e)
261
+ } finally {
262
+ attributionTypeRef.current = AttributionType.None
263
+ }
264
+ }
265
+
214
266
  const openUrl = async (message: IframeMessage<'click-iframe'>) => {
215
267
  if (!message.data.url) {
216
268
  return
@@ -261,6 +313,9 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
261
313
  otherParams,
262
314
  messageId,
263
315
  })
316
+ if (bid?.skan) {
317
+ handleAttributionInit()
318
+ }
264
319
  break
265
320
 
266
321
  case 'error-iframe':
@@ -283,6 +338,7 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
283
338
  if (bid?.bidId && message.data.cachedContent) {
284
339
  context?.cachedContentRef?.current?.set(bid.bidId, message.data.cachedContent)
285
340
  }
341
+ handleAttributionBeginView()
286
342
  break
287
343
 
288
344
  case 'show-iframe':
@@ -461,6 +517,12 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
461
517
 
462
518
  const paramsString = convertParamsToString(otherParams)
463
519
 
520
+ useEffect(() => {
521
+ return () => {
522
+ cleanupAttribution()
523
+ }
524
+ }, [])
525
+
464
526
  useEffect(() => {
465
527
  if (!iframeLoaded || !context?.adServerUrl || !bid || !webViewRef.current) {
466
528
  return
@@ -0,0 +1,75 @@
1
+ import { Platform } from 'react-native'
2
+ import NativeRNKontext from '../NativeRNKontext'
3
+
4
+ export interface SkAdNetworkFidelity {
5
+ fidelity: number
6
+ signature: string
7
+ nonce: string
8
+ timestamp: string | number
9
+ }
10
+
11
+ export interface SkAdNetworkImpressionParams {
12
+ // Required
13
+ version: string
14
+ network: string
15
+ itunesItem: string | number
16
+ sourceApp: string | number
17
+ // Optional
18
+ sourceIdentifier?: string | number
19
+ campaign?: string | number
20
+ fidelities?: SkAdNetworkFidelity[]
21
+ nonce?: string
22
+ timestamp?: string | number
23
+ signature?: string
24
+ }
25
+
26
+ let impressionReady = false
27
+
28
+ export async function initSkAdNetworkImpression(params: SkAdNetworkImpressionParams): Promise<boolean> {
29
+ if (Platform.OS !== 'ios') return false
30
+
31
+ try {
32
+ const result = await NativeRNKontext.initSkAdNetworkImpression(params)
33
+ impressionReady = result
34
+ return result
35
+ } catch (e) {
36
+ impressionReady = false
37
+ console.error('[SKAdNetwork] Error initializing impression:', e)
38
+ return false
39
+ }
40
+ }
41
+
42
+ export async function startSkAdNetworkImpression(): Promise<boolean> {
43
+ if (Platform.OS !== 'ios' || !impressionReady) return false
44
+
45
+ try {
46
+ return await NativeRNKontext.startSkAdNetworkImpression()
47
+ } catch (e) {
48
+ console.error('[SKAdNetwork] Error starting impression:', e)
49
+ return false
50
+ }
51
+ }
52
+
53
+ export async function endSkAdNetworkImpression(): Promise<boolean> {
54
+ if (Platform.OS !== 'ios' || !impressionReady) return false
55
+
56
+ try {
57
+ return await NativeRNKontext.endSkAdNetworkImpression()
58
+ } catch (e) {
59
+ console.error('[SKAdNetwork] Error ending impression:', e)
60
+ return false
61
+ }
62
+ }
63
+
64
+ export async function disposeSkAdNetwork(): Promise<boolean> {
65
+ impressionReady = false
66
+
67
+ if (Platform.OS !== 'ios') return false
68
+
69
+ try {
70
+ return await NativeRNKontext.disposeSkAdNetwork()
71
+ } catch (e) {
72
+ console.error('[SKAdNetwork] Error disposing:', e)
73
+ return false
74
+ }
75
+ }