@advenue/react-native 0.9.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.
Files changed (94) hide show
  1. package/README.md +8 -7
  2. package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +118 -368
  3. package/android/src/main/kotlin/io/advenue/Advenue.kt +549 -0
  4. package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +114 -0
  5. package/android/src/main/kotlin/io/advenue/core/Backoff.kt +28 -0
  6. package/android/src/main/kotlin/io/advenue/core/ClientEvent.kt +136 -0
  7. package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +186 -0
  8. package/android/src/main/kotlin/io/advenue/core/Consent.kt +52 -0
  9. package/android/src/main/kotlin/io/advenue/core/Contracts.kt +80 -0
  10. package/android/src/main/kotlin/io/advenue/core/Conversion.kt +69 -0
  11. package/android/src/main/kotlin/io/advenue/core/Engine.kt +414 -0
  12. package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +89 -0
  13. package/android/src/main/kotlin/io/advenue/core/HmacSigner.kt +31 -0
  14. package/android/src/main/kotlin/io/advenue/core/InstallReferrer.kt +146 -0
  15. package/android/src/main/kotlin/io/advenue/core/Json.kt +272 -0
  16. package/android/src/main/kotlin/io/advenue/core/Limits.kt +47 -0
  17. package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -0
  18. package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +189 -0
  19. package/android/src/main/kotlin/io/advenue/core/SystemServices.kt +58 -0
  20. package/android/src/main/kotlin/io/advenue/core/Tcf.kt +48 -0
  21. package/android/src/main/kotlin/io/advenue/core/Time.kt +56 -0
  22. package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +149 -0
  23. package/android/src/main/kotlin/io/advenue/platform/CompositeStore.kt +33 -0
  24. package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +68 -0
  25. package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +90 -0
  26. package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +101 -0
  27. package/android/src/main/kotlin/io/advenue/platform/Identity.kt +79 -0
  28. package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +197 -0
  29. package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +91 -0
  30. package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +72 -0
  31. package/android/src/main/kotlin/io/advenue/platform/PreferencesStore.kt +32 -0
  32. package/android/src/main/kotlin/io/advenue/plugin/Contracts.kt +67 -0
  33. package/android/src/main/kotlin/io/advenue/plugin/FirebaseAppInstanceIdSource.kt +73 -0
  34. package/android/src/main/kotlin/io/advenue/plugin/PlayAdvertisingIdSource.kt +47 -0
  35. package/android/src/main/kotlin/io/advenue/plugin/PlayInstallReferrerSource.kt +85 -0
  36. package/android/src/main/kotlin/io/advenue/plugin/PlayIntegritySource.kt +86 -0
  37. package/android/src/main/kotlin/io/advenue/plugin/PluginRegistry.kt +61 -0
  38. package/dist/index.cjs +183 -675
  39. package/dist/index.d.cts +199 -336
  40. package/dist/index.d.ts +199 -336
  41. package/dist/index.js +182 -681
  42. package/ios/AdvenueIosModule.swift +144 -446
  43. package/ios/vendor/Advenue/Advenue.swift +596 -0
  44. package/ios/vendor/Advenue/AdvenueConfig.swift +77 -0
  45. package/ios/vendor/AdvenueCore/AdvenueValue.swift +103 -0
  46. package/ios/vendor/AdvenueCore/Attestation.swift +52 -0
  47. package/ios/vendor/AdvenueCore/Backoff.swift +28 -0
  48. package/ios/vendor/AdvenueCore/ClientEvent.swift +154 -0
  49. package/ios/vendor/AdvenueCore/Consent.swift +40 -0
  50. package/ios/vendor/AdvenueCore/Contracts.swift +59 -0
  51. package/ios/vendor/AdvenueCore/Conversion.swift +74 -0
  52. package/ios/vendor/AdvenueCore/ConversionValue.swift +217 -0
  53. package/ios/vendor/AdvenueCore/Engine.swift +587 -0
  54. package/ios/vendor/AdvenueCore/EventQueue.swift +89 -0
  55. package/ios/vendor/AdvenueCore/Limits.swift +41 -0
  56. package/ios/vendor/AdvenueCore/PIIScrub.swift +102 -0
  57. package/ios/vendor/AdvenueCore/SessionTracker.swift +139 -0
  58. package/ios/vendor/AdvenueCore/SkanConfig.swift +76 -0
  59. package/ios/vendor/AdvenueCore/SkanReporter.swift +25 -0
  60. package/ios/vendor/AdvenueCore/SkanState.swift +258 -0
  61. package/ios/vendor/AdvenueCore/Tcf.swift +48 -0
  62. package/ios/vendor/AdvenueCore/Transport.swift +14 -0
  63. package/ios/vendor/AdvenueFirebase/FirebaseAppInstanceId.swift +33 -0
  64. package/ios/vendor/AdvenuePlatform/AdvertisingIdentity.swift +87 -0
  65. package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +42 -0
  66. package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +63 -0
  67. package/ios/vendor/AdvenuePlatform/CryptoKitSigner.swift +19 -0
  68. package/ios/vendor/AdvenuePlatform/DeviceCheckAttestation.swift +91 -0
  69. package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +65 -0
  70. package/ios/vendor/AdvenuePlatform/ForegroundTracker.swift +55 -0
  71. package/ios/vendor/AdvenuePlatform/HttpTransport.swift +96 -0
  72. package/ios/vendor/AdvenuePlatform/Identity.swift +52 -0
  73. package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +143 -0
  74. package/ios/vendor/AdvenuePlatform/KeychainStore.swift +95 -0
  75. package/ios/vendor/AdvenuePlatform/SearchAdsToken.swift +76 -0
  76. package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +70 -0
  77. package/ios/vendor/AdvenuePlatform/StoreKitSkanReporter.swift +140 -0
  78. package/ios/vendor/AdvenuePlatform/SystemServices.swift +55 -0
  79. package/ios/vendor/AdvenuePlatform/TcfReader.swift +18 -0
  80. package/ios/vendor/AdvenuePlatform/UserDefaultsStore.swift +26 -0
  81. package/package.json +9 -11
  82. package/scripts/check-dist.mjs +17 -0
  83. package/scripts/check-vendored-swift.mjs +148 -0
  84. package/scripts/vendor-natives.mjs +149 -0
  85. package/scripts/vendor-natives.test.mjs +112 -0
  86. package/src/deep-links.ts +19 -1
  87. package/src/index.ts +266 -830
  88. package/src/native-types.ts +74 -181
  89. package/src/native.ts +0 -23
  90. package/src/types.ts +81 -0
  91. package/src/aem.ts +0 -33
  92. package/src/mmkv-storage.ts +0 -21
  93. package/src/native-storage.ts +0 -63
  94. package/src/secure-store.ts +0 -26
