@kontextso/sdk-react-native 3.3.1-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/ios/RNKontext.mm CHANGED
@@ -56,6 +56,47 @@ RCT_EXPORT_MODULE()
56
56
  resolve([KontextSDK getTcfData]);
57
57
  }
58
58
 
59
+ - (void)getTrackingAuthorizationStatus:(RCTPromiseResolveBlock)resolve
60
+ reject:(RCTPromiseRejectBlock)reject {
61
+ resolve([KontextSDK getTrackingAuthorizationStatus]);
62
+ }
63
+
64
+ - (void)requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
65
+ reject:(RCTPromiseRejectBlock)reject {
66
+ [KontextSDK requestTrackingAuthorization:resolve rejecter:reject];
67
+ }
68
+
69
+ - (void)getAdvertisingId:(RCTPromiseResolveBlock)resolve
70
+ reject:(RCTPromiseRejectBlock)reject {
71
+ resolve([KontextSDK getAdvertisingId]);
72
+ }
73
+
74
+ - (void)getVendorId:(RCTPromiseResolveBlock)resolve
75
+ reject:(RCTPromiseRejectBlock)reject {
76
+ resolve([KontextSDK getVendorId]);
77
+ }
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
+
59
100
  #else
60
101
 
61
102
  RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)resolve
@@ -103,6 +144,47 @@ RCT_EXPORT_METHOD(getTcfData:(RCTPromiseResolveBlock)resolve
103
144
  resolve([KontextSDK getTcfData]);
104
145
  }
105
146
 
147
+ RCT_EXPORT_METHOD(getTrackingAuthorizationStatus:(RCTPromiseResolveBlock)resolve
148
+ rejecter:(RCTPromiseRejectBlock)reject) {
149
+ resolve([KontextSDK getTrackingAuthorizationStatus]);
150
+ }
151
+
152
+ RCT_EXPORT_METHOD(requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
153
+ rejecter:(RCTPromiseRejectBlock)reject) {
154
+ [KontextSDK requestTrackingAuthorization:resolve rejecter:reject];
155
+ }
156
+
157
+ RCT_EXPORT_METHOD(getAdvertisingId:(RCTPromiseResolveBlock)resolve
158
+ rejecter:(RCTPromiseRejectBlock)reject) {
159
+ resolve([KontextSDK getAdvertisingId]);
160
+ }
161
+
162
+ RCT_EXPORT_METHOD(getVendorId:(RCTPromiseResolveBlock)resolve
163
+ rejecter:(RCTPromiseRejectBlock)reject) {
164
+ resolve([KontextSDK getVendorId]);
165
+ }
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
+
106
188
  #endif
107
189
 
