Skip to content
Merged
39 changes: 33 additions & 6 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
# Migration Guide
This document provides guidance on how to migrate from the old version of the SDK to a newer version.
This document provides guidance on how to migrate from one version of the SDK to a newer version.
It will be updated as new versions are released including deprecations or breaking changes.

# 3.4.0
# 4.0.0

## In-App Forms

As a result of changes summarized below, you may wish to revisit the logic of when you call `registerForInAppForms()`
when upgrading from 3.2.0-3.3.1, particularly if you were registering than once per application session. Consult the
[README](./README.md#in-app-forms) for the latest integration instructions.

### Updated behaviors
- In versions 3.2.0-3.3.1, calling `registerForInAppForms()` functioned like a "fetch" that would check if a form was
available and if yes, display it. Version 4.0.0 changes this behavior so that `registerForInAppForms()` sets up a
persistent listener that will be ready to display a form if and when one is targeted to the current profile.
- For better isolation between the library and host application, a form is now displayed in an Activity overlaid
on the host application, instead of being attached to the view hierarchy of the host's activity.
- A deep link from an In-App Form will now be issued *after* the form has closed, instead of during the close animation in order
to prevent a race condition if the host application expects the form to be closed before handling the deep link.
- To support the native back button, the In-App Forms overlay activity now captures back button input while presented.

### Configurable In-App Form session timeout
- Introduced a configurable session timeout for In-App Forms, which defaults to 60 minutes, as an optional argument to `registerForInAppForms()`.

### New `unregisterFromInAppForms()` method
- Because the `registerForInAppForms()` method now functions as a persistent listener rather than a "fetch",
we've introduced an `unregisterFromInAppForms()` method so you can stop listening for In-App Forms at appropriate times,
such as when a user logs out.

## Push

### Improvements
- A deep link from an in-app form will now be issued *after* the form has closed, instead of during the close animation.
- To facilitate integration, the Klaviyo Push Service manifest entry has been added to our `push-fcm` module.
You no longer have to manually add this to your manifest to register our service. This can be overridden by declaring
your own implementation in your manifest, per the advanced integration guide in the README.

- The Klaviyo Push Service manifest entry has been added to our PushFcm module. You no longer have to manually
add this to your AndroidManifest to register our service. This can be easily overridden by declaring your own implementation
of `KlaviyoPushService` or `FirebaseMessagingService` in the manifest.
### Breaking Changes
- The deprecated extension property `RemoteMessage.smallIcon` has been removed in favor of `RemoteMessage.getSmallIcon()`.

# 3.0.0

Expand Down
116 changes: 88 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,34 @@ The queue is persisted to local storage so that data is not lost if the device i
Once integrated, your marketing team will be able to better understand your app users' needs and
send them timely push notifications via [FCM (Firebase Cloud Messaging)](https://firebase.google.com/docs/cloud-messaging).

## Contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Initialization](#initialization)
- [Profile Identification](#profile-identification)
- [Event Tracking](#event-tracking)
- [Push Notifications](#push-notifications)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Collecting Push Tokens](#collecting-push-tokens)
- [Receiving Push Notifications](#receiving-push-notifications)
- [Rich Push](#rich-push)
- [Tracking Open Events](#tracking-open-events)
- [Silent Push Notifications](#silent-push-notifications)
- [Custom Data](#custom-data)
- [Advanced Setup](#advanced-setup)
- [In-App Forms](#in-app-forms)
- [Prerequisites](#prerequisites-1)
- [Setup](#setup-1)
- [In-App Forms Session Configuration](#in-app-forms-session-configuration)
- [Unregistering from In-App Forms](#unregistering-from-in-app-forms)
- [Deep Linking](#deep-linking)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [License](#license)
- [Code Documentation](#code-documentation)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TOC because the other SDKs have one. left a few low priority headers off for brevity.

## Requirements

- Kotlin **1.8.0** or later
Expand Down Expand Up @@ -317,10 +345,10 @@ additional information from the push payload and handle it appropriately - for i
processing, logging analytics events, or dynamically updating app content.

### Advanced Setup
If you'd prefer to have your own implementation of `FirebaseMessagingService`,
follow the FCM setup docs including referencing your own service class in the manifest. We've included
the default Klaviyo Push Service in our SDK Manifest (which will be merged into the final APK), but
if you'd like to override this you can.
If you'd prefer to have your own implementation of `FirebaseMessagingService`, e.g. to handle push messages from
multiple sources, follow the FCM setup docs including referencing your own service class in the manifest.
We include the default Klaviyo Push Service in our SDK Manifest (which will be merged into the final APK),
but if you'd like to override this you can.

```xml
<!-- AndroidManifest.xml -->
Expand Down Expand Up @@ -430,20 +458,38 @@ extensions such as `import com.klaviyo.pushFcm.KlaviyoRemoteMessage.body` to acc
We also provide an `Intent.appendKlaviyoExtras(RemoteMessage)` extension method, which attaches the data to your
notification intent that the Klaviyo SDK requires in order to track opens when you call `Klaviyo.handlePush(intent)`.

## In App Forms
## In-App Forms

[In-app forms](https://help.klaviyo.com/hc/en-us/articles/34567685177883) are messages displayed to mobile app
users while they are actively using your app. You can create new in-app forms in a drag-and-drop editor in the
[In-App Forms](https://help.klaviyo.com/hc/en-us/articles/34567685177883) are messages displayed to mobile app
users while they are actively using your app. You can create new In-App Forms in a drag-and-drop editor in the
Sign-Up Forms tab in Klaviyo. Follow the instructions in this section to integrate forms with your app. The SDK will
display forms according to their targeting and behavior settings and collect delivery and engagement analytics automatically.

Beginning with version 4.0.0, In-App Forms supports advanced targeting and segmentation. In your Klaviyo account,
you can configure forms to target or exclude specific lists or segments, and the form will only be shown to users
matching those criteria, based on their profile identifiers set via the `analytics` package.

### Prerequisites
- Version 3.2.0 or higher
- Klaviyo `analytics` and `forms` packages
- If you expect to use deep links in forms, see the [deep linking](#deep-linking) section below.
- We strongly recommend using the latest version of the SDK to ensure compatibility with the latest In-App Forms features.
The minimum SDK version supporting In-App Forms is `3.2.0`, and a feature matrix is provided below. Forms that leverage
unsupported features will not appear in your app until you update to a version that supports those features.
- Please read the [migration guide](MIGRATION_GUIDE.md) if you are upgrading from 3.2.0-3.3.1
to understand changes to In-App Forms behavior.

| Feature | Minimum SDK Version |
|--------------------|---------------------|
| Basic In-App Forms | 3.2.0+ |
| Time Delay | 4.0.0 |
| Audience Targeting | 4.0.0 |

### Setup
To display in-app forms add the following code to your application.
To begin, call `Klaviyo.registerForInAppForms()` after initializing the SDK with your public API key.
Once registered, the SDK may launch an overlay Activity at any time to present a form according to its targeting and
behavior settings configured in your Klaviyo account. For the best user experience, we recommend registering after any
splash screen or loading animations have completed. Depending on your app's architecture, this might be in your
`Application.onCreate()` method, or in the `onCreate()` method of your main activity.

```kotlin
import com.klaviyo.analytics.Klaviyo
Expand All @@ -458,27 +504,41 @@ Klaviyo
Klaviyo.registerForInAppForms()
```

### Behavior
#### In-App Forms Session Configuration

Once `registerForInAppForms()` is called, the SDK will load form data for your account and display no more than one form
within 15 seconds, based on form targeting and behavior settings.
A "session" is considered to be a logical unit of user engagement with the app, defined as a series of foreground
interactions that occur within a continuous or near-continuous time window. This is an important concept for In-App Forms,
as we want to ensure that a user will not see the same forms multiple times within a single session.

You can call `registerForInAppForms()` any time after initializing with your company ID to control when and where in
your app's UI a form can appear. It is safe to register multiple times per application session.
The SDK will internally prevent multiple forms appearing at once.
A session will time out after a specified period of inactivity. When a user launches the app, if the time between
the previous interaction with the app and the current one exceeds the specified timeout, we will consider this a new session.

Consider how often you want to register for forms. For example, registering from a lifecycle event is advisable
so that the user has multiple opportunities to see your messaging if they are browsing your app for a prolonged period.
However, be advised the form will be shown as soon as it is ready, so you may still need to condition this based on the
user's context within your application. Future versions of this product will provide more control in this regard.
This timeout has a default value of 3600 seconds (1 hour), but it can be customized. To do so, pass an `InAppFormsConfig`
object to the `registerForInAppForms()` method. For example, to set a session timeout of 30 minutes:

```kotlin
import com.klaviyo.forms.InAppFormsConfig
import kotlin.time.Duration.Companion.minutes

| Callback | Description |
|--------------------------|--------------------------------------------------------------------------------------------------------|
| `Application.onCreate()` | Show a form _only_ upon initial app launch. |
| `Application.onResume()` | Anytime the app is foregrounded, check for forms and show if available. |
| `Activity.onCreate()` | If you only want forms to appear over a specific activity, checks every time this activity is created. |
// e.g. to configure a session timeout of 30 minutes
val config = InAppFormsConfig(
sessionTimeoutDuration = 30.minutes,
)

Klaviyo.registerForInAppForms(config)
```

#### Unregistering from In-App Forms
If at any point you need to prevent the SDK from displaying In-App Forms, e.g. when the user logs out, you may call:

```kotlin
import com.klaviyo.analytics.Klaviyo
import com.klaviyo.forms.unregisterFromInAppForms

Klaviyo.unregisterFromInAppForms()
```

**Note:** At this time, when device orientation changes any currently visible form is closed and will not be re-displayed automatically.
Note that after unregistering, the next call to `registerForInAppForms()` will be considered a new session by the SDK.

## Deep Linking
[Deep Links](https://help.klaviyo.com/hc/en-us/articles/14750403974043) allow you to navigate to a particular
Expand Down Expand Up @@ -512,7 +572,7 @@ and from In-App Forms interactions. There are broadly three steps to implement d

2. Read data from incoming intents:

When a user taps a notification or a deep link in an in-app form, the Klaviyo SDK sends your app an intent containing that link.
When a user taps a notification or a deep link in an In-App Form, the Klaviyo SDK sends your app an intent containing that link.
You can parse the URI from the intent's data property and use it to navigate to the appropriate part of your app.

```kotlin
Expand Down Expand Up @@ -545,7 +605,7 @@ and from In-App Forms interactions. There are broadly three steps to implement d

To perform integration testing, you can send a
[preview push notification](https://help.klaviyo.com/hc/en-us/articles/18011985278875)
containing a deep link from the Klaviyo push editor or use an in-app form that contains a "Go to app screen" action.
containing a deep link from the Klaviyo push editor or use an In-App Form that contains a "Go to app screen" action.

For additional resources on deep linking, refer to
[Android developer documentation](https://developer.android.com/training/app-links/deep-linking).
Expand Down Expand Up @@ -576,7 +636,7 @@ the following metadata tag to your manifest file.
```

#### WebViews Compatibility
Klaviyo's in-app forms are powered by [WebViews](https://developer.android.com/reference/android/webkit/WebView).
Klaviyo's In-App Forms are powered by [WebViews](https://developer.android.com/reference/android/webkit/WebView).
At this time, we require a version of WebView compatible with JavaScript standard ES2015. Older versions will fail
gracefully without displaying a form to the user.

Expand Down
18 changes: 13 additions & 5 deletions sdk/forms/src/main/java/com/klaviyo/forms/InAppForms.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ import com.klaviyo.forms.webview.KlaviyoWebViewClient
import com.klaviyo.forms.webview.WebViewClient

/**
* Load in-app forms data and display a form to the user if applicable based on the forms
* configured in your Klaviyo account. Note [Klaviyo.initialize] must be called first
* Start an In-App Forms session.
*
* This will load forms data and establish ongoing listeners to present a form to the user
* whenever a form is triggered by an event or condition according to the targeting and behavior
* settings configured for forms in your Klaviyo account.
*
* Note: a public API key is required, so [Klaviyo.initialize] must be called first.
* If the API key changes, the session will be re-initialized automatically with the new key.
Comment thread
evan-masseau marked this conversation as resolved.
*
* @param config see [InAppFormsConfig] for configuration options.
*/
@UiThread
fun Klaviyo.registerForInAppForms(
Expand All @@ -43,7 +51,7 @@ fun Klaviyo.registerForInAppForms(
}

/**
* Halts the in-app forms services and observers,
* Halt the In-App Forms services and observers,
* hiding any currently displayed forms and preventing any further forms from being presented.
*/
@UiThread
Expand All @@ -53,13 +61,13 @@ fun Klaviyo.unregisterFromInAppForms() = safeApply {
get<PresentationManager>().dismiss()
get<WebViewClient>().destroyWebView()
} else {
log.warning("Cannot unregister in-app forms, must be registered first.")
log.warning("Cannot unregister In-App Forms, must be registered first.")
}
}
}

/**
* Resets the in-app forms listeners with the current configuration.
* Resets the In-App Forms listeners with the current configuration.
*/
@UiThread
internal fun Klaviyo.reInitializeInAppForms() = safeApply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds

/**
* Configuration for in-app forms
* Configuration for In-App Forms
*
* @param sessionTimeoutDuration Duration of user inactivity after which the form session is terminated.
* Defaults to 1 Hour, must be non-negative. Use 0 to timeout as soon as the app is backgrounded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.klaviyo.core.Registry
import com.klaviyo.forms.bridge.NativeBridge

/**
* Custom WebView that powers the in-app forms experience, running klaviyo.js in its JS engine
* Custom WebView that powers the In-App Forms experience, running klaviyo.js in its JS engine
* to handle forms behavior, triggering, rendering and ultimately displaying the form over the host app.
*/
@SuppressLint("SetJavaScriptEnabled")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.klaviyo.forms.presentation.PresentationManager
import java.io.BufferedReader

/**
* Manages the [KlaviyoWebView] instance that powers in-app forms behavior, triggering, rendering and display,
* Manages the [KlaviyoWebView] instance that powers In-App Forms behavior, triggering, rendering and display,
* and handles all its [android.webkit.WebViewClient] delegate methods, and loading of klaviyo.js
*/
internal class KlaviyoWebViewClient() : AndroidWebViewClient(), WebViewClient, JavaScriptEvaluator {
Expand All @@ -41,7 +41,7 @@ internal class KlaviyoWebViewClient() : AndroidWebViewClient(), WebViewClient, J

/**
* Initialize a webview instance, with protection against duplication
* and initialize klaviyo.js for in-app forms with handshake data injected in the document head
* and initialize klaviyo.js for In-App Forms with handshake data injected in the document head
*/
override fun initializeWebView(): Unit = webView?.let {
Registry.log.debug("Klaviyo webview is already initialized")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.app.Activity
internal interface WebViewClient {
/**
* Initialize a webview instance, with protection against duplication
* and initialize klaviyo.js for in-app forms with handshake data injected in the document head
* and initialize klaviyo.js for In-App Forms with handshake data injected in the document head
*/
fun initializeWebView()

Expand Down