@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/RNKontext.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/so/kontext/react/RNKontextModuleImpl.kt +34 -0
- package/android/src/newarch/java/so/kontext/react/RNKontextModule.kt +33 -0
- package/android/src/oldarch/java/so/kontext/react/RNKontextModule.kt +41 -0
- package/dist/index.js +119 -9
- package/dist/index.mjs +115 -5
- package/ios/KontextSDK.swift +106 -0
- package/ios/PrivacyInfo.xcprivacy +64 -0
- package/ios/RNKontext.mm +82 -0
- package/ios/SkAdNetworkManager.swift +257 -0
- package/ios/TrackingAuthorizationManager.swift +46 -0
- package/package.json +4 -4
- package/src/NativeRNKontext.ts +20 -0
- package/src/context/AdsProvider.tsx +33 -1
- package/src/formats/Format.tsx +62 -0
- package/src/services/Att.ts +117 -0
- package/src/services/SkAdNetwork.ts +75 -0
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
|
+
"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.
|
|
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": "
|
|
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.
|
|
57
|
+
"@kontextso/sdk-react": "^3.0.9"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"dist/*",
|
package/src/NativeRNKontext.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/formats/Format.tsx
CHANGED
|
@@ -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
|