108
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
+ }
@@ -0,0 +1,46 @@
1
+ import Foundation
2
+ import AppTrackingTransparency
3
+ import UIKit
4
+
5
+ @available(iOS 14, *)
6
+ final class TrackingAuthorizationManager {
7
+ static let shared = TrackingAuthorizationManager()
8
+ private var observer: NSObjectProtocol?
9
+
10
+ deinit {
11
+ removeObserver()
12
+ }
13
+
14
+ func requestTrackingAuthorization(completion: @escaping (Int) -> Void) {
15
+ removeObserver()
16
+ ATTrackingManager.requestTrackingAuthorization { [weak self] status in
17
+ // Race condition: OS returned .denied but system status is still
18
+ // .notDetermined — the dialog couldn't show (app wasn't fully active).
19
+ // Wait for the next foreground activation and retry.
20
+ if status == .denied
21
+ && ATTrackingManager.trackingAuthorizationStatus == .notDetermined {
22
+ self?.addObserver(completion: completion)
23
+ return
24
+ }
25
+ completion(Int(status.rawValue))
26
+ }
27
+ }
28
+
29
+ private func addObserver(completion: @escaping (Int) -> Void) {
30
+ removeObserver()
31
+ observer = NotificationCenter.default.addObserver(
32
+ forName: UIApplication.didBecomeActiveNotification,
33
+ object: nil,
34
+ queue: .main
35
+ ) { [weak self] _ in
36
+ self?.requestTrackingAuthorization(completion: completion)
37
+ }
38
+ }
39
+
40
+ private func removeObserver() {
41
+ if let observer = observer {
42
+ NotificationCenter.default.removeObserver(observer)
43
+ self.observer = nil
44
+ }
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "3.3.1-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.5",
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.7"
57
+ "@kontextso/sdk-react": "^3.0.9"
58
58
  },
59
59
  "files": [
60
60
  "dist/*",
@@ -3,11 +3,31 @@ import { TurboModuleRegistry } from 'react-native'
3
3
 
4
4
  export interface Spec extends TurboModule {
5
5
  isSoundOn(): Promise<boolean>
6
+
7
+ // SKOverlay
6
8
  presentSKOverlay(appStoreId: string, position: string, dismissible: boolean): Promise<boolean>
7
9
  dismissSKOverlay(): Promise<boolean>
10
+
11
+ // SKStoreProduct
8
12
  presentSKStoreProduct(appStoreId: string): Promise<boolean>
9
13
  dismissSKStoreProduct(): Promise<boolean>
14
+
15
+ // TCF
10
16
  getTcfData(): Promise<{ gdprApplies: 0 | 1 | null; tcString: string | null }>
17
+
18
+ // ATT
19
+ getTrackingAuthorizationStatus(): Promise<number>
20
+ requestTrackingAuthorization(): Promise<number>
21
+
22
+ // IFA
23
+ getAdvertisingId(): Promise<string | null>
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>
11
31
  }
12
32
 
13
33
  export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
@@ -12,6 +12,8 @@ import { Appearance, Dimensions, PixelRatio, Platform } from 'react-native'
12
12
  import DeviceInfo, { type DeviceType } from 'react-native-device-info'
13
13
  import { version } from '../../package.json'
14
14
  import NativeRNKontext from '../NativeRNKontext'
15
+ import { requestTrackingAuthorization, resolveIds } from '../services/Att'
16
+ import { useEffect, useState } from 'react'
15
17
 
16
18
  ErrorUtils.setGlobalHandler((error, isFatal) => {
17
19
  if (!isFatal) {
@@ -142,5 +144,35 @@ const getTcf = async (): Promise<Pick<RegulatoryConfig, 'gdpr' | 'gdprConsent'>>
142
144
  }
143
145
 
144
146
  export const AdsProvider = (props: AdsProviderProps) => {
145
- return <AdsProviderInternal {...props} getDevice={getDevice} getSdk={getSdk} getApp={getApp} getTcf={getTcf} />
147
+ const [advertisingId, setAdvertisingId] = useState<string | null | undefined>()
148
+ const [vendorId, setVendorId] = useState<string | null | undefined>()
149
+
150
+ const initializeIfa = async () => {
151
+ try {
152
+ if (Platform.OS === 'ios') {
153
+ await requestTrackingAuthorization()
154
+ }
155
+ const ids = await resolveIds({
156
+ advertisingId: props.advertisingId ?? undefined,
157
+ vendorId: props.vendorId ?? undefined,
158
+ })
159
+ setAdvertisingId(ids.advertisingId)
160
+ setVendorId(ids.vendorId)
161
+ } catch (error) {
162
+ console.error(error)
163
+ }
164
+ }
165
+
166
+ useEffect(() => {
167
+ initializeIfa()
168
+ }, [])
169
+ return <AdsProviderInternal
170
+ {...props}
171
+ advertisingId={advertisingId}
172
+ vendorId={vendorId}
173
+ getDevice={getDevice}
174
+ getSdk={getSdk}
175
+ getApp={getApp}
176
+ getTcf={getTcf}
177
+ />
146
178
  }
@@ -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