@kontextso/sdk-react-native 3.4.0-rc.0 → 3.4.0-rc.2
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/src/newarch/java/so/kontext/react/RNKontextModule.kt +17 -0
- package/android/src/oldarch/java/so/kontext/react/RNKontextModule.kt +21 -0
- package/dist/index.js +1 -1
- package/ios/KontextSDK.swift +66 -0
- package/ios/RNKontext.mm +42 -0
- package/ios/SkAdNetworkManager.swift +257 -0
- package/package.json +4 -4
- package/src/NativeRNKontext.ts +6 -0
- package/src/formats/Format.tsx +62 -0
- package/src/services/SkAdNetwork.ts +75 -0
package/RNKontext.podspec
CHANGED
|
@@ -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: String, 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: String, 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 (
|
|
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
|
}
|
package/ios/KontextSDK.swift
CHANGED
|
@@ -100,6 +100,72 @@ public class KontextSDK: NSObject {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// MARK: - SKAdNetwork
|
|
104
|
+
|
|
105
|
+
@objc
|
|
106
|
+
public static func initSkAdNetworkImpression(
|
|
107
|
+
_ paramsJson: String,
|
|
108
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
109
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
110
|
+
) {
|
|
111
|
+
guard
|
|
112
|
+
let data = paramsJson.data(using: .utf8),
|
|
113
|
+
let params = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
114
|
+
else {
|
|
115
|
+
reject("SKAN_INVALID_PARAMS", "Failed to parse SKAdNetwork params JSON", nil)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
SKAdNetworkManager.shared.initImpression(params: params) { success, error in
|
|
119
|
+
if let error = error {
|
|
120
|
+
reject("SKAN_INIT_IMPRESSION_FAILED", error.localizedDescription, error)
|
|
121
|
+
} else {
|
|
122
|
+
resolve(success)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@objc
|
|
128
|
+
public static func startSkAdNetworkImpression(
|
|
129
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
130
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
131
|
+
) {
|
|
132
|
+
SKAdNetworkManager.shared.startImpression { success, error in
|
|
133
|
+
if let error = error {
|
|
134
|
+
reject("SKAN_START_IMPRESSION_FAILED", error.localizedDescription, error)
|
|
135
|
+
} else {
|
|
136
|
+
resolve(success)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@objc
|
|
142
|
+
public static func endSkAdNetworkImpression(
|
|
143
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
144
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
145
|
+
) {
|
|
146
|
+
SKAdNetworkManager.shared.endImpression { success, error in
|
|
147
|
+
if let error = error {
|
|
148
|
+
reject("SKAN_END_IMPRESSION_FAILED", error.localizedDescription, error)
|
|
149
|
+
} else {
|
|
150
|
+
resolve(success)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@objc
|
|
156
|
+
public static func disposeSkAdNetwork(
|
|
157
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
158
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
159
|
+
) {
|
|
160
|
+
SKAdNetworkManager.shared.dispose { success, error in
|
|
161
|
+
if let error = error {
|
|
162
|
+
reject("SKAN_DISPOSE_FAILED", error.localizedDescription, error)
|
|
163
|
+
} else {
|
|
164
|
+
resolve(success)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
103
169
|
// MARK: - TCF (minimal: GDPR applies + TC string)
|
|
104
170
|
|
|
105
171
|
/// 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:(NSString *)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:(NSString *)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.
|
|
3
|
+
"version": "3.4.0-rc.2",
|
|
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
|
@@ -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: string): 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')
|
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
|
|
@@ -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(JSON.stringify(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
|
+
}
|