@advenue/react-native 0.8.0 → 1.0.0
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/README.md +8 -7
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +118 -347
- package/android/src/main/kotlin/io/advenue/Advenue.kt +549 -0
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +114 -0
- package/android/src/main/kotlin/io/advenue/core/Backoff.kt +28 -0
- package/android/src/main/kotlin/io/advenue/core/ClientEvent.kt +136 -0
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +186 -0
- package/android/src/main/kotlin/io/advenue/core/Consent.kt +52 -0
- package/android/src/main/kotlin/io/advenue/core/Contracts.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/Conversion.kt +69 -0
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +414 -0
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +89 -0
- package/android/src/main/kotlin/io/advenue/core/HmacSigner.kt +31 -0
- package/android/src/main/kotlin/io/advenue/core/InstallReferrer.kt +146 -0
- package/android/src/main/kotlin/io/advenue/core/Json.kt +272 -0
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +47 -0
- package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +189 -0
- package/android/src/main/kotlin/io/advenue/core/SystemServices.kt +58 -0
- package/android/src/main/kotlin/io/advenue/core/Tcf.kt +48 -0
- package/android/src/main/kotlin/io/advenue/core/Time.kt +56 -0
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +149 -0
- package/android/src/main/kotlin/io/advenue/platform/CompositeStore.kt +33 -0
- package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +68 -0
- package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +90 -0
- package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +101 -0
- package/android/src/main/kotlin/io/advenue/platform/Identity.kt +79 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +197 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +91 -0
- package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +72 -0
- package/android/src/main/kotlin/io/advenue/platform/PreferencesStore.kt +32 -0
- package/android/src/main/kotlin/io/advenue/plugin/Contracts.kt +67 -0
- package/android/src/main/kotlin/io/advenue/plugin/FirebaseAppInstanceIdSource.kt +73 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayAdvertisingIdSource.kt +47 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayInstallReferrerSource.kt +85 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayIntegritySource.kt +86 -0
- package/android/src/main/kotlin/io/advenue/plugin/PluginRegistry.kt +61 -0
- package/dist/index.cjs +183 -655
- package/dist/index.d.cts +199 -331
- package/dist/index.d.ts +199 -331
- package/dist/index.js +182 -661
- package/ios/AdvenueIosModule.swift +144 -432
- package/ios/vendor/Advenue/Advenue.swift +596 -0
- package/ios/vendor/Advenue/AdvenueConfig.swift +77 -0
- package/ios/vendor/AdvenueCore/AdvenueValue.swift +103 -0
- package/ios/vendor/AdvenueCore/Attestation.swift +52 -0
- package/ios/vendor/AdvenueCore/Backoff.swift +28 -0
- package/ios/vendor/AdvenueCore/ClientEvent.swift +154 -0
- package/ios/vendor/AdvenueCore/Consent.swift +40 -0
- package/ios/vendor/AdvenueCore/Contracts.swift +59 -0
- package/ios/vendor/AdvenueCore/Conversion.swift +74 -0
- package/ios/vendor/AdvenueCore/ConversionValue.swift +217 -0
- package/ios/vendor/AdvenueCore/Engine.swift +587 -0
- package/ios/vendor/AdvenueCore/EventQueue.swift +89 -0
- package/ios/vendor/AdvenueCore/Limits.swift +41 -0
- package/ios/vendor/AdvenueCore/PIIScrub.swift +102 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +139 -0
- package/ios/vendor/AdvenueCore/SkanConfig.swift +76 -0
- package/ios/vendor/AdvenueCore/SkanReporter.swift +25 -0
- package/ios/vendor/AdvenueCore/SkanState.swift +258 -0
- package/ios/vendor/AdvenueCore/Tcf.swift +48 -0
- package/ios/vendor/AdvenueCore/Transport.swift +14 -0
- package/ios/vendor/AdvenueFirebase/FirebaseAppInstanceId.swift +33 -0
- package/ios/vendor/AdvenuePlatform/AdvertisingIdentity.swift +87 -0
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +42 -0
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +63 -0
- package/ios/vendor/AdvenuePlatform/CryptoKitSigner.swift +19 -0
- package/ios/vendor/AdvenuePlatform/DeviceCheckAttestation.swift +91 -0
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +65 -0
- package/ios/vendor/AdvenuePlatform/ForegroundTracker.swift +55 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +96 -0
- package/ios/vendor/AdvenuePlatform/Identity.swift +52 -0
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +143 -0
- package/ios/vendor/AdvenuePlatform/KeychainStore.swift +95 -0
- package/ios/vendor/AdvenuePlatform/SearchAdsToken.swift +76 -0
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +70 -0
- package/ios/vendor/AdvenuePlatform/StoreKitSkanReporter.swift +140 -0
- package/ios/vendor/AdvenuePlatform/SystemServices.swift +55 -0
- package/ios/vendor/AdvenuePlatform/TcfReader.swift +18 -0
- package/ios/vendor/AdvenuePlatform/UserDefaultsStore.swift +26 -0
- package/package.json +9 -11
- package/scripts/check-dist.mjs +17 -0
- package/scripts/check-vendored-swift.mjs +148 -0
- package/scripts/vendor-natives.mjs +149 -0
- package/scripts/vendor-natives.test.mjs +112 -0
- package/src/deep-links.ts +19 -1
- package/src/index.ts +267 -803
- package/src/native-types.ts +74 -171
- package/src/native.ts +0 -23
- package/src/types.ts +81 -0
- package/src/aem.ts +0 -33
- package/src/mmkv-storage.ts +0 -21
- package/src/native-storage.ts +0 -63
- package/src/secure-store.ts +0 -26
|
@@ -1,104 +1,144 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
|
-
import StoreKit
|
|
3
|
-
import AdServices
|
|
4
|
-
import AdSupport
|
|
5
1
|
import AppTrackingTransparency
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
// No `import Advenue` / `import AdvenueCore` here, and that is not an omission.
|
|
6
|
+
// The podspec globs this file and the vendored SDK sources into ONE pod target,
|
|
7
|
+
// so those module names resolve to nothing — importing them is the error that
|
|
8
|
+
// stopped this package building at all. Every symbol below is already in scope
|
|
9
|
+
// for the same reason. `check-vendored-swift.mjs` keeps it that way.
|
|
10
|
+
|
|
11
|
+
/// The React Native bridge to the Advenue iOS SDK.
|
|
12
|
+
///
|
|
13
|
+
/// It used to be a toolbox: one function per native capability — storage,
|
|
14
|
+
/// advertising ids, App Attest, SKAdNetwork, TCF, device info — because a
|
|
15
|
+
/// TypeScript core drove them one at a time. That core is retired, and every
|
|
16
|
+
/// one of those functions had stopped being called. They are gone rather than
|
|
17
|
+
/// left in place, because dead code in an SDK is code someone later assumes is
|
|
18
|
+
/// live, and one of them was warming a second Play Integrity provider on the
|
|
19
|
+
/// Android side for nobody.
|
|
20
|
+
///
|
|
21
|
+
/// What is left is the bridge and one capability. `requestTrackingAuthorization`
|
|
22
|
+
/// stays a capability because the app decides WHEN to prompt, and that decision
|
|
23
|
+
/// belongs to the app's own UX rather than to the SDK's lifecycle.
|
|
17
24
|
public class AdvenueIosModule: Module {
|
|
18
|
-
// UIKit (UIScreen/UIDevice/UIPasteboard) is main-thread-only, but synchronous
|
|
19
|
-
// `Function` handlers run on whatever thread invoked them (the JS thread on
|
|
20
|
-
// RN's bridge). Hop to main when needed; `sync` is safe because these are
|
|
21
|
-
// cheap local reads and the guard prevents a same-thread deadlock.
|
|
22
|
-
private static func onMainSync<T>(_ work: () -> T) -> T {
|
|
23
|
-
if Thread.isMainThread {
|
|
24
|
-
return work()
|
|
25
|
-
}
|
|
26
|
-
return DispatchQueue.main.sync(execute: work)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
public func definition() -> ModuleDefinition {
|
|
30
26
|
Name("AdvenueIos")
|
|
31
27
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
28
|
+
// Diagnostics travel back to JavaScript. `onError` is a JS closure and
|
|
29
|
+
// cannot cross the channel, so what crosses is the event: a stable context
|
|
30
|
+
// identifier and a message, never PII. Without this the SDK's own swallowed
|
|
31
|
+
// failures — deferred identity, a rejected event, a transport error — reach
|
|
32
|
+
// nobody, which is what happened between the inversion and this commit.
|
|
33
|
+
Events("onAdvenueError")
|
|
34
|
+
|
|
35
|
+
Function("bridgeInitialize") { (options: [String: Any]) in
|
|
36
|
+
guard let app = options["apiKey"] as? String else { return }
|
|
37
|
+
var config = AdvenueConfig(
|
|
38
|
+
apiKey: app,
|
|
39
|
+
endpoint: (options["endpoint"] as? String) ?? DEFAULT_ENDPOINT,
|
|
40
|
+
appVersion: options["appVersion"] as? String,
|
|
41
|
+
requireConsent: (options["requireConsent"] as? Bool) ?? false,
|
|
42
|
+
signingSecret: options["signingSecret"] as? String,
|
|
43
|
+
onError: { [weak self] context, error in
|
|
44
|
+
self?.sendEvent("onAdvenueError", ["context": context, "message": "\(error)"])
|
|
45
|
+
},
|
|
46
|
+
// The wrapper's version, not this SDK's: a React Native release pins
|
|
47
|
+
// the native snapshot inside it, so its version identifies both.
|
|
48
|
+
sdkVersion: options["sdkVersion"] as? String)
|
|
49
|
+
if let flushInterval = options["flushIntervalMs"] as? Int {
|
|
50
|
+
config.flushIntervalMs = flushInterval
|
|
51
|
+
}
|
|
52
|
+
if let batchSize = options["batchSize"] as? Int { config.batchSize = batchSize }
|
|
53
|
+
Advenue.initialize(config)
|
|
54
|
+
// Firebase App Instance ID, when the host app already ships Firebase.
|
|
55
|
+
// `AdvenueFirebase` guards its import with `#if canImport`, which is
|
|
56
|
+
// false under SPM unless declared but true under CocoaPods, where the
|
|
57
|
+
// host app's pods are linked together — so this is safe here and would
|
|
58
|
+
// not be in a Swift Package. The Android side gets the same automatic
|
|
59
|
+
// detection from `plugin-firebase-reflect`.
|
|
60
|
+
AdvenueFirebase.register()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Function("bridgeTrack") { (name: String, properties: [String: Any]?) in
|
|
64
|
+
Advenue.track(name, properties: properties.map(Self.toAdvenueValues))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Function("bridgeFlush") { Advenue.flush() }
|
|
68
|
+
Function("bridgeSetUserId") { (id: String?) in Advenue.setUserId(id) }
|
|
69
|
+
Function("bridgeSetConsent") { (granted: Bool) in Advenue.setConsent(granted) }
|
|
70
|
+
Function("bridgeForgetMe") { Advenue.forgetMe() }
|
|
71
|
+
Function("bridgeShutdown") { Advenue.shutdown() }
|
|
72
|
+
Function("bridgeResolvedAppId") { () -> String? in Advenue.resolvedAppId() }
|
|
73
|
+
|
|
74
|
+
Function("bridgeSetConsentData") { (data: [String: Any]) in
|
|
75
|
+
guard let gdpr = data["isUserSubjectToGDPR"] as? Bool else { return }
|
|
76
|
+
Advenue.setConsentData(
|
|
77
|
+
Consent(
|
|
78
|
+
isUserSubjectToGDPR: gdpr,
|
|
79
|
+
hasConsentForDataUsage: data["hasConsentForDataUsage"] as? Bool,
|
|
80
|
+
hasConsentForAdsPersonalization: data["hasConsentForAdsPersonalization"] as? Bool,
|
|
81
|
+
hasConsentForAdStorage: data["hasConsentForAdStorage"] as? Bool))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Read back, for the JavaScript consent cache. The cache is seeded from
|
|
85
|
+
// this at initialize: consent is persisted and survives restarts, so a
|
|
86
|
+
// facade defaulting to false on every cold start would silently discard a
|
|
87
|
+
// preference the user gave.
|
|
88
|
+
Function("bridgeGetConsent") { () -> Bool in Advenue.trackingConsent() }
|
|
89
|
+
|
|
90
|
+
Function("bridgeGetConsentData") { () -> [String: Any]? in
|
|
91
|
+
guard let consent = Advenue.consentData() else { return nil }
|
|
92
|
+
var out: [String: Any] = ["isUserSubjectToGDPR": consent.isUserSubjectToGDPR]
|
|
93
|
+
// Written only when stated: "not asked" is not "denied", and a false
|
|
94
|
+
// invented here is a recorded refusal that never happened.
|
|
95
|
+
if let v = consent.hasConsentForDataUsage { out["hasConsentForDataUsage"] = v }
|
|
96
|
+
if let v = consent.hasConsentForAdsPersonalization {
|
|
97
|
+
out["hasConsentForAdsPersonalization"] = v
|
|
98
|
+
}
|
|
99
|
+
if let v = consent.hasConsentForAdStorage { out["hasConsentForAdStorage"] = v }
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Function("bridgeSetAppInstanceId") { (id: String?) in Advenue.setAppInstanceId(id) }
|
|
104
|
+
|
|
105
|
+
Function("bridgeSetPushToken") { (token: String?, provider: String?) in
|
|
106
|
+
Advenue.setPushToken(token, provider: provider)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Function("bridgeProcessDeepLink") { (url: String) in
|
|
110
|
+
guard let parsed = URL(string: url) else { return }
|
|
111
|
+
Advenue.processDeepLink(parsed)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The amount arrives as the app's price string, not micros: converting it
|
|
115
|
+
// in JavaScript would put a money computation back in the layer this
|
|
116
|
+
// release is emptying. A malformed amount records nothing.
|
|
117
|
+
Function("bridgeRecordSkanRevenue") { (amount: String, currency: String) in
|
|
118
|
+
Advenue.recordSkanRevenue(amount: amount, currency: currency)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
AsyncFunction("bridgeDeviceId") { (promise: Promise) in
|
|
122
|
+
Task { promise.resolve(await Advenue.deviceId()) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
AsyncFunction("bridgeResolveDeferredDeepLink") { (promise: Promise) in
|
|
126
|
+
Task {
|
|
127
|
+
guard let link = await Advenue.resolveDeferredDeepLink() else {
|
|
128
|
+
promise.resolve(nil)
|
|
129
|
+
return
|
|
82
130
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
promise.resolve([
|
|
132
|
+
"deepLinkValue": link.deepLinkValue as Any,
|
|
133
|
+
"campaign": link.campaign as Any,
|
|
134
|
+
"network": link.network as Any,
|
|
135
|
+
"influencerId": link.influencerId as Any,
|
|
136
|
+
"isDeferred": link.isDeferred,
|
|
137
|
+
"isFirstLaunch": link.isFirstLaunch,
|
|
138
|
+
])
|
|
86
139
|
}
|
|
87
140
|
}
|
|
88
141
|
|
|
89
|
-
// Apple Search Ads attribution token; resolved server-side via @advenue/skadnetwork.
|
|
90
|
-
AsyncFunction("getAttributionToken") { (promise: Promise) in
|
|
91
|
-
if #available(iOS 14.3, *) {
|
|
92
|
-
do {
|
|
93
|
-
let token = try AAAttribution.attributionToken()
|
|
94
|
-
promise.resolve(token)
|
|
95
|
-
} catch {
|
|
96
|
-
promise.reject("EASA", error.localizedDescription)
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
promise.resolve(nil)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
142
|
|
|
103
143
|
// ATT consent prompt. The consumer app must declare
|
|
104
144
|
// NSUserTrackingUsageDescription in Info.plist, or iOS rejects the call.
|
|
@@ -111,262 +151,23 @@ public class AdvenueIosModule: Module {
|
|
|
111
151
|
promise.resolve("authorized")
|
|
112
152
|
}
|
|
113
153
|
}
|
|
114
|
-
|
|
115
|
-
AsyncFunction("getAdvertisingId") { (promise: Promise) in
|
|
116
|
-
if #available(iOS 14, *) {
|
|
117
|
-
guard ATTrackingManager.trackingAuthorizationStatus == .authorized else {
|
|
118
|
-
promise.resolve(nil)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
|
|
123
|
-
promise.resolve(idfa == "00000000-0000-0000-0000-000000000000" ? nil : idfa)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
Function("getTrackingAuthorizationStatus") { () -> String in
|
|
127
|
-
if #available(iOS 14, *) {
|
|
128
|
-
return Self.attStatusString(ATTrackingManager.trackingAuthorizationStatus)
|
|
129
|
-
}
|
|
130
|
-
return "authorized"
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Vendor-scoped device id (identifierForVendor). Available WITHOUT ATT because
|
|
134
|
-
// it is NOT an advertising identifier. First-party fraud/dedup use only — it
|
|
135
|
-
// must not be used for cross-network attribution, which would be "tracking".
|
|
136
|
-
Function("getIdentifierForVendor") { () -> String? in
|
|
137
|
-
return Self.onMainSync { UIDevice.current.identifierForVendor?.uuidString }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// G3.3 — DeviceCheck token. Calls DCDevice.current.generateToken to obtain
|
|
141
|
-
// the opaque base64 token that identifies this physical device to the Apple
|
|
142
|
-
// DeviceCheck API. The server uses it (with the per-app DeviceCheck key) to
|
|
143
|
-
// query/update the persistent 2-bit per-device flag. Unlike App Attest, this
|
|
144
|
-
// requires no challenge/server round-trip — just generate and attach.
|
|
145
|
-
//
|
|
146
|
-
// Rejects with EUNSUPPORTED when DCDevice.current.isSupported is false (iOS
|
|
147
|
-
// simulator, Catalyst, etc.) so the JS layer can fail-safe and omit the token.
|
|
148
|
-
// IMPORTANT: NOT compile/on-device verified here; reviewed against the
|
|
149
|
-
// DCDevice API. The JS fail-safe in sdk-react-native ensures missing token
|
|
150
|
-
// never blocks attribution.
|
|
151
|
-
AsyncFunction("getDeviceCheckToken") { (promise: Promise) in
|
|
152
|
-
guard DCDevice.current.isSupported else {
|
|
153
|
-
promise.reject("EUNSUPPORTED", "DCDevice is not supported on this device")
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
DCDevice.current.generateToken { tokenData, error in
|
|
157
|
-
if let error = error {
|
|
158
|
-
promise.reject("EDEVICECHECK", error.localizedDescription)
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
guard let tokenData = tokenData else {
|
|
162
|
-
promise.reject("EDEVICECHECK", "generateToken returned nil data")
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
promise.resolve(tokenData.base64EncodedString())
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// G3.2n — App Attest. Takes the server-supplied challenge string, computes
|
|
170
|
-
// clientDataHash = SHA-256(challenge UTF-8), generates (or reuses a
|
|
171
|
-
// keychain-cached) DCAppAttest key, and calls attestKey to obtain the
|
|
172
|
-
// CBOR attestation object. Resolves { keyId, attestationObject } where
|
|
173
|
-
// attestationObject is base64-encoded — pass directly to the server's
|
|
174
|
-
// verifyAppAttest endpoint. Rejects with a code on any failure so the JS
|
|
175
|
-
// layer can fail-safe (omit attestation rather than blocking the install).
|
|
176
|
-
// IMPORTANT: DCAppAttestService.isSupported is false on simulators; this
|
|
177
|
-
// causes the JS layer to skip attestation entirely (server sees 'unavailable').
|
|
178
|
-
AsyncFunction("attestKey") { (challenge: String, promise: Promise) in
|
|
179
|
-
guard #available(iOS 14.0, *) else {
|
|
180
|
-
promise.reject("EUNSUPPORTED", "App Attest requires iOS 14.0+")
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
let service = DCAppAttestService.shared
|
|
184
|
-
guard service.isSupported else {
|
|
185
|
-
promise.reject("EUNSUPPORTED", "DCAppAttestService is not supported on this device")
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Compute clientDataHash = SHA-256 of the UTF-8 challenge bytes.
|
|
190
|
-
guard let challengeData = challenge.data(using: .utf8) else {
|
|
191
|
-
promise.reject("EBADCHALLENGE", "Failed to encode challenge as UTF-8")
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
let clientDataHash = Data(SHA256.hash(data: challengeData))
|
|
195
|
-
|
|
196
|
-
// Load a previously generated key from Keychain, or generate a new one.
|
|
197
|
-
// Key is cached per-install/device so we only call generateKey once.
|
|
198
|
-
Self.resolveKeyId(service: service) { result in
|
|
199
|
-
switch result {
|
|
200
|
-
case .failure(let error):
|
|
201
|
-
promise.reject("EKEYID", error.localizedDescription)
|
|
202
|
-
case .success(let keyId):
|
|
203
|
-
service.attestKey(keyId, clientDataHash: clientDataHash) { attestationObject, error in
|
|
204
|
-
if let error = error {
|
|
205
|
-
// A key can be attested only once. If Apple rejects this keyId as
|
|
206
|
-
// invalid (e.g. it was already attested but the server never recorded
|
|
207
|
-
// it, or the key became invalid), the cached id is permanently
|
|
208
|
-
// unusable — drop it so the NEXT call generates a fresh key instead
|
|
209
|
-
// of looping on the dead id forever.
|
|
210
|
-
if (error as? DCError)?.code == .invalidKey {
|
|
211
|
-
Self.deleteKeyId()
|
|
212
|
-
}
|
|
213
|
-
promise.reject("EATTEST", error.localizedDescription)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
guard let attestationObject = attestationObject else {
|
|
217
|
-
promise.reject("EATTEST", "attestKey returned nil attestation object")
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
let base64Attest = attestationObject.base64EncodedString()
|
|
221
|
-
promise.resolve(["keyId": keyId, "attestationObject": base64Attest])
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Device metadata for Meta CAPI `extinfo` (Task 3n). Synchronous — every
|
|
228
|
-
// API used here is a cheap local read (no network I/O). Best-effort per
|
|
229
|
-
// field: a field is simply omitted when its source is unavailable, so the
|
|
230
|
-
// JS-side fail-safe (collectDeviceInfo) degrades gracefully rather than
|
|
231
|
-
// sending misleading placeholder values.
|
|
232
|
-
// IMPORTANT: NOT compile/on-device verified here; reviewed against the
|
|
233
|
-
// UIKit/Foundation APIs. The JS fail-safe in sdk-react-native ensures a
|
|
234
|
-
// missing/partial result never blocks attribution.
|
|
235
|
-
Function("getDeviceInfo") { () -> [String: Any] in
|
|
236
|
-
var info: [String: Any] = [:]
|
|
237
|
-
|
|
238
|
-
// Hardware model identifier (e.g. "iPhone16,1") via utsname, not the
|
|
239
|
-
// marketing name — matches the Android side's Build.MODEL granularity.
|
|
240
|
-
var systemInfo = utsname()
|
|
241
|
-
uname(&systemInfo)
|
|
242
|
-
let model = withUnsafePointer(to: &systemInfo.machine) {
|
|
243
|
-
$0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in
|
|
244
|
-
String(cString: ptr)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
info["model"] = model
|
|
248
|
-
info["locale"] = Locale.current.identifier
|
|
249
|
-
info["timezone"] = TimeZone.current.identifier
|
|
250
|
-
info["cpuCores"] = ProcessInfo.processInfo.activeProcessorCount
|
|
251
|
-
|
|
252
|
-
let (screenWidth, screenHeight, screenDensity) = Self.onMainSync { () -> (Int, Int, Double) in
|
|
253
|
-
let screen = UIScreen.main
|
|
254
|
-
return (
|
|
255
|
-
Int(screen.bounds.width * screen.scale),
|
|
256
|
-
Int(screen.bounds.height * screen.scale),
|
|
257
|
-
Double(screen.scale)
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
info["screenWidth"] = screenWidth
|
|
261
|
-
info["screenHeight"] = screenHeight
|
|
262
|
-
info["screenDensity"] = screenDensity
|
|
263
|
-
|
|
264
|
-
if let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
|
|
265
|
-
let gb = 1024.0 * 1024.0 * 1024.0
|
|
266
|
-
if let total = attrs[.systemSize] as? NSNumber {
|
|
267
|
-
info["totalStorageGb"] = total.doubleValue / gb
|
|
268
|
-
}
|
|
269
|
-
if let free = attrs[.systemFreeSize] as? NSNumber {
|
|
270
|
-
info["freeStorageGb"] = free.doubleValue / gb
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return info
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// MARK: - Built-in SDK storage (Adjust-style: the app supplies nothing)
|
|
278
|
-
|
|
279
|
-
// Sync key/value storage backing the SDK's offline buffer + state. A
|
|
280
|
-
// dedicated UserDefaults suite keeps SDK keys out of the app's defaults.
|
|
281
|
-
Function("storageGetItem") { (key: String) -> String? in
|
|
282
|
-
Self.sdkDefaults.string(forKey: key)
|
|
283
|
-
}
|
|
284
|
-
Function("storageSetItem") { (key: String, value: String) in
|
|
285
|
-
Self.sdkDefaults.set(value, forKey: key)
|
|
286
|
-
}
|
|
287
|
-
Function("storageRemoveItem") { (key: String) in
|
|
288
|
-
Self.sdkDefaults.removeObject(forKey: key)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// MARK: - DMA / TCF consent (read-only)
|
|
292
|
-
|
|
293
|
-
// IAB TCF v2 data a TCF-compliant CMP (e.g. Google UMP) writes to the
|
|
294
|
-
// app's *standard* UserDefaults — deliberately not our io.advenue.sdk
|
|
295
|
-
// suite. Raw values pass through; the TS side (tcfToConsent) owns the
|
|
296
|
-
// policy mapping. UserDefaults is thread-safe — no main-thread hop.
|
|
297
|
-
Function("getTcfData") { () -> [String: Any?]? in
|
|
298
|
-
let std = UserDefaults.standard
|
|
299
|
-
// object(forKey:), not integer(forKey:): a missing key must read as
|
|
300
|
-
// unknown, not 0 ("GDPR does not apply") — a defaulted 0 would strip
|
|
301
|
-
// consent handling from EEA traffic.
|
|
302
|
-
let gdprApplies = std.object(forKey: "IABTCF_gdprApplies") as? Int
|
|
303
|
-
let purposeConsents = std.string(forKey: "IABTCF_PurposeConsents")
|
|
304
|
-
if gdprApplies == nil && purposeConsents == nil { return nil }
|
|
305
|
-
return ["gdprApplies": gdprApplies, "purposeConsents": purposeConsents]
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Keychain-backed secure storage for the reinstall-durable identifiers
|
|
309
|
-
// (device id, install guard). kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
310
|
-
// survives app reinstall, readable after first unlock (background events),
|
|
311
|
-
// and never migrates via device-to-device backup restore — a restored
|
|
312
|
-
// backup must NOT clone another device's attribution identity.
|
|
313
|
-
Function("secureGetItem") { (key: String) -> String? in
|
|
314
|
-
Self.keychainGet(key)
|
|
315
|
-
}
|
|
316
|
-
Function("secureSetItem") { (key: String, value: String) in
|
|
317
|
-
Self.keychainSet(key, value)
|
|
318
|
-
}
|
|
319
|
-
Function("secureRemoveItem") { (key: String) in
|
|
320
|
-
Self.keychainRemove(key)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// MARK: - Built-in storage helpers
|
|
325
|
-
|
|
326
|
-
private static let sdkDefaults: UserDefaults =
|
|
327
|
-
UserDefaults(suiteName: "io.advenue.sdk") ?? .standard
|
|
328
|
-
|
|
329
|
-
// Shares `keychainService` with the App Attest helpers below — same service,
|
|
330
|
-
// distinct accounts (storage keys vs "app-attest-key-id"), so neither can
|
|
331
|
-
// read or delete the other's entries.
|
|
332
|
-
private static func keychainBaseQuery(_ key: String) -> [String: Any] {
|
|
333
|
-
[
|
|
334
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
335
|
-
kSecAttrService as String: keychainService,
|
|
336
|
-
kSecAttrAccount as String: key,
|
|
337
|
-
]
|
|
338
154
|
}
|
|
339
155
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
private static func keychainSet(_ key: String, _ value: String) {
|
|
353
|
-
guard let data = value.data(using: .utf8) else { return }
|
|
354
|
-
let base = keychainBaseQuery(key)
|
|
355
|
-
let update: [String: Any] = [kSecValueData as String: data]
|
|
356
|
-
let status = SecItemUpdate(base as CFDictionary, update as CFDictionary)
|
|
357
|
-
if status == errSecItemNotFound {
|
|
358
|
-
var add = base
|
|
359
|
-
add[kSecValueData as String] = data
|
|
360
|
-
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
361
|
-
SecItemAdd(add as CFDictionary, nil)
|
|
156
|
+
/// reaches the warehouse looking like a measurement.
|
|
157
|
+
private static func toAdvenueValues(_ raw: [String: Any]) -> [String: AdvenueValue] {
|
|
158
|
+
var out: [String: AdvenueValue] = [:]
|
|
159
|
+
for (key, value) in raw {
|
|
160
|
+
switch value {
|
|
161
|
+
case let v as String: out[key] = .string(v)
|
|
162
|
+
case let v as Bool: out[key] = .bool(v)
|
|
163
|
+
case let v as Int: out[key] = .int(v)
|
|
164
|
+
case let v as Double: out[key] = .double(v)
|
|
165
|
+
default: continue
|
|
166
|
+
}
|
|
362
167
|
}
|
|
168
|
+
return out
|
|
363
169
|
}
|
|
364
170
|
|
|
365
|
-
private static func keychainRemove(_ key: String) {
|
|
366
|
-
SecItemDelete(keychainBaseQuery(key) as CFDictionary)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
@available(iOS 14, *)
|
|
370
171
|
private static func attStatusString(_ status: ATTrackingManager.AuthorizationStatus) -> String {
|
|
371
172
|
switch status {
|
|
372
173
|
case .authorized: return "authorized"
|
|
@@ -375,93 +176,4 @@ public class AdvenueIosModule: Module {
|
|
|
375
176
|
default: return "notDetermined"
|
|
376
177
|
}
|
|
377
178
|
}
|
|
378
|
-
|
|
379
|
-
// MARK: - App Attest Keychain helpers
|
|
380
|
-
|
|
381
|
-
private static let keychainService = "io.advenue.sdk"
|
|
382
|
-
private static let keychainAccount = "app-attest-key-id"
|
|
383
|
-
|
|
384
|
-
/// Loads the persisted App Attest keyId from the Keychain. Returns nil when
|
|
385
|
-
/// no key has been generated yet; the caller must then call generateKey and
|
|
386
|
-
/// persist the result via saveKeyId(_:).
|
|
387
|
-
@available(iOS 14.0, *)
|
|
388
|
-
private static func loadKeyId() -> String? {
|
|
389
|
-
let query: [CFString: Any] = [
|
|
390
|
-
kSecClass: kSecClassGenericPassword,
|
|
391
|
-
kSecAttrService: keychainService,
|
|
392
|
-
kSecAttrAccount: keychainAccount,
|
|
393
|
-
kSecReturnData: true,
|
|
394
|
-
kSecMatchLimit: kSecMatchLimitOne,
|
|
395
|
-
]
|
|
396
|
-
var item: CFTypeRef?
|
|
397
|
-
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
398
|
-
guard status == errSecSuccess, let data = item as? Data else { return nil }
|
|
399
|
-
return String(data: data, encoding: .utf8)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/// Persists the App Attest keyId to the Keychain, replacing any prior value.
|
|
403
|
-
@available(iOS 14.0, *)
|
|
404
|
-
private static func saveKeyId(_ keyId: String) {
|
|
405
|
-
guard let data = keyId.data(using: .utf8) else { return }
|
|
406
|
-
// Try to update an existing item first; fall back to adding a new one.
|
|
407
|
-
let query: [CFString: Any] = [
|
|
408
|
-
kSecClass: kSecClassGenericPassword,
|
|
409
|
-
kSecAttrService: keychainService,
|
|
410
|
-
kSecAttrAccount: keychainAccount,
|
|
411
|
-
]
|
|
412
|
-
let attributes: [CFString: Any] = [kSecValueData: data]
|
|
413
|
-
var status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
414
|
-
if status == errSecItemNotFound {
|
|
415
|
-
var addQuery = query
|
|
416
|
-
addQuery[kSecValueData] = data
|
|
417
|
-
status = SecItemAdd(addQuery as CFDictionary, nil)
|
|
418
|
-
}
|
|
419
|
-
if status != errSecSuccess {
|
|
420
|
-
NSLog("[Advenue] Keychain write for App Attest keyId failed: %d", status)
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/// Removes the persisted App Attest keyId so the next resolveKeyId generates a
|
|
425
|
-
/// fresh key. Called when Apple reports the cached key as invalid.
|
|
426
|
-
@available(iOS 14.0, *)
|
|
427
|
-
private static func deleteKeyId() {
|
|
428
|
-
let query: [CFString: Any] = [
|
|
429
|
-
kSecClass: kSecClassGenericPassword,
|
|
430
|
-
kSecAttrService: keychainService,
|
|
431
|
-
kSecAttrAccount: keychainAccount,
|
|
432
|
-
]
|
|
433
|
-
let status = SecItemDelete(query as CFDictionary)
|
|
434
|
-
if status != errSecSuccess && status != errSecItemNotFound {
|
|
435
|
-
NSLog("[Advenue] Keychain delete for App Attest keyId failed: %d", status)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/// Resolves a DCAppAttest keyId: loads from Keychain if present, otherwise
|
|
440
|
-
/// generates a new one via DCAppAttestService.generateKey and persists it.
|
|
441
|
-
@available(iOS 14.0, *)
|
|
442
|
-
private static func resolveKeyId(
|
|
443
|
-
service: DCAppAttestService,
|
|
444
|
-
completion: @escaping (Result<String, Error>) -> Void
|
|
445
|
-
) {
|
|
446
|
-
if let cached = loadKeyId() {
|
|
447
|
-
completion(.success(cached))
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
service.generateKey { keyId, error in
|
|
451
|
-
if let error = error {
|
|
452
|
-
completion(.failure(error))
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
guard let keyId = keyId else {
|
|
456
|
-
completion(.failure(NSError(
|
|
457
|
-
domain: "io.advenue.appattest",
|
|
458
|
-
code: -1,
|
|
459
|
-
userInfo: [NSLocalizedDescriptionKey: "generateKey returned nil keyId"]
|
|
460
|
-
)))
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
saveKeyId(keyId)
|
|
464
|
-
completion(.success(keyId))
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
179
|
}
|