package/README.md CHANGED
@@ -8,8 +8,7 @@ engine.
8
8
  ## Install
9
9
 
10
10
  ```sh
11
- npx expo install @advenue/react-native \
12
- react-native-mmkv expo-secure-store
11
+ npx expo install @advenue/react-native
13
12
  ```
14
13
 
15
14
  The iOS and Android Expo native modules ship inside this package (`ios/`, `android/`) and are built by
@@ -19,20 +18,22 @@ your app's own Gradle/Xcode build — no manual linking.
19
18
 
20
19
  ```ts
21
20
  import { Advenue } from '@advenue/react-native';
22
- import { MMKV } from 'react-native-mmkv';
23
- import * as SecureStore from 'expo-secure-store';
24
21
 
25
22
  await Advenue.initialize({
26
23
  apiKey: 'apk_live_...', // from your Advenue dashboard
27
- platform: 'ios', // or 'android'
28
- mmkv: new MMKV(), // offline event buffering
29
- secureStore: SecureStore, // durable device identity
30
24
  // endpoint: 'https://ingest.your-domain.com', // self-hosted only
31
25
  });
32
26
 
33
27
  Advenue.track('signup_completed', { plan: 'pro' });
34
28
  ```
35
29
 
30
+ Storage and platform detection are built in. `track()` properties never become
31
+ financial revenue; connect a verified S2S store source for revenue/LTV/ROAS.
32
+
33
+ On iOS, the SDK fetches the versioned SKAN config and persists monotonic state.
34
+ Use `Advenue.recordSkanRevenue({ amount: '9.99', currency: 'USD' })` only to
35
+ advance Apple's local conversion value. It does not record Advenue revenue.
36
+
36
37
  > The `adv_` event-name prefix (e.g. `adv_meta_aem`) is reserved for
