@@ -4,12 +4,10 @@ import android.app.Application
44import android.content.Context
55import android.content.Intent
66import android.net.Uri
7- import androidx.core.app.NotificationManagerCompat
87import androidx.core.net.toUri
98import com.klaviyo.analytics.linking.DeepLinkHandler
109import com.klaviyo.analytics.linking.DeepLinking
1110import com.klaviyo.analytics.model.Event
12- import com.klaviyo.analytics.model.EventKey
1311import com.klaviyo.analytics.model.EventMetric
1412import com.klaviyo.analytics.model.Profile
1513import com.klaviyo.analytics.model.ProfileKey
@@ -19,22 +17,19 @@ import com.klaviyo.analytics.state.KlaviyoState
1917import com.klaviyo.analytics.state.State
2018import com.klaviyo.analytics.state.StateSideEffects
2119import com.klaviyo.core.Constants
22- import com.klaviyo.core.Constants.KEY_VALUE_PAIRS
2320import com.klaviyo.core.Constants.PACKAGE_PREFIX
2421import com.klaviyo.core.Constants.TRACKING_PARAMETER
2522import com.klaviyo.core.Operation
23+ import com.klaviyo.core.PushTokenFetcher
2624import com.klaviyo.core.Registry
2725import com.klaviyo.core.config.Config
2826import com.klaviyo.core.config.LifecycleException
2927import com.klaviyo.core.safeApply
3028import com.klaviyo.core.safeCall
31- import com.klaviyo.core.utils.BoundedIdSet
32- import com.klaviyo.core.utils.JSONUtil.toHashMap
3329import com.klaviyo.core.utils.takeIf
3430import java.io.Serializable
3531import java.util.LinkedList
3632import java.util.Queue
37- import org.json.JSONObject
3833
3934/* *
4035 * Public API for the core Klaviyo SDK.
@@ -48,17 +43,6 @@ object Klaviyo {
4843 */
4944 private val preInitQueue: Queue <Operation <Unit >> = LinkedList ()
5045
51- /* *
52- * Push delivery IDs already handled within this process, so a single tap records one
53- * `$opened_push`. See [handlePush] for how entries are matched and added.
54- */
55- private val handledPushDeliveries = BoundedIdSet ()
56-
57- /* *
58- * Key within the `_k` tracking payload whose value uniquely identifies a single push delivery.
59- */
60- private const val PUSH_DELIVERY_KEY = " tm"
61-
6246 /* *
6347 * This method is provided for apps that are unable to register their API key immediately
6448 * on app launch in order enable limited SDK functionality including tracking app lifecycle,
@@ -128,6 +112,43 @@ object Klaviyo {
128112 preInitQueue.poll()?.let { safeCall(null , it) }
129113 }
130114 }
115+
116+ // Optional side effect, kept last so it can never interfere with core initialization
117+ maybeAutoRegisterPushToken()
118+ }
119+
120+ /* *
121+ * Opt-in automatic push token registration (via [Constants.AUTOMATIC_PUSH_TRACKING]): pull the
122+ * current token and forward it to Klaviyo. Called from [initialize] and on each app foreground so
123+ * rotations are picked up. No-op when the flag is off, when forwarding is opted out via
124+ * [Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING], or when `push-fcm` is absent.
125+ */
126+ internal fun maybeAutoRegisterPushToken () {
127+ val autoTrackingEnabled = Registry .config.getManifestBoolean(
128+ Constants .AUTOMATIC_PUSH_TRACKING ,
129+ false
130+ )
131+ // Avoid a second manifest read on the common flag-off path
132+ val tokenForwardingDisabled = autoTrackingEnabled && Registry .config.getManifestBoolean(
133+ Constants .DISABLE_AUTOMATIC_TOKEN_FORWARDING ,
134+ false
135+ )
136+ if (! autoTrackingEnabled || tokenForwardingDisabled) {
137+ Registry .log.verbose(
138+ " Skipping automatic push token registration " +
139+ " (automaticPushTracking=$autoTrackingEnabled , tokenForwardingDisabled=$tokenForwardingDisabled )"
140+ )
141+ return
142+ }
143+
144+ Registry .getOrNull<PushTokenFetcher >()?.also { fetcher ->
145+ Registry .log.debug(" Automatically fetching push token" )
146+ // Contain any fetcher failure so this optional side effect cannot disrupt the caller
147+ runCatching { fetcher.fetchAndSetPushToken() }
148+ .onFailure { Registry .log.warning(" Automatic push token fetch failed" , it) }
149+ } ? : Registry .log.verbose(
150+ " No push token fetcher registered; skipping automatic registration"
151+ )
131152 }
132153
133154 /* *
@@ -337,53 +358,7 @@ object Klaviyo {
337358 */
338359 @JvmStatic
339360 fun handlePush (intent : Intent ? ): Klaviyo {
340- if (intent == null || ! intent.isKlaviyoNotificationIntent) {
341- Registry .log.verbose(" Non-Klaviyo intent ignored" )
342- return this
343- }
344-
345- // Dedup guard: track each push delivery at most once per process. The trampoline calls
346- // handlePush and forwards the same intent to the host, so a leftover manual handlePush call
347- // (or singleTask re-entry) would otherwise double-track one tap. The first call to see a
348- // delivery records it and proceeds; a later call for the same delivery short-circuits — no
349- // event, dismissal, or deep link.
350- val deliveryId = intent.pushDeliveryId
351- if (deliveryId != null && ! handledPushDeliveries.markOnce(deliveryId)) {
352- Registry .log.verbose(" Ignoring duplicate push open" )
353- return this
354- }
355-
356- // Create and enqueue an $opened_push. safeApply(preInitQueue) buffers this for replay if
357- // handlePush runs before initialize(), and guards against unexpected failures.
358- safeApply(preInitQueue) {
359- val state = Registry .get<State >()
360- val event = Event (EventMetric .OPENED_PUSH )
361- event.appendKlaviyoExtras(intent)
362- state.pushToken?.let { event[EventKey .PUSH_TOKEN ] = it }
363- // Not using Klaviyo.createEvent here to avoid nesting safeApply calls
364- state.createEvent(event, state.getAsProfile())
365- }
366-
367- // Dismiss the notification if opened via an action button. Body taps auto-cancel via
368- // setAutoCancel(true) on the builder; action button taps don't (standard Android behavior).
369- safeApply {
370- val notificationTag = intent.getStringExtra(Constants .NOTIFICATION_TAG_EXTRA )
371- if (notificationTag != null ) {
372- NotificationManagerCompat
373- .from(Registry .config.applicationContext)
374- .cancel(notificationTag, Constants .NOTIFICATION_ID )
375- }
376- }
377-
378- // If the notification carries a deep link and a handler is registered, invoke it. Otherwise
379- // do nothing — the host already received the appropriate intent.
380- safeApply {
381- val deepLink = intent.data
382- if (deepLink != null && DeepLinking .isHandlerRegistered) {
383- DeepLinking .handleDeepLink(deepLink)
384- }
385- }
386-
361+ KlaviyoPushOpenHandler .handle(intent, preInitQueue)
387362 return this
388363 }
389364
@@ -456,29 +431,6 @@ object Klaviyo {
456431 @JvmStatic
457432 fun isKlaviyoNotificationIntent (intent : Intent ? ): Boolean = intent.isKlaviyoNotificationIntent
458433
459- /* *
460- * Dedup key for this intent's push delivery, or `null` if none is available. Prefers the `tm`
461- * field of the `_k` payload (a per-delivery ULID on campaign sends), else the SDK-generated
462- * per-notification [Constants.NOTIFICATION_UID_EXTRA]. Both are copied forward to the host's
463- * intent, so a delivery handled by the trampoline and then a leftover manual call resolve to the
464- * same key, while distinct notifications stay distinct.
465- *
466- * Deliberately not the raw `_k`: minus `tm` it is per-message metadata shared across deliveries,
467- * so it would collapse distinct opens.
468- */
469- private val Intent ?.pushDeliveryId: String?
470- get() {
471- val deliveryId = this ?.getStringExtra(PACKAGE_PREFIX + TRACKING_PARAMETER )
472- ?.takeIf { it.isNotEmpty() }
473- ?.let { trackingPayload ->
474- runCatching { JSONObject (trackingPayload).optString(PUSH_DELIVERY_KEY ) }
475- .getOrNull()
476- ?.takeIf { it.isNotEmpty() }
477- }
478- return deliveryId ? : this ?.getStringExtra(Constants .NOTIFICATION_UID_EXTRA )
479- ?.takeIf { it.isNotEmpty() }
480- }
481-
482434 /* *
483435 * Determine if an intent is a Klaviyo click-tracking universal/app link
484436 */
@@ -508,33 +460,4 @@ object Klaviyo {
508460 */
509461 @JvmStatic
510462 fun isKlaviyoUniversalTrackingUri (uri : Uri ): Boolean = uri.isKlaviyoUniversalTrackingUri
511-
512- /* *
513- * Appends Klaviyo extras from an intent to this event, parsing special fields as needed
514- */
515- internal fun Event.appendKlaviyoExtras (intent : Intent ? ) {
516- intent?.extras?.keySet()?.forEach { key ->
517- if (key.contains(PACKAGE_PREFIX )) {
518- val eventKey = EventKey .CUSTOM (key.replace(PACKAGE_PREFIX , " " ))
519- val rawValue = intent.extras?.getString(key, " " ) ? : " "
520- val parsedValue = when (eventKey.name) {
521- KEY_VALUE_PAIRS -> {
522- try {
523- JSONObject (rawValue).toHashMap()
524- } catch (e: Exception ) {
525- Registry .log.warning(
526- " Failed to parse $KEY_VALUE_PAIRS JSON: $rawValue " ,
527- e
528- )
529- rawValue
530- }
531- }
532-
533- else -> rawValue
534- }
535-
536- this [eventKey] = parsedValue
537- }
538- }
539- }
540463}
0 commit comments