Skip to content

Commit c120ac0

Browse files
authored
Version 4.4.0 (#436)
2 parents d9ace10 + fd5734d commit c120ac0

39 files changed

Lines changed: 4734 additions & 169 deletions

README.md

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
2828
- [Collecting Push Tokens](#collecting-push-tokens)
2929
- [Receiving Push Notifications](#receiving-push-notifications)
3030
- [Rich Push](#rich-push)
31+
- [Push Action Buttons](#push-action-buttons)
3132
- [Tracking Open Events](#tracking-open-events)
3233
- [Silent Push Notifications](#silent-push-notifications)
3334
- [Custom Data](#custom-data)
@@ -37,6 +38,7 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
3738
- [Setup](#setup-1)
3839
- [In-App Forms Session Configuration](#in-app-forms-session-configuration)
3940
- [Unregistering from In-App Forms](#unregistering-from-in-app-forms)
41+
- [Monitoring Form Lifecycle Events](#monitoring-form-lifecycle-events)
4042
- [Geofencing](#geofencing)
4143
- [Setup](#setup-2)
4244
- [Requesting Permissions](#requesting-permissions)
@@ -103,10 +105,10 @@ The sample app serves as both a reference implementation and a testing tool for
103105
```kotlin
104106
// build.gradle.kts
105107
dependencies {
106-
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:4.3.2")
107-
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:4.3.2")
108-
implementation("com.github.klaviyo.klaviyo-android-sdk:forms:4.3.2")
109-
implementation("com.github.klaviyo.klaviyo-android-sdk:location:4.3.2")
108+
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:4.4.0")
109+
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:4.4.0")
110+
implementation("com.github.klaviyo.klaviyo-android-sdk:forms:4.4.0")
111+
implementation("com.github.klaviyo.klaviyo-android-sdk:location:4.4.0")
110112
}
111113
```
112114
</details>
@@ -117,10 +119,10 @@ The sample app serves as both a reference implementation and a testing tool for
117119
```groovy
118120
// build.gradle
119121
dependencies {
120-
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:4.3.2"
121-
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:4.3.2"
122-
implementation "com.github.klaviyo.klaviyo-android-sdk:forms:4.3.2"
123-
implementation "com.github.klaviyo.klaviyo-android-sdk:location:4.3.2"
122+
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:4.4.0"
123+
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:4.4.0"
124+
implementation "com.github.klaviyo.klaviyo-android-sdk:forms:4.4.0"
125+
implementation "com.github.klaviyo.klaviyo-android-sdk:location:4.4.0"
124126
}
125127
```
126128
</details>
@@ -479,6 +481,11 @@ attaching it to the notification is handled within `KlaviyoPushService`. If an i
479481
(e.g. if the device has a poor network connection) the notification will be displayed without an image
480482
after the download times out.
481483

484+
#### Push Action Buttons
485+
[Push Action Buttons](https://help.klaviyo.com/hc/en-us/article/46285872166683) provide the ability to add clickable buttons to
486+
push notification messages. These buttons can show custom text, and, when clicked, deep link or open your app.
487+
A notification can include up to 3 buttons. No additional SDK setup is required.
488+
482489
#### Tracking Open Events
483490
To track push notification opens, you must call `Klaviyo.handlePush(intent)` when your app is launched from an intent.
484491
This method will check if the app was opened from a notification originating from Klaviyo and if so, create an
@@ -695,6 +702,7 @@ See the table below to understand available features by SDK version.
695702
| Time Delay | 4.0.0 |
696703
| Audience Targeting | 4.0.0 |
697704
| Event Triggers | 4.1.0 |
705+
| Form Lifecycle Hooks | 4.4.0 |
698706

699707
### Setup
700708
To begin, call `Klaviyo.registerForInAppForms()` after initializing the SDK with your public API key.
@@ -787,6 +795,76 @@ object to the `registerForInAppForms()` method. For example, to set a session ti
787795

788796
**Note:** After unregistering, the next call to `registerForInAppForms()` will be considered a new session by the SDK.
789797

798+
### Monitoring Form Lifecycle Events
799+
800+
> Form lifecycle events are available in SDK version 4.4.0 and higher.
801+
802+
You can register a handler to receive callbacks whenever a form is shown, dismissed, or a CTA button is tapped.
803+
This is useful for forwarding engagement data to a third-party analytics platform such as Amplitude, Segment, or Mixpanel.
804+
805+
The handler is invoked on the **main thread**, so avoid performing long-running or blocking work inside it.
806+
807+
<details open>
808+
<summary>Kotlin</summary>
809+
810+
```kotlin
811+
import com.klaviyo.analytics.Klaviyo
812+
import com.klaviyo.forms.FormLifecycleEvent.FormCtaClicked
813+
import com.klaviyo.forms.FormLifecycleEvent.FormDismissed
814+
import com.klaviyo.forms.FormLifecycleEvent.FormShown
815+
import com.klaviyo.forms.registerFormLifecycleHandler
816+
import com.klaviyo.forms.unregisterFormLifecycleHandler
817+
818+
Klaviyo.registerFormLifecycleHandler { event ->
819+
when (event) {
820+
is FormShown -> {
821+
// e.g. myAnalytics.track("Form Shown", mapOf("formId" to event.formId, "formName" to event.formName))
822+
}
823+
is FormDismissed -> {
824+
// e.g. myAnalytics.track("Form Dismissed", mapOf("formId" to event.formId, "formName" to event.formName))
825+
}
826+
is FormCtaClicked -> {
827+
// e.g. myAnalytics.track("Form CTA Clicked", mapOf(
828+
// "formId" to event.formId,
829+
// "formName" to event.formName,
830+
// "buttonLabel" to event.buttonLabel,
831+
// "deepLinkUrl" to event.deepLinkUrl.toString()
832+
// ))
833+
}
834+
}
835+
}
836+
837+
// To stop receiving events, unregister the handler
838+
Klaviyo.unregisterFormLifecycleHandler()
839+
```
840+
</details>
841+
842+
<details>
843+
<summary>Java</summary>
844+
845+
```java
846+
import com.klaviyo.forms.FormLifecycleEvent;
847+
import com.klaviyo.forms.KlaviyoForms;
848+
849+
KlaviyoForms.registerFormLifecycleHandler(event -> {
850+
if (event instanceof FormLifecycleEvent.FormShown shown) {
851+
// e.g. myAnalytics.track("Form Shown", ...)
852+
} else if (event instanceof FormLifecycleEvent.FormDismissed dismissed) {
853+
// e.g. myAnalytics.track("Form Dismissed", ...)
854+
} else if (event instanceof FormLifecycleEvent.FormCtaClicked ctaClicked) {
855+
// e.g. myAnalytics.track("Form CTA Clicked", ...)
856+
}
857+
});
858+
859+
// To stop receiving events, unregister the handler
860+
KlaviyoForms.unregisterFormLifecycleHandler();
861+
```
862+
</details>
863+
864+
Registering a lifecycle handler is optional and does not affect normal form behavior — forms are displayed and dismissed
865+
regardless of whether a handler is registered. Only one handler can be registered at a time; calling
866+
`registerFormLifecycleHandler` again replaces the previous registration.
867+
790868
## Geofencing
791869

792870
[Geofencing](https://help.klaviyo.com/hc/en-us/articles/45194892526747) allows you to trigger events when users enter or exit geographic regions defined in your Klaviyo account.

docs/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
<!-- Redirect to latest version -->
2-
<meta HTTP-EQUIV="REFRESH" content="0; url=./4.3.2/index.html">
2+
<meta HTTP-EQUIV="REFRESH" content="0; url=./4.4.0/index.html">

sample/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ dependencies {
133133

134134
// Other app and testing dependencies (refreshVersions syntax)
135135
implementation(platform(AndroidX.compose.bom))
136-
implementation(AndroidX.core.ktx)
136+
implementation("androidx.core:core-ktx:1.17.0")
137137
implementation(AndroidX.lifecycle.runtime.ktx)
138138
implementation(AndroidX.lifecycle.viewModelCompose)
139139
implementation(AndroidX.activity.compose)

sample/src/main/java/com/klaviyo/sample/SampleApplication.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import android.app.Application
44
import android.content.Context
55
import android.widget.Toast
66
import com.klaviyo.analytics.Klaviyo
7+
import com.klaviyo.core.Registry
8+
import com.klaviyo.forms.FormLifecycleEvent.FormCtaClicked
9+
import com.klaviyo.forms.FormLifecycleEvent.FormDismissed
10+
import com.klaviyo.forms.FormLifecycleEvent.FormShown
711
import com.klaviyo.forms.registerForInAppForms
12+
import com.klaviyo.forms.registerFormLifecycleHandler
813
import com.klaviyo.location.registerGeofencing
914

1015
class SampleApplication : Application() {
@@ -22,6 +27,27 @@ class SampleApplication : Application() {
2227
// If not using a deep link handler, Klaviyo will send an Intent to your app with the deep link in intent.data
2328
showToast("Deep link to: $uri")
2429
}
30+
.registerFormLifecycleHandler { event ->
31+
// OPTIONAL SETUP NOTE: Register a callback to receive form lifecycle events
32+
// This allows you to track when forms are shown, dismissed, or when CTAs are clicked
33+
when (event) {
34+
is FormShown -> {
35+
Registry.log.debug(
36+
"Form Lifecycle: ${event.formName} (${event.formId}) Shown"
37+
)
38+
}
39+
is FormDismissed -> {
40+
Registry.log.debug(
41+
"Form Lifecycle: ${event.formName} (${event.formId}) Dismissed"
42+
)
43+
}
44+
is FormCtaClicked -> {
45+
Registry.log.debug(
46+
"Form Lifecycle: CTA ${event.buttonLabel} -> ${event.deepLinkUrl} from ${event.formName} (${event.formId})"
47+
)
48+
}
49+
}
50+
}
2551
}
2652
}
2753

sdk/analytics/src/main/java/com/klaviyo/analytics/Klaviyo.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Application
44
import android.content.Context
55
import android.content.Intent
66
import android.net.Uri
7+
import androidx.core.app.NotificationManagerCompat
78
import androidx.core.net.toUri
89
import com.klaviyo.analytics.linking.DeepLinkHandler
910
import com.klaviyo.analytics.linking.DeepLinking
@@ -17,6 +18,7 @@ import com.klaviyo.analytics.networking.KlaviyoApiClient
1718
import com.klaviyo.analytics.state.KlaviyoState
1819
import com.klaviyo.analytics.state.State
1920
import com.klaviyo.analytics.state.StateSideEffects
21+
import com.klaviyo.core.Constants
2022
import com.klaviyo.core.Constants.KEY_VALUE_PAIRS
2123
import com.klaviyo.core.Constants.PACKAGE_PREFIX
2224
import com.klaviyo.core.Constants.TRACKING_PARAMETER
@@ -333,6 +335,15 @@ object Klaviyo {
333335

334336
// Not using createEvent here to avoid nested safeApply calls
335337
Registry.get<State>().createEvent(event, Registry.get<State>().getAsProfile())
338+
}?.safeApply {
339+
// Dismiss the notification if opened via an action button.
340+
// Body taps are handled by setAutoCancel(true) on the notification builder,
341+
// but action button taps don't trigger auto-cancel (standard Android behavior).
342+
intent?.getStringExtra(Constants.NOTIFICATION_TAG_EXTRA)?.let { tag ->
343+
NotificationManagerCompat
344+
.from(Registry.config.applicationContext)
345+
.cancel(tag, Constants.NOTIFICATION_ID)
346+
}
336347
}?.safeApply {
337348
// If a Klaviyo notification is deep linked, invoke the developer's deep link handler
338349
// if registered. If not, do nothing. The host already received the appropriate intent.

sdk/core/src/main/java/com/klaviyo/core/Constants.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,20 @@ object Constants {
1818
* Klaviyo push messages contain metadata to associate an event with its original transmission
1919
*/
2020
const val TRACKING_PARAMETER = "_k"
21+
22+
/**
23+
* Intent extra key for the notification tag, used to dismiss the notification
24+
* when an action button is tapped and [handlePush] processes the intent.
25+
*
26+
* Uses [INTERNAL_PREFIX] instead of [PACKAGE_PREFIX] to avoid being swept into
27+
* analytics event properties by [appendKlaviyoExtras].
28+
*/
29+
private const val INTERNAL_PREFIX = "_klaviyo."
30+
const val NOTIFICATION_TAG_EXTRA = INTERNAL_PREFIX + "notification_tag"
31+
32+
/**
33+
* Fixed notification ID used in all notify/cancel calls.
34+
* Notifications are uniquely identified by their string tag, not this ID.
35+
*/
36+
const val NOTIFICATION_ID = 0
2137
}

sdk/core/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<resources>
33
<!-- Please do not modify / override these values, they are critical for internal versioning -->
44
<string name="klaviyo_sdk_name_override">android</string>
5-
<string name="klaviyo_sdk_version_override">4.3.2</string>
5+
<string name="klaviyo_sdk_version_override">4.4.0</string>
66
<string name="klaviyo_sdk_plugin_name_override"/>
77
<string name="klaviyo_sdk_plugin_version_override"/>
88
</resources>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.klaviyo.forms
2+
3+
import android.net.Uri
4+
5+
/**
6+
* Represents a lifecycle event of an in-app form, carrying contextual metadata
7+
* about the form and event-specific data.
8+
*
9+
* Use [formId] and [formName] to identify the form associated with any event.
10+
* For CTA-specific data, match on [FormCtaClicked] to access [FormCtaClicked.buttonLabel]
11+
* and [FormCtaClicked.deepLinkUrl].
12+
*/
13+
sealed interface FormLifecycleEvent {
14+
/**
15+
* The form ID of the form associated with this event.
16+
*/
17+
val formId: String
18+
19+
/**
20+
* The display name of the form associated with this event.
21+
*/
22+
val formName: String
23+
24+
/**
25+
* Triggered when a form is shown to the user.
26+
*
27+
* Fired after the SDK has initiated form presentation.
28+
*/
29+
data class FormShown(
30+
override val formId: String,
31+
override val formName: String
32+
) : FormLifecycleEvent
33+
34+
/**
35+
* Triggered when a form is dismissed by the user.
36+
*
37+
* Fired after the SDK has initiated form dismissal. Fires for
38+
* user-initiated dismissals (e.g. tapping outside, close button).
39+
* Does not fire when the SDK tears down the form internally
40+
* (session timeouts, aborts).
41+
*/
42+
data class FormDismissed(
43+
override val formId: String,
44+
override val formName: String
45+
) : FormLifecycleEvent
46+
47+
/**
48+
* Triggered when a user taps a call-to-action (CTA) button in a form
49+
* that has a deep link URL configured.
50+
*
51+
* Fired after the SDK has initiated deep link navigation. Not emitted
52+
* if no deep link URL is configured for the CTA.
53+
*
54+
* @property buttonLabel The text label of the CTA button.
55+
* @property deepLinkUrl The deep link URI configured for the CTA.
56+
*/
57+
data class FormCtaClicked(
58+
override val formId: String,
59+
override val formName: String,
60+
val buttonLabel: String,
61+
val deepLinkUrl: Uri
62+
) : FormLifecycleEvent
63+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.klaviyo.forms
2+
3+
/**
4+
* Functional interface for handling form lifecycle events.
5+
*
6+
* Implement this interface to receive notifications when in-app form lifecycle events occur.
7+
*
8+
* **Threading:** The handler is always invoked on the main thread.
9+
* Lifecycle callbacks fire after the SDK has already initiated the corresponding
10+
* action (presentation, dismissal, or deep link navigation). Avoid performing
11+
* long-running or blocking work in this handler, as it runs on the main thread.
12+
*
13+
* Example usage:
14+
* ```
15+
* Klaviyo.registerFormLifecycleHandler { event ->
16+
* when (event) {
17+
* is FormLifecycleEvent.FormShown -> Log.d("Forms", "Form shown: ${event.formId}")
18+
* is FormLifecycleEvent.FormDismissed -> Log.d("Forms", "Form dismissed: ${event.formId}")
19+
* is FormLifecycleEvent.FormCtaClicked -> Log.d("Forms", "CTA: ${event.buttonLabel}")
20+
* }
21+
* }
22+
* ```
23+
*/
24+
fun interface FormLifecycleHandler {
25+
/**
26+
* Called when a form lifecycle event occurs.
27+
*
28+
* @param event The lifecycle event, containing form metadata and event-specific data.
29+
*/
30+
fun onFormLifecycleEvent(event: FormLifecycleEvent)
31+
}

0 commit comments

Comments
 (0)