@advenue/react-native 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +0 -1
- package/android/src/main/kotlin/io/advenue/Advenue.kt +27 -2
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +27 -0
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +8 -1
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +24 -2
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +3 -0
- package/android/src/main/kotlin/io/advenue/core/PiiScrub.kt +77 -0
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +14 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +5 -0
- package/dist/index.cjs +8 -3
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -3
- package/ios/AdvenueIosModule.swift +0 -1
- package/ios/vendor/Advenue/Advenue.swift +41 -5
- package/ios/vendor/Advenue/AdvenueConfig.swift +13 -1
- package/ios/vendor/AdvenueCore/Engine.swift +25 -5
- package/ios/vendor/AdvenuePlatform/CacheFileStore.swift +90 -0
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +12 -0
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +16 -4
- package/package.json +9 -9
- package/src/index.ts +9 -2
- package/src/native-types.ts +0 -1
- package/src/types.ts +5 -0
|
@@ -44,7 +44,6 @@ class AdvenueAndroidModule : Module() {
|
|
|
44
44
|
AdvenueConfig(
|
|
45
45
|
apiKey = apiKey,
|
|
46
46
|
endpoint = (options["endpoint"] as? String) ?: DEFAULT_ENDPOINT,
|
|
47
|
-
appVersion = options["appVersion"] as? String,
|
|
48
47
|
requireConsent = (options["requireConsent"] as? Boolean) ?: false,
|
|
49
48
|
batchSize = (options["batchSize"] as? Number)?.toInt() ?: 20,
|
|
50
49
|
flushIntervalMs = (options["flushIntervalMs"] as? Number)?.toLong() ?: 15_000L,
|
|
@@ -30,6 +30,7 @@ import io.advenue.core.extractAemCampaignIds
|
|
|
30
30
|
import io.advenue.core.tcfToConsent
|
|
31
31
|
import io.advenue.platform.collectDeviceInfo
|
|
32
32
|
import io.advenue.platform.readTcf
|
|
33
|
+
import io.advenue.platform.readAppVersion
|
|
33
34
|
import io.advenue.platform.collectEnrichment
|
|
34
35
|
import io.advenue.platform.sha256Hex
|
|
35
36
|
import io.advenue.platform.resolveIdentity
|
|
@@ -256,6 +257,14 @@ internal class FacadeState {
|
|
|
256
257
|
// Replace-and-shut-down, never add.
|
|
257
258
|
stop()
|
|
258
259
|
|
|
260
|
+
// The SDK resolves the app version itself (readAppVersion); a config value
|
|
261
|
+
// is ignored — and said so, rather than looking like it took effect.
|
|
262
|
+
if (config.appVersion != null) {
|
|
263
|
+
config.onError(
|
|
264
|
+
"config.ignored:appVersion",
|
|
265
|
+
IllegalArgumentException("appVersion is resolved by the SDK and has no effect"),
|
|
266
|
+
)
|
|
267
|
+
}
|
|
259
268
|
val scoped = InstallScopedStore(application)
|
|
260
269
|
val prefs = PreferencesStore(application)
|
|
261
270
|
val uuid = SystemUuids()
|
|
@@ -292,12 +301,13 @@ internal class FacadeState {
|
|
|
292
301
|
platform = "android",
|
|
293
302
|
deviceId = identity.deviceId,
|
|
294
303
|
installationId = identity.installationId,
|
|
295
|
-
appVersion =
|
|
304
|
+
appVersion = readAppVersion(application),
|
|
296
305
|
osVersion = Build.VERSION.RELEASE,
|
|
297
306
|
sdkVersion = (config.sdkVersion ?: AdvenueVersion.CURRENT).take(32),
|
|
298
307
|
requireConsent = config.requireConsent,
|
|
299
308
|
sessionWindowMs = config.sessionWindowMs,
|
|
300
309
|
batchSize = config.batchSize,
|
|
310
|
+
piiScrubEnabled = config.piiScrubEnabled,
|
|
301
311
|
),
|
|
302
312
|
store = CompositeStore(prefs, scoped),
|
|
303
313
|
clock = SystemClock(),
|
|
@@ -368,6 +378,8 @@ internal class FacadeState {
|
|
|
368
378
|
gaid = enrichment.gaid,
|
|
369
379
|
androidId = null,
|
|
370
380
|
appInstanceId = enrichment.appInstanceId,
|
|
381
|
+
// B2: bayrak artık düşürülmüyor — olaya işlenir.
|
|
382
|
+
limitAdTracking = enrichment.limitAdTracking,
|
|
371
383
|
),
|
|
372
384
|
)
|
|
373
385
|
// Device metadata rides the install, where Meta CAPI reads it.
|
|
@@ -458,7 +470,10 @@ internal class FacadeState {
|
|
|
458
470
|
}
|
|
459
471
|
|
|
460
472
|
private fun sendDeepLink(url: String) {
|
|
461
|
-
|
|
473
|
+
// B3: ham query gönderilmez — token/e-posta query'de taşınır. AEM
|
|
474
|
+
// ayrıştırması ve sourceUrlHash TAM url'den yapılır (aşağıda), yalnızca
|
|
475
|
+
// gönderilen `url` özelliği scheme+host+path'e indirgenir.
|
|
476
|
+
submit(Command.Track("deep_link", mapOf("url" to stripUrlQuery(url)), "custom"))
|
|
462
477
|
|
|
463
478
|
// Meta AEM: a link from Meta carries al_applink_data with an opaque,
|
|
464
479
|
// Meta-encrypted campaign_ids blob. Emitted at most once per URL — the
|
|
@@ -482,6 +497,7 @@ internal class FacadeState {
|
|
|
482
497
|
private fun queryValue(url: String, name: String): String? =
|
|
483
498
|
runCatching { android.net.Uri.parse(url).getQueryParameter(name) }.getOrNull()
|
|
484
499
|
|
|
500
|
+
|
|
485
501
|
/** Test surface: how many links are waiting for `initialize`. */
|
|
486
502
|
val bufferedDeepLinkCount: Int
|
|
487
503
|
get() = synchronized(lock) { pendingDeepLinks.size }
|
|
@@ -547,3 +563,12 @@ internal class FacadeState {
|
|
|
547
563
|
val app: Application?,
|
|
548
564
|
)
|
|
549
565
|
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* B3: deep-link URL'sinden query+fragment atılır (token/e-posta taşınır).
|
|
569
|
+
* Saf dize işlemi — Android çalıştırıcısı gerekmez, JVM testinde de çalışır.
|
|
570
|
+
*/
|
|
571
|
+
internal fun stripUrlQuery(url: String): String {
|
|
572
|
+
val cut = url.indexOfAny(charArrayOf('?', '#'))
|
|
573
|
+
return if (cut >= 0) url.substring(0, cut) else url
|
|
574
|
+
}
|
|
@@ -12,6 +12,7 @@ public data class AdvenueConfig
|
|
|
12
12
|
constructor(
|
|
13
13
|
val apiKey: String,
|
|
14
14
|
val endpoint: String = DEFAULT_ENDPOINT,
|
|
15
|
+
/** Ignored: the SDK reads the app version from the package itself. Setting it reports `config.ignored:appVersion` through [onError]. Removed in the next major. */
|
|
15
16
|
val appVersion: String? = null,
|
|
16
17
|
val requireConsent: Boolean = false,
|
|
17
18
|
val sessionWindowMs: Long = DEFAULT_SESSION_WINDOW_MS,
|
|
@@ -53,7 +54,24 @@ constructor(
|
|
|
53
54
|
* iOS has no equivalent. A field on the Swift config would do nothing.
|
|
54
55
|
*/
|
|
55
56
|
val fbAppId: String? = null,
|
|
57
|
+
/**
|
|
58
|
+
* B1: track properties PII scrub'u. Varsayılan-açık; yalnızca açıkça
|
|
59
|
+
* kapatın (dokümante risk — ham PII ingest'e gider). Swift ile aynı.
|
|
60
|
+
*/
|
|
61
|
+
val piiScrubEnabled: Boolean = true,
|
|
62
|
+
/**
|
|
63
|
+
* B5: testler için `http://` izni. Üretimde kapalı tutun — açık `http://`
|
|
64
|
+
* API anahtarı dahil tüm olayları şifresiz taşır.
|
|
65
|
+
*/
|
|
66
|
+
val allowInsecureHttp: Boolean = false,
|
|
56
67
|
) {
|
|
68
|
+
init {
|
|
69
|
+
// B5: şemasız endpoint fail-fast — geçersiz şema transportta değil,
|
|
70
|
+
// kurulumda yakalanır.
|
|
71
|
+
require(allowInsecureHttp || endpoint.startsWith("https://")) {
|
|
72
|
+
"Advenue endpoint must use https:// (set allowInsecureHttp only for tests)"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
/**
|
|
58
76
|
* Java-friendly builder. Kotlin's named and default arguments are invisible
|
|
59
77
|
* from Java, and both incumbents are Java-first — a Kotlin SDK that is painful
|
|
@@ -64,6 +82,7 @@ constructor(
|
|
|
64
82
|
|
|
65
83
|
public fun endpoint(value: String): Builder = apply { config = config.copy(endpoint = value) }
|
|
66
84
|
|
|
85
|
+
/** Ignored — see [AdvenueConfig.appVersion]. */
|
|
67
86
|
public fun appVersion(value: String?): Builder = apply {
|
|
68
87
|
config = config.copy(appVersion = value)
|
|
69
88
|
}
|
|
@@ -98,6 +117,14 @@ constructor(
|
|
|
98
117
|
config = config.copy(fbAppId = value)
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
public fun piiScrubEnabled(value: Boolean): Builder = apply {
|
|
121
|
+
config = config.copy(piiScrubEnabled = value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public fun allowInsecureHttp(value: Boolean): Builder = apply {
|
|
125
|
+
config = config.copy(allowInsecureHttp = value)
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
public fun build(): AdvenueConfig = config
|
|
102
129
|
}
|
|
103
130
|
}
|
|
@@ -19,6 +19,8 @@ internal sealed class Command {
|
|
|
19
19
|
val gaid: String?,
|
|
20
20
|
val androidId: String?,
|
|
21
21
|
val appInstanceId: String?,
|
|
22
|
+
/** B2: reklam kimliği rıza bayrağı — toplanıp düşürülüyordu. */
|
|
23
|
+
val limitAdTracking: Boolean? = null,
|
|
22
24
|
) : Command()
|
|
23
25
|
|
|
24
26
|
internal data class SetAppInstanceId(val id: String?) : Command()
|
|
@@ -121,7 +123,12 @@ internal class CommandPipe(
|
|
|
121
123
|
is Command.SetConsent -> engine.setConsent(command.granted)
|
|
122
124
|
is Command.SetUserId -> engine.setUserId(command.id)
|
|
123
125
|
is Command.SetIdentity ->
|
|
124
|
-
engine.setIdentity(
|
|
126
|
+
engine.setIdentity(
|
|
127
|
+
command.gaid,
|
|
128
|
+
command.androidId,
|
|
129
|
+
command.appInstanceId,
|
|
130
|
+
limitAdTracking = command.limitAdTracking,
|
|
131
|
+
)
|
|
125
132
|
is Command.SetAppInstanceId -> engine.setAppInstanceId(command.id)
|
|
126
133
|
is Command.TrackInstall -> engine.trackInstall(command.properties)
|
|
127
134
|
is Command.SetConsentData -> engine.setConsentData(command.consent)
|
|
@@ -39,6 +39,9 @@ internal data class EngineConfig(
|
|
|
39
39
|
val batchSize: Int = 20,
|
|
40
40
|
val retryBaseMs: Double = 1_000.0,
|
|
41
41
|
val retryCapMs: Double = 60_000.0,
|
|
42
|
+
/** B1: track properties PII scrub'u. Varsayılan-açık; yalnızca açık bayrakla
|
|
43
|
+
* kapatılır (opt-out) ve bu dokümante risklidir. Swift EngineConfig ile aynı. */
|
|
44
|
+
val piiScrubEnabled: Boolean = true,
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -72,6 +75,8 @@ internal class AdvenueEngine(
|
|
|
72
75
|
private var gaid: String? = null
|
|
73
76
|
private var androidId: String? = null
|
|
74
77
|
private var appInstanceId: String? = null
|
|
78
|
+
/** B2: rıza bayrağı — her olaya işlenir, persist edilmez (kimlik gibi). */
|
|
79
|
+
private var limitAdTracking: Boolean? = null
|
|
75
80
|
private var consentData: Consent? = readConsentData()
|
|
76
81
|
private var pushToken: String? = null
|
|
77
82
|
private var pushProvider: String? = null
|
|
@@ -90,7 +95,9 @@ internal class AdvenueEngine(
|
|
|
90
95
|
onError("track.rejected:$rejection", IngestError(400))
|
|
91
96
|
return false
|
|
92
97
|
}
|
|
93
|
-
|
|
98
|
+
// B1: PII scrub (Swift PIIScrub ile aynı sözleşme).
|
|
99
|
+
val scrubbed = if (config.piiScrubEnabled) PiiScrub.scrub(properties) else properties
|
|
100
|
+
queue.enqueue(newEvent(type = type, name = name, properties = scrubbed))
|
|
94
101
|
return true
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -146,6 +153,12 @@ internal class AdvenueEngine(
|
|
|
146
153
|
(parseJson(store.getString(CONSENT_DATA_KEY) ?: "") as? Map<*, *>)?.let(Consent::fromMap)
|
|
147
154
|
|
|
148
155
|
public fun setUserId(id: String?) {
|
|
156
|
+
// B6: 128 üstü id tüm batch'i 400'e düşürür (ingest bütün-batch ayrıştırır).
|
|
157
|
+
// Reddet, persist etme, onError ile bildir (Limits.kt deseni).
|
|
158
|
+
if (id != null && id.length > MAX_USER_ID_LENGTH) {
|
|
159
|
+
onError("account.setUserId:id_too_long", IngestError(400))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
149
162
|
customerUserId = id
|
|
150
163
|
if (id != null) store.setString(USER_ID_KEY, id) else store.remove(USER_ID_KEY)
|
|
151
164
|
}
|
|
@@ -160,11 +173,13 @@ internal class AdvenueEngine(
|
|
|
160
173
|
androidId: String?,
|
|
161
174
|
appInstanceId: String?,
|
|
162
175
|
idfa: String? = null,
|
|
176
|
+
limitAdTracking: Boolean? = null,
|
|
163
177
|
) {
|
|
164
178
|
this.gaid = gaid
|
|
165
179
|
this.androidId = androidId
|
|
166
180
|
this.appInstanceId = appInstanceId
|
|
167
181
|
this.idfa = idfa
|
|
182
|
+
this.limitAdTracking = limitAdTracking
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
/**
|
|
@@ -189,7 +204,9 @@ internal class AdvenueEngine(
|
|
|
189
204
|
public fun trackInstall(properties: Map<String, Any?>? = null): Boolean {
|
|
190
205
|
if (forgotten || (config.requireConsent && !consent)) return false
|
|
191
206
|
if (store.getString(INSTALL_SENT_KEY) == "1") return false
|
|
192
|
-
|
|
207
|
+
// B1: install properties de scrub kapsamındadır.
|
|
208
|
+
val scrubbed = if (config.piiScrubEnabled) PiiScrub.scrub(properties) else properties
|
|
209
|
+
queue.enqueue(newEvent(type = "install", name = "install", properties = scrubbed))
|
|
193
210
|
store.setString(INSTALL_SENT_KEY, "1")
|
|
194
211
|
return true
|
|
195
212
|
}
|
|
@@ -231,6 +248,10 @@ internal class AdvenueEngine(
|
|
|
231
248
|
DEVICE_ID_KEY,
|
|
232
249
|
INSTALLATION_ID_KEY,
|
|
233
250
|
INSTALL_SENT_KEY,
|
|
251
|
+
// queue.clear() above persists an empty blob synchronously; remove it
|
|
252
|
+
// so erasure leaves no queue file behind either. Safe to do last:
|
|
253
|
+
// `forgotten` stops every later enqueue, so nothing rewrites it.
|
|
254
|
+
QUEUE_KEY,
|
|
234
255
|
)) {
|
|
235
256
|
store.remove(key)
|
|
236
257
|
}
|
|
@@ -402,6 +423,7 @@ internal class AdvenueEngine(
|
|
|
402
423
|
customerUserId = customerUserId,
|
|
403
424
|
idfa = idfa,
|
|
404
425
|
gaid = gaid,
|
|
426
|
+
limitAdTracking = limitAdTracking,
|
|
405
427
|
androidId = androidId,
|
|
406
428
|
appInstanceId = appInstanceId,
|
|
407
429
|
consent = consentData?.toMap(),
|
|
@@ -22,6 +22,9 @@ package io.advenue.core
|
|
|
22
22
|
/** `clientEventSchema.name` — z.string().min(1).max(128). */
|
|
23
23
|
internal const val MAX_NAME_LENGTH: Int = 128
|
|
24
24
|
|
|
25
|
+
/** `customerUserId` — z.string().max(128). Üstü batch'i 400'e düşürür. */
|
|
26
|
+
internal const val MAX_USER_ID_LENGTH: Int = 128
|
|
27
|
+
|
|
25
28
|
/** `propertiesSchema` — at most this many keys. */
|
|
26
29
|
internal const val MAX_PROPERTIES_KEYS: Int = 50
|
|
27
30
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* B1: track properties PII scrubbing (varsayılan-açık) — Swift `PIIScrub`
|
|
5
|
+
* ile aynı sözleşme. Anahtar-adı tabanlı + değer-örüntü tabanlı eşleşen
|
|
6
|
+
* değerler gönderilmeden önce kaldırılır; ham değer ağa çıkmaz.
|
|
7
|
+
*/
|
|
8
|
+
internal object PiiScrub {
|
|
9
|
+
private val blockedKeys: Set<String> = setOf(
|
|
10
|
+
"email", "emailaddress", "emailadres", "eposta", "mail",
|
|
11
|
+
"phone", "phonenumber", "mobile", "mobilenumber", "mobil",
|
|
12
|
+
"telefon", "telefonno", "tel", "telno", "gsm",
|
|
13
|
+
"firstname", "lastname", "ad", "soyad", "adsoyad", "adisoyadi",
|
|
14
|
+
"fullname", "namesurname", "isim", "isimsoyisim",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
private val blockedSubstrings: List<String> = listOf(
|
|
18
|
+
"email", "phone", "telefon", "soyad", "adsoyad", "firstname", "lastname",
|
|
19
|
+
"fullname",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
private val emailRegex =
|
|
23
|
+
Regex("[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}", RegexOption.IGNORE_CASE)
|
|
24
|
+
|
|
25
|
+
private val phoneChars: Set<Char> = "+0123456789 \t.-()".toSet()
|
|
26
|
+
|
|
27
|
+
internal fun normalizeKey(key: String): String =
|
|
28
|
+
key.lowercase().filter { it.isLetterOrDigit() }
|
|
29
|
+
|
|
30
|
+
internal fun isSensitiveKey(key: String): Boolean {
|
|
31
|
+
val normalized = normalizeKey(key)
|
|
32
|
+
if (blockedKeys.contains(normalized)) return true
|
|
33
|
+
return blockedSubstrings.any { normalized.contains(it) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Telefon sezgiseli (Swift ile aynı): çapasız alt-dize araması YOK.
|
|
38
|
+
* Değerin TÜMÜ telefon karakterlerinden oluşmalı ve rakam sayısı
|
|
39
|
+
* E.164 aralığında (10–15) olmalı.
|
|
40
|
+
*/
|
|
41
|
+
internal fun looksLikePhone(value: String): Boolean {
|
|
42
|
+
val trimmed = value.trim()
|
|
43
|
+
if (trimmed.isEmpty()) return false
|
|
44
|
+
if (!trimmed.all { phoneChars.contains(it) }) return false
|
|
45
|
+
val first = trimmed.first()
|
|
46
|
+
val last = trimmed.last()
|
|
47
|
+
if (!(first == '+' || first.isDigit()) || !last.isDigit()) return false
|
|
48
|
+
return trimmed.count { it.isDigit() } in 10..15
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
internal fun isSensitiveValue(value: String): Boolean =
|
|
52
|
+
emailRegex.containsMatchIn(value) || looksLikePhone(value)
|
|
53
|
+
|
|
54
|
+
private fun scrubValue(value: Any?): Any? =
|
|
55
|
+
when (value) {
|
|
56
|
+
is String -> if (isSensitiveValue(value)) null else value
|
|
57
|
+
is Map<*, *> -> {
|
|
58
|
+
@Suppress("UNCHECKED_CAST")
|
|
59
|
+
val cleaned = scrub(value as Map<String, Any?>) ?: return null
|
|
60
|
+
cleaned
|
|
61
|
+
}
|
|
62
|
+
is List<*> -> value.mapNotNull { scrubValue(it) }
|
|
63
|
+
else -> value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** PII taşıyan girdileri kaldırır. Girdi null ise null döner. */
|
|
67
|
+
internal fun scrub(properties: Map<String, Any?>?): Map<String, Any?>? {
|
|
68
|
+
if (properties == null) return null
|
|
69
|
+
val kept = LinkedHashMap<String, Any?>(properties.size)
|
|
70
|
+
for ((key, value) in properties) {
|
|
71
|
+
if (isSensitiveKey(key)) continue
|
|
72
|
+
val cleaned = scrubValue(value) ?: continue
|
|
73
|
+
kept[key] = cleaned
|
|
74
|
+
}
|
|
75
|
+
return kept
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -103,6 +103,20 @@ internal fun tcfFrom(gdprApplies: Int?, purposeConsents: String?): Map<String, A
|
|
|
103
103
|
* source is unavailable — a misleading zero is worse than a missing value,
|
|
104
104
|
* because downstream it is indistinguishable from a real measurement.
|
|
105
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* The app's own version (`versionName`) for every event's top-level
|
|
108
|
+
* `appVersion`. Resolved here, never taken from the app: the React Native 0.x
|
|
109
|
+
* layer filled it this way, and 1.0 relying on the config instead left every
|
|
110
|
+
* event unversioned (2026-09-19). Null when the package manager can't answer.
|
|
111
|
+
*/
|
|
112
|
+
internal fun readAppVersion(context: Context): String? =
|
|
113
|
+
runCatching {
|
|
114
|
+
@Suppress("DEPRECATION")
|
|
115
|
+
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
|
116
|
+
}
|
|
117
|
+
.getOrNull()
|
|
118
|
+
?.takeIf { it.isNotBlank() }
|
|
119
|
+
|
|
106
120
|
internal fun collectDeviceInfo(context: Context): Map<String, Any?> {
|
|
107
121
|
val map = LinkedHashMap<String, Any?>()
|
|
108
122
|
map["model"] = Build.MODEL
|
|
@@ -4,6 +4,7 @@ import android.content.Context
|
|
|
4
4
|
import io.advenue.core.DEVICE_ID_KEY
|
|
5
5
|
import io.advenue.core.INSTALLATION_ID_KEY
|
|
6
6
|
import io.advenue.core.INSTALL_SENT_KEY
|
|
7
|
+
import io.advenue.core.QUEUE_KEY
|
|
7
8
|
import java.io.File
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -83,6 +84,10 @@ internal class InstallScopedStore(context: Context) : ScopedStore {
|
|
|
83
84
|
DEVICE_ID_KEY to "advenue-device-id",
|
|
84
85
|
INSTALLATION_ID_KEY to "advenue-installation-id",
|
|
85
86
|
INSTALL_SENT_KEY to "advenue-install-sent",
|
|
87
|
+
// B4: kuyruk blob'u (GAID'li olay gövdeleri) yedekten çıkar — geri
|
|
88
|
+
// yüklenen kuyruk hem PII sızıntısı hem de bayat olay yağmurudur.
|
|
89
|
+
// Eski SharedPreferences anahtarı ilk okumada migrate edilir.
|
|
90
|
+
QUEUE_KEY to "advenue-queue",
|
|
86
91
|
)
|
|
87
92
|
|
|
88
93
|
/** The keys this store owns; everything else belongs in [PreferencesStore]. */
|
package/dist/index.cjs
CHANGED
|
@@ -65,7 +65,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
65
65
|
// package.json
|
|
66
66
|
var package_default = {
|
|
67
67
|
name: "@advenue/react-native",
|
|
68
|
-
version: "1.0.
|
|
68
|
+
version: "1.0.1",
|
|
69
69
|
description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
70
70
|
type: "module",
|
|
71
71
|
license: "MIT",
|
|
@@ -214,7 +214,13 @@ function getLinking() {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// src/index.ts
|
|
217
|
-
var IGNORED_OPTIONS = [
|
|
217
|
+
var IGNORED_OPTIONS = [
|
|
218
|
+
"mmkv",
|
|
219
|
+
"secureStore",
|
|
220
|
+
"sdkConfigFetcher",
|
|
221
|
+
"tcfDataCollection",
|
|
222
|
+
"appVersion"
|
|
223
|
+
];
|
|
218
224
|
var started = false;
|
|
219
225
|
var deepLinkSub = null;
|
|
220
226
|
var errorSub = null;
|
|
@@ -268,7 +274,6 @@ var Advenue = {
|
|
|
268
274
|
call("bridgeInitialize", {
|
|
269
275
|
apiKey: config.apiKey,
|
|
270
276
|
endpoint: config.endpoint,
|
|
271
|
-
appVersion: config.appVersion,
|
|
272
277
|
requireConsent: config.requireConsent ?? false,
|
|
273
278
|
batchSize: config.batchSize,
|
|
274
279
|
flushIntervalMs: config.flushIntervalMs,
|
package/dist/index.d.cts
CHANGED
|
@@ -55,6 +55,11 @@ interface AdvenueConfig {
|
|
|
55
55
|
apiKey: string;
|
|
56
56
|
endpoint?: string;
|
|
57
57
|
platform?: 'ios' | 'android' | 'web' | string;
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Ignored: the native SDK resolves the app version from the
|
|
60
|
+
* package/bundle. Passing it reports `config.ignored:appVersion` via
|
|
61
|
+
* `onError`. Removed in the next major.
|
|
62
|
+
*/
|
|
58
63
|
appVersion?: string;
|
|
59
64
|
osVersion?: string;
|
|
60
65
|
/** Gates every event on `setTrackingConsent(true)`. */
|
|
@@ -80,7 +85,6 @@ type TrackingAuthorizationStatus = 'authorized' | 'denied' | 'restricted' | 'not
|
|
|
80
85
|
interface BridgeInitOptions {
|
|
81
86
|
apiKey: string;
|
|
82
87
|
endpoint?: string;
|
|
83
|
-
appVersion?: string;
|
|
84
88
|
requireConsent: boolean;
|
|
85
89
|
batchSize?: number;
|
|
86
90
|
flushIntervalMs?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ interface AdvenueConfig {
|
|
|
55
55
|
apiKey: string;
|
|
56
56
|
endpoint?: string;
|
|
57
57
|
platform?: 'ios' | 'android' | 'web' | string;
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Ignored: the native SDK resolves the app version from the
|
|
60
|
+
* package/bundle. Passing it reports `config.ignored:appVersion` via
|
|
61
|
+
* `onError`. Removed in the next major.
|
|
62
|
+
*/
|
|
58
63
|
appVersion?: string;
|
|
59
64
|
osVersion?: string;
|
|
60
65
|
/** Gates every event on `setTrackingConsent(true)`. */
|
|
@@ -80,7 +85,6 @@ type TrackingAuthorizationStatus = 'authorized' | 'denied' | 'restricted' | 'not
|
|
|
80
85
|
interface BridgeInitOptions {
|
|
81
86
|
apiKey: string;
|
|
82
87
|
endpoint?: string;
|
|
83
|
-
appVersion?: string;
|
|
84
88
|
requireConsent: boolean;
|
|
85
89
|
batchSize?: number;
|
|
86
90
|
flushIntervalMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ var require_native_require = __commonJS({
|
|
|
54
54
|
// package.json
|
|
55
55
|
var package_default = {
|
|
56
56
|
name: "@advenue/react-native",
|
|
57
|
-
version: "1.0.
|
|
57
|
+
version: "1.0.1",
|
|
58
58
|
description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
59
59
|
type: "module",
|
|
60
60
|
license: "MIT",
|
|
@@ -203,7 +203,13 @@ function getLinking() {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// src/index.ts
|
|
206
|
-
var IGNORED_OPTIONS = [
|
|
206
|
+
var IGNORED_OPTIONS = [
|
|
207
|
+
"mmkv",
|
|
208
|
+
"secureStore",
|
|
209
|
+
"sdkConfigFetcher",
|
|
210
|
+
"tcfDataCollection",
|
|
211
|
+
"appVersion"
|
|
212
|
+
];
|
|
207
213
|
var started = false;
|
|
208
214
|
var deepLinkSub = null;
|
|
209
215
|
var errorSub = null;
|
|
@@ -257,7 +263,6 @@ var Advenue = {
|
|
|
257
263
|
call("bridgeInitialize", {
|
|
258
264
|
apiKey: config.apiKey,
|
|
259
265
|
endpoint: config.endpoint,
|
|
260
|
-
appVersion: config.appVersion,
|
|
261
266
|
requireConsent: config.requireConsent ?? false,
|
|
262
267
|
batchSize: config.batchSize,
|
|
263
268
|
flushIntervalMs: config.flushIntervalMs,
|
|
@@ -37,7 +37,6 @@ public class AdvenueIosModule: Module {
|
|
|
37
37
|
var config = AdvenueConfig(
|
|
38
38
|
apiKey: app,
|
|
39
39
|
endpoint: (options["endpoint"] as? String) ?? DEFAULT_ENDPOINT,
|
|
40
|
-
appVersion: options["appVersion"] as? String,
|
|
41
40
|
requireConsent: (options["requireConsent"] as? Bool) ?? false,
|
|
42
41
|
signingSecret: options["signingSecret"] as? String,
|
|
43
42
|
onError: { [weak self] context, error in
|
|
@@ -208,12 +208,30 @@ final class FacadeState: @unchecked Sendable {
|
|
|
208
208
|
_ config: AdvenueConfig,
|
|
209
209
|
transport: (any EventTransport)? = nil,
|
|
210
210
|
sources: EnrichmentSources? = nil,
|
|
211
|
-
skan skanReporter: (any SkanReporter)? = nil
|
|
211
|
+
skan skanReporter: (any SkanReporter)? = nil,
|
|
212
|
+
appVersion readVersion: () -> String? = readAppVersion
|
|
212
213
|
) {
|
|
213
214
|
// Replace-and-shut-down, never add.
|
|
214
215
|
stop()
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
// The SDK resolves the app version itself (readAppVersion); a config value
|
|
218
|
+
// is ignored — and said so, rather than looking like it took effect.
|
|
219
|
+
if config.appVersion != nil {
|
|
220
|
+
config.onError(
|
|
221
|
+
"config.ignored:appVersion",
|
|
222
|
+
IngestError(status: 0))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// B4: kuyruk blob'u Caches dosyalarına (yedek dışı); UserDefaults'ta
|
|
226
|
+
// kalan eski blob ilk okumada migrate edilir. Caches yoksa UserDefaults
|
|
227
|
+
// geri dönüşüdür — yedek kapsamı açılır ama SDK çalışır (fail-open, loglanır).
|
|
228
|
+
let store: any KeyValueStore
|
|
229
|
+
if let files = CacheFileStore.caches() {
|
|
230
|
+
store = CompositeStore(files: files)
|
|
231
|
+
} else {
|
|
232
|
+
store = UserDefaultsStore()
|
|
233
|
+
config.onError("store.cache_unavailable", IngestError(status: 0))
|
|
234
|
+
}
|
|
217
235
|
let secure = KeychainStore()
|
|
218
236
|
let uuid = SystemUUIDs()
|
|
219
237
|
|
|
@@ -245,7 +263,7 @@ final class FacadeState: @unchecked Sendable {
|
|
|
245
263
|
let engine = AdvenueEngine(
|
|
246
264
|
config: EngineConfig(
|
|
247
265
|
apiKey: config.apiKey, platform: "ios", deviceId: deviceId,
|
|
248
|
-
installationId: installationId, appVersion:
|
|
266
|
+
installationId: installationId, appVersion: readVersion(),
|
|
249
267
|
osVersion: osVersion,
|
|
250
268
|
sdkVersion: String((config.sdkVersion ?? AdvenueVersion.current).prefix(32)),
|
|
251
269
|
requireConsent: config.requireConsent, sessionWindowMs: config.sessionWindowMs,
|
|
@@ -357,7 +375,8 @@ final class FacadeState: @unchecked Sendable {
|
|
|
357
375
|
self?.submit(
|
|
358
376
|
.setIdentity(
|
|
359
377
|
idfa: enrichment.idfa, vendorId: enrichment.vendorId,
|
|
360
|
-
appInstanceId: enrichment.appInstanceId
|
|
378
|
+
appInstanceId: enrichment.appInstanceId,
|
|
379
|
+
limitAdTracking: enrichment.limitAdTracking))
|
|
361
380
|
self?.submit(.setDeviceInfo(collectDeviceInfo()))
|
|
362
381
|
// The CMP writes TCF to the standard defaults, and reading it is the
|
|
363
382
|
// difference between shipping a real consent signal and shipping none.
|
|
@@ -487,10 +506,27 @@ final class FacadeState: @unchecked Sendable {
|
|
|
487
506
|
return pendingDeepLinks.count
|
|
488
507
|
}
|
|
489
508
|
|
|
509
|
+
/// B3: deep-link URL'sinden query+fragment atılır. URLComponents
|
|
510
|
+
/// çözümlenemezse ham dizenin `?`/`#` öncesi alınır (asla ham query sızmaz).
|
|
511
|
+
func stripUrlQuery(_ url: URL) -> String {
|
|
512
|
+
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
|
|
513
|
+
components.query = nil
|
|
514
|
+
components.fragment = nil
|
|
515
|
+
if let stripped = components.string { return stripped }
|
|
516
|
+
}
|
|
517
|
+
let raw = url.absoluteString
|
|
518
|
+
if let cut = raw.firstIndex(where: { $0 == "?" || $0 == "#" }) {
|
|
519
|
+
return String(raw[..<cut])
|
|
520
|
+
}
|
|
521
|
+
return raw
|
|
522
|
+
}
|
|
523
|
+
|
|
490
524
|
private func send(_ url: URL) {
|
|
525
|
+
// B3: ham query gönderilmez — token/e-posta query'de taşınır. AEM
|
|
526
|
+
// ayrıştırması ve hash TAM url'den yapılır (aşağıda).
|
|
491
527
|
submit(
|
|
492
528
|
.track(
|
|
493
|
-
name: "deep_link", properties: ["url": .string(url
|
|
529
|
+
name: "deep_link", properties: ["url": .string(stripUrlQuery(url))], type: "custom"))
|
|
494
530
|
|
|
495
531
|
// Meta AEM: a link from Meta carries al_applink_data with an opaque,
|
|
496
532
|
// Meta-encrypted campaign_ids blob. Emitted at most once per URL — the same
|
|
@@ -3,6 +3,8 @@ import Foundation
|
|
|
3
3
|
public struct AdvenueConfig: Sendable {
|
|
4
4
|
public var apiKey: String
|
|
5
5
|
public var endpoint: String
|
|
6
|
+
/// Ignored: the SDK reads the app version from the bundle itself. Setting it
|
|
7
|
+
/// reports `config.ignored:appVersion` through `onError`. Removed in the next major.
|
|
6
8
|
public var appVersion: String?
|
|
7
9
|
public var requireConsent: Bool
|
|
8
10
|
public var sessionWindowMs: Int64
|
|
@@ -41,6 +43,12 @@ public struct AdvenueConfig: Sendable {
|
|
|
41
43
|
/// the whole batch down with a 400, so it is truncated rather than sent.
|
|
42
44
|
public var sdkVersion: String?
|
|
43
45
|
|
|
46
|
+
/// B5: `https://` şema kuralı. Fail-fast `precondition` ile kurulumda
|
|
47
|
+
/// yakalanır; kuralın kendisi test edilebilir saf fonksiyondur.
|
|
48
|
+
public static func isSecureEndpoint(_ endpoint: String) -> Bool {
|
|
49
|
+
endpoint.lowercased().hasPrefix("https://")
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
public init(
|
|
45
53
|
apiKey: String,
|
|
46
54
|
endpoint: String = DEFAULT_ENDPOINT,
|
|
@@ -52,8 +60,12 @@ public struct AdvenueConfig: Sendable {
|
|
|
52
60
|
signingSecret: String? = nil,
|
|
53
61
|
conversionValues: ConversionValueConfig? = nil,
|
|
54
62
|
onError: @escaping @Sendable (String, any Error) -> Void = { _, _ in },
|
|
55
|
-
sdkVersion: String? = nil
|
|
63
|
+
sdkVersion: String? = nil,
|
|
64
|
+
allowInsecureHttp: Bool = false
|
|
56
65
|
) {
|
|
66
|
+
precondition(
|
|
67
|
+
allowInsecureHttp || Self.isSecureEndpoint(endpoint),
|
|
68
|
+
"Advenue endpoint must use https:// (allowInsecureHttp is test-only)")
|
|
57
69
|
self.apiKey = apiKey
|
|
58
70
|
self.endpoint = endpoint
|
|
59
71
|
self.appVersion = appVersion
|
|
@@ -147,6 +147,8 @@ public actor AdvenueEngine {
|
|
|
147
147
|
event.sdkVersion = config.sdkVersion
|
|
148
148
|
event.customerUserId = customerUserId
|
|
149
149
|
event.idfa = idfa
|
|
150
|
+
// B2: rıza bayrağı olaya işlenir.
|
|
151
|
+
event.limitAdTracking = limitAdTracking
|
|
150
152
|
event.vendorId = vendorId
|
|
151
153
|
event.appInstanceId = appInstanceId
|
|
152
154
|
event.consent = consentData
|
|
@@ -163,10 +165,17 @@ public actor AdvenueEngine {
|
|
|
163
165
|
/// Identity attached to every subsequent event. Not persisted: it is
|
|
164
166
|
/// re-resolved each launch, because ATT status and the vendor id can both
|
|
165
167
|
/// change between them.
|
|
166
|
-
|
|
168
|
+
/// B2: rıza bayrağı — her olaya işlenir, persist edilmez (kimlik gibi).
|
|
169
|
+
private var limitAdTracking: Bool?
|
|
170
|
+
|
|
171
|
+
public func setIdentity(
|
|
172
|
+
idfa: String?, vendorId: String?, appInstanceId: String?,
|
|
173
|
+
limitAdTracking: Bool? = nil
|
|
174
|
+
) {
|
|
167
175
|
self.idfa = idfa
|
|
168
176
|
self.vendorId = vendorId
|
|
169
177
|
self.appInstanceId = appInstanceId
|
|
178
|
+
self.limitAdTracking = limitAdTracking
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
/// The Firebase App Instance ID alone. Separate from `setIdentity` because it
|
|
@@ -206,6 +215,8 @@ public actor AdvenueEngine {
|
|
|
206
215
|
event.sdkVersion = config.sdkVersion
|
|
207
216
|
event.customerUserId = customerUserId
|
|
208
217
|
event.idfa = idfa
|
|
218
|
+
// B2: rıza bayrağı install olayına da işlenir.
|
|
219
|
+
event.limitAdTracking = limitAdTracking
|
|
209
220
|
event.vendorId = vendorId
|
|
210
221
|
event.appInstanceId = appInstanceId
|
|
211
222
|
event.consent = consentData
|
|
@@ -347,6 +358,11 @@ public actor AdvenueEngine {
|
|
|
347
358
|
|
|
348
359
|
|
|
349
360
|
public func setUserId(_ id: String?) {
|
|
361
|
+
// B6: 128 üstü id tüm batch'i 400'e düşürür. Reddet, persist etme.
|
|
362
|
+
if let id, id.count > 128 {
|
|
363
|
+
onError("account.setUserId:id_too_long", IngestError(status: 400))
|
|
364
|
+
return
|
|
365
|
+
}
|
|
350
366
|
customerUserId = id
|
|
351
367
|
if let id {
|
|
352
368
|
store.set(id, forKey: USER_ID_KEY)
|
|
@@ -368,7 +384,9 @@ public actor AdvenueEngine {
|
|
|
368
384
|
pushToken = nil
|
|
369
385
|
pushProvider = nil
|
|
370
386
|
customerUserId = nil
|
|
371
|
-
|
|
387
|
+
// QUEUE_KEY last: queue.clear() above persists an empty blob synchronously,
|
|
388
|
+
// and `forgotten` stops every later enqueue, so nothing rewrites it.
|
|
389
|
+
for key in [CONSENT_KEY, CONSENT_DATA_KEY, SESSION_STATE_KEY, USER_ID_KEY, QUEUE_KEY] {
|
|
372
390
|
store.removeObject(forKey: key)
|
|
373
391
|
}
|
|
374
392
|
}
|
|
@@ -477,7 +495,7 @@ public enum Command: Sendable {
|
|
|
477
495
|
case foreground
|
|
478
496
|
case background
|
|
479
497
|
case flush
|
|
480
|
-
case setIdentity(idfa: String?, vendorId: String?, appInstanceId: String?)
|
|
498
|
+
case setIdentity(idfa: String?, vendorId: String?, appInstanceId: String?, limitAdTracking: Bool? = nil)
|
|
481
499
|
case setAppInstanceId(String?)
|
|
482
500
|
case setConsentData(Consent?)
|
|
483
501
|
case setPushToken(token: String?, provider: String?)
|
|
@@ -543,8 +561,10 @@ public final class CommandPipe: @unchecked Sendable {
|
|
|
543
561
|
await engine.notifyBackground()
|
|
544
562
|
case .flush:
|
|
545
563
|
await engine.flush()
|
|
546
|
-
case .setIdentity(let idfa, let vendorId, let appInstanceId):
|
|
547
|
-
await engine.setIdentity(
|
|
564
|
+
case .setIdentity(let idfa, let vendorId, let appInstanceId, let limitAdTracking):
|
|
565
|
+
await engine.setIdentity(
|
|
566
|
+
idfa: idfa, vendorId: vendorId, appInstanceId: appInstanceId,
|
|
567
|
+
limitAdTracking: limitAdTracking)
|
|
548
568
|
case .setAppInstanceId(let id):
|
|
549
569
|
await engine.setAppInstanceId(id)
|
|
550
570
|
case .trackInstall(let token, let attestation, let challenge):
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// B4: dosya destekli anahtar-değer deposu. Caches dizini iCloud/yedek
|
|
4
|
+
/// kapsamı dışındadır — kuyruk blob'u (reklam kimlikli olay gövdeleri)
|
|
5
|
+
/// yedek imajına sızmaz. Linux taşınabilirliği için yalnızca Foundation.
|
|
6
|
+
public struct CacheFileStore: KeyValueStore, @unchecked Sendable {
|
|
7
|
+
private let directory: URL
|
|
8
|
+
private let fileManager = FileManager.default
|
|
9
|
+
|
|
10
|
+
/// - Parameter directory: Kök dizin. Üretimde Caches/advenue kullanılır;
|
|
11
|
+
/// testler geçici dizin verir.
|
|
12
|
+
public init(directory: URL) {
|
|
13
|
+
self.directory = directory
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static func caches() -> CacheFileStore? {
|
|
17
|
+
guard
|
|
18
|
+
let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
|
19
|
+
else { return nil }
|
|
20
|
+
let directory = base.appendingPathComponent("advenue", isDirectory: true)
|
|
21
|
+
try? FileManager.default.createDirectory(
|
|
22
|
+
at: directory, withIntermediateDirectories: true)
|
|
23
|
+
return CacheFileStore(directory: directory)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private func file(forKey key: String) -> URL {
|
|
27
|
+
// Anahtar dosya adı olur — boş/kök kaçışı engellenir.
|
|
28
|
+
let safe = key.replacingOccurrences(of: "/", with: "_")
|
|
29
|
+
return directory.appendingPathComponent(safe, isDirectory: false)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func string(forKey key: String) -> String? {
|
|
33
|
+
try? String(contentsOf: file(forKey: key), encoding: .utf8)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public func set(_ value: String, forKey key: String) {
|
|
37
|
+
try? fileManager.createDirectory(
|
|
38
|
+
at: directory, withIntermediateDirectories: true)
|
|
39
|
+
try? value.write(to: file(forKey: key), atomically: true, encoding: .utf8)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func removeObject(forKey key: String) {
|
|
43
|
+
try? fileManager.removeItem(at: file(forKey: key))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// B4: bileşik depo — kuyruk blob'u dosyalara (yedek dışı), geri kalan
|
|
48
|
+
/// UserDefaults'a. Android `CompositeStore` ile aynı yönlendirme.
|
|
49
|
+
/// Eski UserDefaults anahtarı ilk okumada migrate edilir (RN uyumluluğu).
|
|
50
|
+
public struct CompositeStore: KeyValueStore, @unchecked Sendable {
|
|
51
|
+
private let defaults: UserDefaultsStore
|
|
52
|
+
private let files: CacheFileStore
|
|
53
|
+
private let fileKeys: Set<String>
|
|
54
|
+
|
|
55
|
+
public init(
|
|
56
|
+
defaults: UserDefaultsStore = UserDefaultsStore(),
|
|
57
|
+
files: CacheFileStore,
|
|
58
|
+
fileKeys: Set<String> = [QUEUE_KEY]
|
|
59
|
+
) {
|
|
60
|
+
self.defaults = defaults
|
|
61
|
+
self.files = files
|
|
62
|
+
self.fileKeys = fileKeys
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public func string(forKey key: String) -> String? {
|
|
66
|
+
guard fileKeys.contains(key) else { return defaults.string(forKey: key) }
|
|
67
|
+
if let hit = files.string(forKey: key) { return hit }
|
|
68
|
+
// Tek seferlik geriye dönük migrate: RN/önceki sürüm UserDefaults'ta bıraktı.
|
|
69
|
+
guard let legacy = defaults.string(forKey: key) else { return nil }
|
|
70
|
+
files.set(legacy, forKey: key)
|
|
71
|
+
defaults.removeObject(forKey: key)
|
|
72
|
+
return legacy
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public func set(_ value: String, forKey key: String) {
|
|
76
|
+
if fileKeys.contains(key) {
|
|
77
|
+
files.set(value, forKey: key)
|
|
78
|
+
} else {
|
|
79
|
+
defaults.set(value, forKey: key)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public func removeObject(forKey key: String) {
|
|
84
|
+
if fileKeys.contains(key) {
|
|
85
|
+
files.removeObject(forKey: key)
|
|
86
|
+
} else {
|
|
87
|
+
defaults.removeObject(forKey: key)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -4,6 +4,18 @@ import Foundation
|
|
|
4
4
|
import UIKit
|
|
5
5
|
#endif
|
|
6
6
|
|
|
7
|
+
/// The app's own version (`CFBundleShortVersionString`) for every event's
|
|
8
|
+
/// top-level `appVersion`. Resolved by the SDK, never taken from the app: the
|
|
9
|
+
/// React Native 0.x layer filled it this way, and 1.0 relying on the config
|
|
10
|
+
/// instead left every event unversioned (2026-09-19). Nil when the bundle has
|
|
11
|
+
/// no version.
|
|
12
|
+
public func readAppVersion() -> String? {
|
|
13
|
+
guard let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
|
14
|
+
!short.isEmpty
|
|
15
|
+
else { return nil }
|
|
16
|
+
return short
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/// Device metadata for Meta CAPI `extinfo`.
|
|
8
20
|
///
|
|
9
21
|
/// Best-effort per field: a field is **omitted**, never defaulted, when its
|
|
@@ -13,11 +13,14 @@ public struct Enrichment: Sendable, Equatable {
|
|
|
13
13
|
public var vendorId: String?
|
|
14
14
|
public var appInstanceId: String?
|
|
15
15
|
public var adservicesToken: String?
|
|
16
|
+
/// B2: reklam takibi sınırlama bayrağı — düşürülüyordu, artık olaya işlenir.
|
|
17
|
+
public var limitAdTracking: Bool
|
|
16
18
|
|
|
17
19
|
public init(
|
|
18
20
|
idfa: String? = nil, vendorId: String? = nil,
|
|
19
21
|
appInstanceId: String? = nil, adservicesToken: String? = nil,
|
|
20
|
-
attestation: AttestationResult? = nil, attestationChallenge: String? = nil
|
|
22
|
+
attestation: AttestationResult? = nil, attestationChallenge: String? = nil,
|
|
23
|
+
limitAdTracking: Bool = false
|
|
21
24
|
) {
|
|
22
25
|
self.attestation = attestation
|
|
23
26
|
self.attestationChallenge = attestationChallenge
|
|
@@ -25,6 +28,7 @@ public struct Enrichment: Sendable, Equatable {
|
|
|
25
28
|
self.vendorId = vendorId
|
|
26
29
|
self.appInstanceId = appInstanceId
|
|
27
30
|
self.adservicesToken = adservicesToken
|
|
31
|
+
self.limitAdTracking = limitAdTracking
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -34,6 +38,9 @@ public struct Enrichment: Sendable, Equatable {
|
|
|
34
38
|
public struct EnrichmentSources: Sendable {
|
|
35
39
|
public var searchAdsToken: @Sendable () async -> String?
|
|
36
40
|
public var advertisingId: @Sendable () -> (idfa: String?, vendorId: String?)
|
|
41
|
+
/// B2: ATT sınırlama bayrağı. Ayrı kapanış — advertisingId demetini
|
|
42
|
+
/// değiştirmek mevcut sahte kaynakları kırardı.
|
|
43
|
+
public var limitAdTracking: @Sendable () -> Bool
|
|
37
44
|
public var appInstanceId: @Sendable () async -> String?
|
|
38
45
|
/// Returns the attested challenge and its result, or nil. Every failure path
|
|
39
46
|
/// — unsupported device, challenge fetch, attest — yields nil, because an
|
|
@@ -46,12 +53,14 @@ public struct EnrichmentSources: Sendable {
|
|
|
46
53
|
appInstanceId: @escaping @Sendable () async -> String?,
|
|
47
54
|
attestation: @escaping @Sendable () async -> (challenge: String, result: AttestationResult)? = {
|
|
48
55
|
nil
|
|
49
|
-
}
|
|
56
|
+
},
|
|
57
|
+
limitAdTracking: @escaping @Sendable () -> Bool = { false }
|
|
50
58
|
) {
|
|
51
59
|
self.searchAdsToken = searchAdsToken
|
|
52
60
|
self.advertisingId = advertisingId
|
|
53
61
|
self.appInstanceId = appInstanceId
|
|
54
62
|
self.attestation = attestation
|
|
63
|
+
self.limitAdTracking = limitAdTracking
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/// Production wiring. `AdvertisingIdentity.advertisingId` already returns nil
|
|
@@ -68,7 +77,8 @@ public struct EnrichmentSources: Sendable {
|
|
|
68
77
|
return (idfa: identity.advertisingId, vendorId: identity.vendorId)
|
|
69
78
|
},
|
|
70
79
|
appInstanceId: { await appInstanceIdProvider?() },
|
|
71
|
-
attestation: { await attestation?() }
|
|
80
|
+
attestation: { await attestation?() },
|
|
81
|
+
limitAdTracking: { AdvertisingIdentity().limitAdTracking })
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -85,7 +95,9 @@ public func collectEnrichment(
|
|
|
85
95
|
) async -> Enrichment {
|
|
86
96
|
// Synchronous reads; no reason to race them.
|
|
87
97
|
let ids = sources.advertisingId()
|
|
88
|
-
var result = Enrichment(
|
|
98
|
+
var result = Enrichment(
|
|
99
|
+
idfa: ids.idfa, vendorId: ids.vendorId,
|
|
100
|
+
limitAdTracking: sources.limitAdTracking())
|
|
89
101
|
|
|
90
102
|
await withTaskGroup(of: (String, String?).self) { group in
|
|
91
103
|
group.addTask { ("adservices", await sources.searchAdsToken()) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@advenue/react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Advenue MMP React Native SDK — attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs && tsup && node scripts/check-dist.mjs",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run --minWorkers=1 --maxWorkers=2 && node --test scripts/vendor-natives.test.mjs && node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs",
|
|
37
|
+
"vendor": "node scripts/vendor-natives.mjs"
|
|
38
|
+
},
|
|
33
39
|
"dependencies": {},
|
|
34
40
|
"peerDependencies": {
|
|
35
41
|
"@expo/config-plugins": ">=8",
|
|
@@ -50,11 +56,5 @@
|
|
|
50
56
|
"typescript": "^5.7.2",
|
|
51
57
|
"vitest": "^2.1.8"
|
|
52
58
|
},
|
|
53
|
-
"react-native": "./dist/index.cjs"
|
|
54
|
-
|
|
55
|
-
"build": "node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs && tsup && node scripts/check-dist.mjs",
|
|
56
|
-
"typecheck": "tsc --noEmit",
|
|
57
|
-
"test": "vitest run --minWorkers=1 --maxWorkers=2 && node --test scripts/vendor-natives.test.mjs && node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs",
|
|
58
|
-
"vendor": "node scripts/vendor-natives.mjs"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
59
|
+
"react-native": "./dist/index.cjs"
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,15 @@ export type {
|
|
|
27
27
|
} from './native-types';
|
|
28
28
|
|
|
29
29
|
/** Options this package used to act on and the native SDKs now decide. */
|
|
30
|
-
|
|
30
|
+
// appVersion joins them: the native SDK resolves it from the package/bundle
|
|
31
|
+
// (as the 0.x JS layer did), so a config value would only shadow the real one.
|
|
32
|
+
const IGNORED_OPTIONS = [
|
|
33
|
+
'mmkv',
|
|
34
|
+
'secureStore',
|
|
35
|
+
'sdkConfigFetcher',
|
|
36
|
+
'tcfDataCollection',
|
|
37
|
+
'appVersion',
|
|
38
|
+
] as const;
|
|
31
39
|
|
|
32
40
|
export interface RNAdvenueConfig extends AdvenueConfig {
|
|
33
41
|
/** Meta app id, for the Meta install referrer on Android. */
|
|
@@ -131,7 +139,6 @@ export const Advenue = {
|
|
|
131
139
|
call('bridgeInitialize', {
|
|
132
140
|
apiKey: config.apiKey,
|
|
133
141
|
endpoint: config.endpoint,
|
|
134
|
-
appVersion: config.appVersion,
|
|
135
142
|
requireConsent: config.requireConsent ?? false,
|
|
136
143
|
batchSize: config.batchSize,
|
|
137
144
|
flushIntervalMs: config.flushIntervalMs,
|
package/src/native-types.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -60,6 +60,11 @@ export interface AdvenueConfig {
|
|
|
60
60
|
apiKey: string;
|
|
61
61
|
endpoint?: string;
|
|
62
62
|
platform?: 'ios' | 'android' | 'web' | string;
|
|
63
|
+
/**
|
|
64
|
+
* @deprecated Ignored: the native SDK resolves the app version from the
|
|
65
|
+
* package/bundle. Passing it reports `config.ignored:appVersion` via
|
|
66
|
+
* `onError`. Removed in the next major.
|
|
67
|
+
*/
|
|
63
68
|
appVersion?: string;
|
|
64
69
|
osVersion?: string;
|
|
65
70
|
/** Gates every event on `setTrackingConsent(true)`. */
|