37
38
  > Advenue-internal signals — don't name your own custom events with it. Such
38
39
  > events are still accepted by `track()` (the pipeline relies on them) but are
@@ -1,398 +1,148 @@
1
1
  package expo.modules.advenue
2
2
 
3
- import android.content.pm.ApplicationInfo
4
- import com.android.installreferrer.api.InstallReferrerClient
5
- import com.android.installreferrer.api.InstallReferrerStateListener
6
- import com.google.android.gms.ads.identifier.AdvertisingIdClient
7
- import com.google.android.gms.appset.AppSet
8
- import com.google.android.play.core.integrity.IntegrityManagerFactory
9
- import com.google.android.play.core.integrity.StandardIntegrityManager
10
- import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
11
- import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
3
+ import expo.modules.kotlin.Promise
12
4
  import expo.modules.kotlin.modules.Module
13
5
  import expo.modules.kotlin.modules.ModuleDefinition
14
- import expo.modules.kotlin.Promise
15
-
16
- // Advenue Android native module. Fetches the Google Play Install Referrer; the
17
- // referrer string is parsed in TS (@advenue/install-referrer).
6
+ import io.advenue.Advenue
7
+ import io.advenue.AdvenueConfig
8
+ import io.advenue.platform.DEFAULT_ENDPOINT
9
+
10
+ /**
11
+ * The React Native bridge to the Advenue Android SDK.
12
+ *
13
+ * It used to be a toolbox: the Play Install Referrer, the Meta install
14
+ * referrer, the GAID, the App Set ID, Play Integrity, the SSAID, TCF, device
15
+ * info and two storage backends, each its own function, because a TypeScript
16
+ * core drove them one at a time. That core is retired and none of them had a
17
+ * caller left.
18
+ *
19
+ * They are deleted rather than left in place. One of them was actively harmful:
20
+ * an `OnCreate` block warmed a Play Integrity token provider for nobody, and
21
+ * the SDK's own `PlayIntegritySource` warms another — two `prepareIntegrityToken`
22
+ * calls per launch, against a quota, for one token. The rest were merely dead,
23
+ * which in an SDK means code someone later assumes is live.
24
+ *
25
+ * Everything they did, the vendored SDK now does through its own plugins.
26
+ */
18
27
  class AdvenueAndroidModule : Module() {
19
- // Standard Integrity token provider, warmed once at module init. Null when
20
- // Play Integrity is unavailable (side-loaded APK, emulator, no Play Services).
21
- // Warming is best-effort — a failure leaves this null and getIntegrityToken
22
- // will reject with a clear error.
23
- private var integrityProvider: StandardIntegrityManager.StandardIntegrityTokenProvider? = null
24
-
25
28
  override fun definition() = ModuleDefinition {
26
29
  Name("AdvenueAndroid")
27
30
 
28
- // Warm the Standard Integrity token provider when the module is created.
29
- // The Google Cloud project number is read from the application's <meta-data>
30
- // tag in AndroidManifest.xml:
31
- //
32
- // <meta-data
33
- // android:name="io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER"
34
- // android:value="<your-GCP-project-number>" />
35
- //
36
- // This is the numeric project ID shown in the Google Cloud Console URL:
37
- // https://console.cloud.google.com/home/dashboard?project=<project-id>
38
- // The number itself lives in Settings → Project info → Project number.
39
- OnCreate {
40
- val ctx = appContext.reactContext?.applicationContext ?: return@OnCreate
41
- try {
42
- val ai: ApplicationInfo = ctx.packageManager.getApplicationInfo(
43
- ctx.packageName,
44
- android.content.pm.PackageManager.GET_META_DATA,
45
- )
46
- // GCP project numbers are large longs (12+ digits). Android meta-data
47
- // stores them as String when declared as android:value="123..." (no type
48
- // suffix). We try getLong() first (works when stored as an integer
49
- // resource), then fall back to getString() + toLong() for string values.
50
- val metaBundle = ai.metaData
51
- val cloudProjectNumber: Long = metaBundle?.getLong(
52
- "io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER", 0L
53
- )?.takeIf { it != 0L }
54
- ?: metaBundle?.getString("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER")
55
- ?.toLongOrNull()
56
- ?: 0L
57
- if (cloudProjectNumber == 0L) {
58
- // Project number not configured — attestation will be unavailable.
59
- // Callers can still call getIntegrityToken(); it will reject with ECONFIG.
60
- return@OnCreate
61
- }
62
- val manager = IntegrityManagerFactory.createStandard(ctx)
63
- manager.prepareIntegrityToken(
64
- PrepareIntegrityTokenRequest.builder()
65
- .setCloudProjectNumber(cloudProjectNumber)
66
- .build()
67
- )
68
- .addOnSuccessListener { provider -> integrityProvider = provider }
69
- .addOnFailureListener { /* provider stays null — getIntegrityToken will reject */ }
70
- } catch (_: Exception) {
71
- // Play Integrity unavailable (no Play Services, side-loaded build, etc.)
72
- }
73
- }
74
-
75
- AsyncFunction("getInstallReferrer") { promise: Promise ->
76
- val context = appContext.reactContext ?: run {
77
- promise.reject("ECTX", "No React context", null)
78
- return@AsyncFunction
79
- }
80
- val client = InstallReferrerClient.newBuilder(context).build()
81
- // The Play Install Referrer callback can fire setup-finished, disconnect, or
82
- // never respond (offline / unavailable). Settle the promise EXACTLY once and
83
- // release the service binding on every exit path — otherwise a disconnect or
84
- // a hung connection leaks the binding and hangs trackInstall forever.
85
- val settled = java.util.concurrent.atomic.AtomicBoolean(false)
86
- val handler = android.os.Handler(android.os.Looper.getMainLooper())
87
- fun release() {
88
- try { client.endConnection() } catch (_: Exception) {}
89
- }
90
- // Watchdog: if the service never calls back, resolve null (referrer is
91
- // best-effort enrichment) so the JS promise always settles.
92
- handler.postDelayed({
93
- if (settled.compareAndSet(false, true)) {
94
- release()
95
- promise.resolve(null)
96
- }
97
- }, 5_000)
98
-
99
- client.startConnection(object : InstallReferrerStateListener {
100
- override fun onInstallReferrerSetupFinished(responseCode: Int) {
101
- if (!settled.compareAndSet(false, true)) return
102
- handler.removeCallbacksAndMessages(null)
103
- try {
104
- if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
105
- val response = client.installReferrer
106
- promise.resolve(mapOf(
107
- "referrer" to response.installReferrer,
108
- "clickTimestamp" to response.referrerClickTimestampSeconds,
109
- "installTimestamp" to response.installBeginTimestampSeconds,
110
- // Server-clock getters (G1): Play returns 0 when the install is
111
- // unverified. The JS layer forwards these as server-trusted keys
112
- // only when non-zero. Do NOT confuse with the device-clock
113
- // *TimestampSeconds getters above.
114
- "clickServerTimestamp" to response.referrerClickTimestampServerSeconds,
115
- "installServerTimestamp" to response.installBeginTimestampServerSeconds,
116
- "googlePlayInstant" to response.googlePlayInstantParam,
117
- ))
118
- } else {
119
- promise.resolve(null)
120
- }
121
- } catch (e: Exception) {
122
- promise.reject("EREFERRER", e.message, e)
123
- } finally {
124
- release()
125
- }
126
- }
127
-
128
- override fun onInstallReferrerServiceDisconnected() {
129
- // Service dropped before setup finished (e.g. low memory). Release the
130
- // binding and settle as best-effort null instead of hanging forever.
131
- if (settled.compareAndSet(false, true)) {
132
- handler.removeCallbacksAndMessages(null)
133
- release()
134
- promise.resolve(null)
135
- }
136
- }
137
- })
138
- }
139
-
140
- // Meta (Facebook/Instagram) install referrer via content provider. The
141
- // payload is parsed in TS and the encrypted blob decrypted server-side
142
- // (@advenue/install-referrer). Resolves null when no Meta app is installed.
143
- AsyncFunction("getMetaInstallReferrer") { promise: Promise ->
144
- val context = appContext.reactContext ?: run {
145
- promise.reject("ECTX", "No React context", null)
146
- return@AsyncFunction
147
- }
148
- val providers = listOf("com.facebook.katana", "com.instagram.android", "com.facebook.lite")
149
- try {
150
- for (pkg in providers) {
151
- val uri = android.net.Uri.parse(
152
- "content://$pkg.provider.InstallReferrerProvider/${context.packageName}"
153
- )
154
- val cursor = try {
155
- context.contentResolver.query(
156
- uri,
157
- arrayOf("install_referrer", "is_ct", "actual_timestamp"),
158
- null,
159
- null,
160
- null,
161
- )
162
- } catch (_: Exception) {
163
- null
164
- } ?: continue
165
- cursor.use {
166
- if (it.moveToFirst()) {
167
- val referrerIdx = it.getColumnIndex("install_referrer")
168
- val ctIdx = it.getColumnIndex("is_ct")
169
- val tsIdx = it.getColumnIndex("actual_timestamp")
170
- if (referrerIdx >= 0) {
171
- promise.resolve(mapOf(
172
- "installReferrer" to it.getString(referrerIdx),
173
- "isClickThrough" to (ctIdx >= 0 && it.getInt(ctIdx) == 1),
174
- "actualTimestamp" to (if (tsIdx >= 0) it.getLong(tsIdx) else 0L),
175
- "provider" to pkg,
176
- ))
177
- return@AsyncFunction
178
- }
179
- }
180
- }
181
- }
182
- promise.resolve(null)
183
- } catch (e: Exception) {
184
- promise.reject("EMETAREF", e.message, e)
185
- }
31
+ // Diagnostics travel back to JavaScript. `onError` is a JS closure and
32
+ // cannot cross the channel, so what crosses is the event: a stable context
33
+ // identifier and a message, never PII. Without this the SDK's own swallowed
34
+ // failures reach nobody, which is what happened between the inversion and
35
+ // this commit.
36
+ Events("onAdvenueError")
37
+
38
+ Function("bridgeInitialize") { options: Map<String, Any?> ->
39
+ val application = appContext.reactContext?.applicationContext as? android.app.Application
40
+ val apiKey = options["apiKey"] as? String
41
+ if (application == null || apiKey == null) return@Function
42
+ Advenue.initialize(
43
+ application,
44
+ AdvenueConfig(
45
+ apiKey = apiKey,
46
+ endpoint = (options["endpoint"] as? String) ?: DEFAULT_ENDPOINT,
47
+ appVersion = options["appVersion"] as? String,
48
+ requireConsent = (options["requireConsent"] as? Boolean) ?: false,
49
+ batchSize = (options["batchSize"] as? Number)?.toInt() ?: 20,
50
+ flushIntervalMs = (options["flushIntervalMs"] as? Number)?.toLong() ?: 15_000L,
51
+ signingSecret = options["signingSecret"] as? String,
52
+ onError = { context, error ->
53
+ sendEvent("onAdvenueError", mapOf("context" to context, "message" to error.toString()))
54
+ },
55
+ // The wrapper's version, not this SDK's: a React Native release pins
56
+ // the native snapshot inside it, so its version identifies both.
57
+ sdkVersion = options["sdkVersion"] as? String,
58
+ // The JavaScript has sent this since before the inversion; nothing
59
+ // read it, so Meta install-referrer attribution never ran.
60
+ fbAppId = options["fbAppId"] as? String,
61
+ ),
62
+ )
186
63
  }
187
64
 
188
- // Google Advertising ID (GAID) + limit-ad-tracking flag. The blocking
189
- // Play Services call is run on a background thread.
190
- AsyncFunction("getAdvertisingId") { promise: Promise ->
191
- Thread {
192
- try {
193
- val ctx = appContext.reactContext?.applicationContext
194
- if (ctx == null) { promise.resolve(null); return@Thread }
195
- val info = AdvertisingIdClient.getAdvertisingIdInfo(ctx)
196
- val id = info.id
197
- val zeroGaid = "00000000-0000-0000-0000-000000000000"
198
- if (id.isNullOrEmpty() || id == zeroGaid) promise.resolve(null)
199
- else promise.resolve(mapOf("id" to id, "limitAdTracking" to info.isLimitAdTrackingEnabled))
200
- } catch (e: Exception) {
201
- promise.resolve(null)
202
- }
203
- }.start()
65
+ Function("bridgeTrack") { name: String, properties: Map<String, Any?>? ->
66
+ Advenue.track(name, properties)
204
67
  }
205
68
 
206
- // App Set ID — non-advertising, developer-scoped id. First-party fraud/dedup
207
- // use only; Google forbids using it for ads or measurement.
208
- AsyncFunction("getAppSetId") { promise: Promise ->
209
- val ctx = appContext.reactContext?.applicationContext
210
- if (ctx == null) { promise.resolve(null); return@AsyncFunction }
211
- AppSet.getClient(ctx).appSetIdInfo
212
- .addOnSuccessListener { info -> promise.resolve(info.id) }
213
- .addOnFailureListener { promise.resolve(null) }
69
+ Function("bridgeFlush") { Advenue.flush() }
70
+ Function("bridgeSetUserId") { id: String? -> Advenue.setUserId(id) }
71
+ Function("bridgeSetConsent") { granted: Boolean -> Advenue.setConsent(granted) }
72
+ Function("bridgeForgetMe") { Advenue.forgetMe() }
73
+ Function("bridgeShutdown") { Advenue.shutdown() }
74
+ Function("bridgeResolvedAppId") { Advenue.resolvedAppId() }
75
+ Function("bridgeDeviceId") { Advenue.deviceId() }
76
+
77
+ Function("bridgeSetConsentData") { data: Map<String, Any?> ->
78
+ val gdpr = data["isUserSubjectToGDPR"] as? Boolean ?: return@Function
79
+ Advenue.setConsentData(
80
+ gdpr,
81
+ data["hasConsentForDataUsage"] as? Boolean,
82
+ data["hasConsentForAdsPersonalization"] as? Boolean,
83
+ data["hasConsentForAdStorage"] as? Boolean,
84
+ )
214
85
  }
215
86
 
216
- // Play Integrity Standard API — device attestation (G3.1n).
217
- //
218
- // `requestHash` MUST be sha256_hex("<deviceId>") — lowercase hex of SHA-256
219
- // over the device id alone. The server's mapIntegrityVerdict re-derives the
220
- // same string and compares it to the token's requestHash field; the strings
221
- // must match byte-for-byte. No app id is hashed in: the token is decoded
222
- // against this app's Play package name and must be PLAY_RECOGNIZED, which
223
- // binds the app already.
224
- //
225
- // The provider is warmed in OnCreate (above). If it is null — because the
226
- // manifest meta-data key is absent, Play Integrity is unsupported, or warming
227
- // failed — this rejects with ECONFIG / EUNSUPPORTED. The JS layer treats any
228
- // rejection as "no attestation" and omits the token from the install event.
229
- AsyncFunction("getIntegrityToken") { requestHash: String, promise: Promise ->
230
- val provider = integrityProvider
231
- if (provider == null) {
232
- // Distinguish "project number not set" from "Play Integrity not available".
233
- val ctx = appContext.reactContext?.applicationContext
234
- if (ctx != null) {
235
- val ai = try {
236
- ctx.packageManager.getApplicationInfo(
237
- ctx.packageName,
238
- android.content.pm.PackageManager.GET_META_DATA,
239
- )
240
- } catch (_: Exception) { null }
241
- val mb = ai?.metaData
242
- val cfgNum: Long = mb?.getLong("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER", 0L)
243
- ?.takeIf { it != 0L }
244
- ?: mb?.getString("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER")?.toLongOrNull()
245
- ?: 0L
246
- if (cfgNum == 0L) {
247
- promise.reject(
248
- "ECONFIG",
249
- "Play Integrity not configured: add io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER to AndroidManifest.xml meta-data",
250
- null,
251
- )
252
- return@AsyncFunction
87
+ // Read back, for the JavaScript consent cache. The cache is seeded from
88
+ // this at initialize: consent is persisted and survives restarts, so a
89
+ // facade defaulting to false on every cold start would silently discard a
90
+ // preference the user gave.
91
+ Function("bridgeGetConsent") { Advenue.trackingConsent() }
92
+
93
+ Function("bridgeGetConsentData") {
94
+ val consent = Advenue.consentData()
95
+ if (consent == null) {
96
+ null
97
+ } else {
98
+ // Written only when stated: "not asked" is not "denied", and a false
99
+ // invented here is a recorded refusal that never happened.
100
+ buildMap<String, Any> {
101
+ put("isUserSubjectToGDPR", consent.isUserSubjectToGDPR)
102
+ consent.hasConsentForDataUsage?.let { put("hasConsentForDataUsage", it) }
103
+ consent.hasConsentForAdsPersonalization?.let {
104
+ put("hasConsentForAdsPersonalization", it)
253
105
  }
106
+ consent.hasConsentForAdStorage?.let { put("hasConsentForAdStorage", it) }
254
107
  }
255
- promise.reject(
256
- "EUNSUPPORTED",
257
- "Play Integrity token provider not ready (Play Services may be unavailable or warming failed)",
258
- null,
259
- )
260
- return@AsyncFunction
261
108
  }
262
-
263
- val request = StandardIntegrityTokenRequest.builder()
264
- .setRequestHash(requestHash)
265
- .build()
266
-
267
- provider.request(request)
268
- .addOnSuccessListener { response -> promise.resolve(response.token()) }
269
- .addOnFailureListener { e ->
270
- promise.reject("EINTEGRITY", e.message ?: "Play Integrity request failed", e)
271
- }
272
109
  }
273
110
 
274
- // Device metadata for Meta CAPI `extinfo` (Task 3n). Synchronous — every
275
- // API used here is a cheap local read (no network, no blocking I/O beyond
276
- // a local StatFs stat call). Best-effort per field: a field is simply
277
- // omitted (not defaulted) when its source is unavailable, so the JS-side
278
- // fail-safe (collectDeviceInfo) degrades gracefully rather than sending
279
- // misleading zeros.
280
- Function("getDeviceInfo") {
281
- val map = mutableMapOf<String, Any?>()
282
- map["model"] = android.os.Build.MODEL
283
- map["locale"] = java.util.Locale.getDefault().toString()
284
- map["timezone"] = java.util.TimeZone.getDefault().id
285
- map["cpuCores"] = Runtime.getRuntime().availableProcessors()
286
-
287
- val ctx = appContext.reactContext?.applicationContext
288
- if (ctx != null) {
289
- // Meta CAPI extinfo slots 1-3. versionName is the user-facing version
290
- // (slot 2, surfaced as the event's appVersion); the version code is the
291
- // build identifier (slot 3). Best-effort: a missing PackageInfo must
292
- // not cost the caller the rest of the device metadata.
293
- map["packageName"] = ctx.packageName
294
- try {
295
- val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
296
- pi.versionName?.let { map["shortVersion"] = it }
297
- @Suppress("DEPRECATION")
298
- val code =
299
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
300
- pi.longVersionCode
301
- } else {
302
- pi.versionCode.toLong()
303
- }
304
- map["longVersion"] = code.toString()
305
- } catch (_: Exception) {
306
- // package info best-effort
307
- }
308
- try {
309
- val dm = ctx.resources.displayMetrics
310
- map["screenWidth"] = dm.widthPixels
311
- map["screenHeight"] = dm.heightPixels
312
- map["screenDensity"] = dm.density.toDouble()
313
- } catch (_: Exception) {
314
- // screen metrics best-effort
315
- }
316
- try {
317
- val statFs = android.os.StatFs(ctx.filesDir.path)
318
- val blockSize = statFs.blockSizeLong
319
- val gb = 1024.0 * 1024.0 * 1024.0
320
- map["totalStorageGb"] = (statFs.blockCountLong * blockSize).toDouble() / gb
321
- map["freeStorageGb"] = (statFs.availableBlocksLong * blockSize).toDouble() / gb
322
- } catch (_: Exception) {
323
- // storage stat best-effort
324
- }
325
- try {
326
- val tm = ctx.getSystemService(android.content.Context.TELEPHONY_SERVICE)
327
- as? android.telephony.TelephonyManager
328
- val carrier = tm?.networkOperatorName
329
- if (!carrier.isNullOrEmpty()) map["carrier"] = carrier
330
- } catch (_: Exception) {
331
- // carrier best-effort
332
- }
333
- }
334
- map
335
- }
111
+ Function("bridgeSetAppInstanceId") { id: String? -> Advenue.setAppInstanceId(id) }
336
112
 
337
- // Built-in SDK storage (Adjust-style: the app supplies nothing). A
338
- // dedicated SharedPreferences file keeps SDK keys out of the app's prefs.
339
- // Wiped on uninstall — Android has no reinstall-durable store, so a
340
- // reinstall legitimately re-fires the install (backend window dedups).
341
- Function("storageGetItem") { key: String ->
342
- sdkPrefs()?.getString(key, null)
343
- }
344
- Function("storageSetItem") { key: String, value: String ->
345
- sdkPrefs()?.edit()?.putString(key, value)?.apply()
346
- }
347
- Function("storageRemoveItem") { key: String ->
348
- sdkPrefs()?.edit()?.remove(key)?.apply()
113
+ Function("bridgeSetPushToken") { token: String?, provider: String? ->
114
+ Advenue.setPushToken(token, provider)
349
115
  }
350
116
 
351
- // IAB TCF v2 data a TCF-compliant CMP (e.g. Google UMP) writes to the
352
- // *default* SharedPreferences ("<package>_preferences", per the TCF spec) —
353
- // deliberately not our io.advenue.sdk file. Raw values pass through; the
354
- // TS side (tcfToConsent) owns the policy mapping.
355
- Function("getTcfData") { ->
356
- val ctx = appContext.reactContext?.applicationContext ?: return@Function null
357
- val prefs = ctx.getSharedPreferences(
358
- "${ctx.packageName}_preferences", android.content.Context.MODE_PRIVATE
359
- )
360
- // prefs.all + map lookup: a missing key reads as null (no default value
361
- // involved), so "missing" and "0" stay distinguishable. `as? Number`
362
- // tolerates a CMP writing the value as Int OR Long (SharedPreferences
363
- // has no fixed numeric type per the TCF spec's own contract) without
364
- // degrading a Long writer to unknown. runCatching stays as the
365
- // last-resort crash guard.
366
- val gdprApplies = runCatching {
367
- (prefs.all["IABTCF_gdprApplies"] as? Number)?.toInt()
368
- }.getOrNull()
369
- val purposeConsents = runCatching { prefs.getString("IABTCF_PurposeConsents", null) }.getOrNull()
370
- if (gdprApplies == null && purposeConsents == null) return@Function null
371
- mapOf("gdprApplies" to gdprApplies, "purposeConsents" to purposeConsents)
117
+ Function("bridgeProcessDeepLink") { url: String ->
118
+ val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
119
+ intent.data = android.net.Uri.parse(url)
120
+ Advenue.processDeepLink(intent)
372
121
  }
373
122
 
374
- // SSAID (ANDROID_ID): signing-key-scoped, stable across reinstalls, reset
375
- // only by factory reset. Read by the JS facade ONLY when no usable GAID
376
- // exists, as a first-party fraud/dedup + reinstall-detection signal (Play
377
- // policy: never linked to the advertising id, never used for ads).
378
- Function("getAndroidId") { ->
379
- try {
380
- val ctx = appContext.reactContext?.applicationContext
381
- if (ctx != null) {
382
- android.provider.Settings.Secure.getString(
383
- ctx.contentResolver,
384
- android.provider.Settings.Secure.ANDROID_ID,
385
- )
123
+ // Blocking by nature — it polls with backoff — so it is async across the
124
+ // bridge and runs on its own thread. Calling it on the JS thread would
125
+ // freeze the UI for up to eight seconds.
126
+ AsyncFunction("bridgeResolveDeferredDeepLink") { promise: Promise ->
127
+ Thread {
128
+ val link = Advenue.resolveDeferredDeepLink()
129
+ if (link == null) {
130
+ promise.resolve(null)
386
131
  } else {
387
- null
132
+ promise.resolve(
133
+ mapOf(
134
+ "deepLinkValue" to link.deepLinkValue,
135
+ "campaign" to link.campaign,
136
+ "network" to link.network,
137
+ "influencerId" to link.influencerId,
138
+ "isDeferred" to link.isDeferred,
139
+ "isFirstLaunch" to link.isFirstLaunch,
140
+ ),
141
+ )
388
142
  }
389
- } catch (_: Exception) {
390
- null // best-effort: a missing SSAID never blocks anything
391
143
  }
144
+ .apply { isDaemon = true }
145
+ .start()
392
146
  }
393
147
  }
394
-
395
- private fun sdkPrefs(): android.content.SharedPreferences? =
396
- appContext.reactContext?.applicationContext
397
- ?.getSharedPreferences("io.advenue.sdk", android.content.Context.MODE_PRIVATE)
398
148
  }