Skip to content

Commit

Permalink
Staging branch for next major release 3.0.0 (#161)
Browse files Browse the repository at this point in the history
* Separation of API, State, and Side Effects (#160)

* Separation of API, State and Side Effects. Largely left tests alone except for adding a couple and adjusting setup to accommodate changes.

* Use consistent nullability pattern so we aren't converting between empty string and null with identifiers.
Added proper deserializing of the profile attributes store property

* There is indeed a better way to do that

* Renamed some of the new classes and added new tests for them

* Doc blocks, minor cleanup, naming conventions

* Fix nullability test
Don't need immutable profile to expose attributes, but would be good to expose single attribute getter

* First round of PR comments

* missed another arg label

---------

Co-authored-by: Evan Masseau <>

* Set an alpha version number on this branch in case it gets pulled by a snapshot jitpack build

* Remove deprecated lifecycle property (#156)

Co-authored-by: Evan Masseau <>

* Transition profile identifier keys to internal visibility (#155)

Co-authored-by: Evan Masseau <>

* Add Unregister Push Token Request (#162)

* Publish old value of a state property (#164)

Co-authored-by: Evan Masseau <>

* Rename these properties so it is clear what true and false mean. Fix polarity of how we check background data (#166)

Co-authored-by: Evan Masseau <>

* Added tests for publishing of the old value (#165)

* Added test coverage for publishing old value with change callback

* Broadcast on reset, and tests

* Reset as a cleanup step, not a setup step.

---------

Co-authored-by: Evan Masseau <>

* Trigger Unregister on API Key Change (#163)

* Using token endpoint for profile requests when a push token is present in SDK state (#168)

* making token request if token is present

* minor refactor

* fixed tests

* added some tests

* removed push state when resetting

* updated tests

* fixed side effects tests

* removed unused comment

* updated readme

* finally fixed tests

* Fix some setup/teardown in tests to take care of isolation issues. (#169)

Co-authored-by: Evan Masseau <>

---------

Co-authored-by: Evan C Masseau <5167687+evan-masseau@users.noreply.github.com>

* API Header Fix: Move attempt count increment to before we write request headers into the url connection. (#170)

Co-authored-by: Evan Masseau <>

* setProfileAttribute should accept any Serializable value not just String (#179)

* changing profile attribute to accept serializable

* updating for 3.0 release

* logging incorrectly typed profile attribute

* Concurrent network observer fix (#180)

* using synchronized list for observers

* adding concurreny safe structure to all observer instances

* Refresh versions syntax for dependencies

* removing concurrent suffix

---------

Co-authored-by: Evan Masseau <>

* Retry on 503 (#181)

* Sending 503 through retry logic

* removing version.properties unecessary changes

* fixing http import

* CHNL-6996 Proguard docs and consumer rules (#184)

* Adding consumer rules and updating docs

* progaurd changes

* Introduce a de-dupe mechanism for push notifications (#177)

* Introduce tag support so we have the option to de-dupe push notifications similar to how stock FCM sdk can.

* adding constant id and null-checked notification tag

---------

Co-authored-by: Evan Masseau <>
Co-authored-by: Daniel Peluso <daniel.peluso@klaviyo.com>

* CHNL-3990 Remove identifiers from state if format issues (#186)

* Remove identifiers from state if API reports format errors

* pr comments

* removing extraneous decoder test

* Exposing klaviyo SDK name and version privately (#185)

* added klaviyo sdk name and version

* Update sdk/analytics/src/main/java/com/klaviyo/analytics/DeviceProperties.kt

Co-authored-by: dan-peluso <daniel.peluso@klaviyo.com>

* fixed some formatting

* updated the SDK name to be non lazy

---------

Co-authored-by: dan-peluso <daniel.peluso@klaviyo.com>

* Refactor singleton service initializers (#172)

* Convert SystemClock to class

* These shouldn't have been mocked in this test class, its masking a bug.

* Remove unnecessary, slightly risky, object initializers in core.

* Restore system clock to object -- I take it back, we don't have to get rid of objects, just have to be more particular about using object initializers.

* KLog can be a singleton object, no risk there.

* This simple fix would take care of the issue, but i still don't love KlaviyoApiClient being a singleton object that can hit an unrecoverable exception in its initializer...

* Found a balanced solution for KlaviyoApiClient -- converting this to a class was complicated by the inner class, and besides it works well as a singleton. The initializer was the real problem, so I extract the listeners from the initializer and created a startService method. This is safe to run multiple times if the SDK is re-initialized.

* Prefer this way: we don't need to wrap any of the code in this method body except the methods that are themselves protected anyway

* Forgot to add back starting the service in these tests' setup

* Naming convention things

* First shot at a pre-init buffer for failed operations. Needs tests

* Added test of in-memory slate

* I don't like import *

* Added comments, logging, and wrapped the retry operations in safeCall

* Trivial: move error logging out of exception class. Now that we wrap all SDK functions in safeCall, it is more straightforward to log from there.

* Allow ONLY handlePush to be buffered

* Remove unused test

* keep 3.0 version increment

* don't add this back

* never trust github

* Update versions.properties

---------

Co-authored-by: Evan Masseau <>

* Automatically check push permission status on app resume (#188)

* Protect against double subscriptions
* readme and migration guide updates

---------

Co-authored-by: Evan Masseau <>

* Move API revision to core config property pulled from build property (#190)

Co-authored-by: Evan Masseau <>

* [CHNL-12521] Looking for react-native strings to determine sdk name and version for config (#191)

* resource reading maven attmept

* working resource share attempt

* update log level

* fixing unit tests and working resource sharing

* pr comments

* fixing config

* replace bump version task with xml-based task

* fixing unit test

* fixing file pathing for composite builds

* fixing bump version task

* removing build config field from build.gradle

* using readXml for default config

* removing unused versionFor import

* removing capability change broadcast (#192)

* renaming removals to 'breaking changes' (#193)

* bumping version

---------

Co-authored-by: Evan Masseau <>
Co-authored-by: Kenny Tsui <63658871+kennyklaviyo@users.noreply.github.com>
Co-authored-by: Ajay Subramanya <118314354+ajaysubra@users.noreply.github.com>
Co-authored-by: dan-peluso <daniel.peluso@klaviyo.com>
  • Loading branch information
4 people authored Oct 23, 2024
1 parent c842528 commit 798d03d
Show file tree
Hide file tree
Showing 73 changed files with 2,811 additions and 849 deletions.
13 changes: 13 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
This document provides guidance on how to migrate from the old version of the SDK to a newer version.
It will be updated as new versions are released including deprecations or breaking changes.

# 3.0.0

### Improvements
- The Klaviyo Android SDK now automatically tracks changes to the
user's notification permission whenever the app is opened or resumed.
- Additionally, the SDK will now hold the push token internally after you `resetProfile`
and automatically attach the token to the next profile. This is a change from past behavior where the token
would need to be explicitly set again after resetting.

### Breaking Changes
- The `ProfileKey` options deprecated in `2.3.0` have been removed
- `Klaviyo.lifecycleCallbacks`, deprecated in `2.1.0` has been removed

## 2.3.0 Deprecations
#### Deprecated `ProfileKey` objects pertaining to identifiers
The following `ProfileKey` objects have been deprecated in favor of using the explicit
Expand Down
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
```kotlin
// build.gradle.kts
dependencies {
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.1")
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.1")
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:3.0.0")
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:3.0.0")
}
```
</details>
Expand All @@ -67,8 +67,8 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
```groovy
// build.gradle
dependencies {
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.1"
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.1"
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:3.0.0"
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:3.0.0"
}
```
</details>
Expand Down Expand Up @@ -235,7 +235,7 @@ In order to send push notifications to your users, you must collect their push t
This is done via the `Klaviyo.setPushToken` method, which registers push token and current authorization state
via the [Create Client Push Token API](https://developers.klaviyo.com/en/reference/create_client_push_token).
Once registered in your manifest, `KlaviyoPushService` will receive *new* push tokens via the `onNewToken` method.
We also recommend retrieving the current token on app startup and registering it with Klaviyo SDK.
We also recommend retrieving the latest token value on app startup and registering it with Klaviyo SDK.
Add the following to your `Application.onCreate` method.

```kotlin
Expand All @@ -249,6 +249,9 @@ override fun onCreate(savedInstanceState: Bundle?) {
}
```

*As of version 3.0.0*: After setting a push token, the Klaviyo SDK will automatically track changes to
the user's notification permission whenever the application is opened or resumed from the background.

**Reminder**: `Klaviyo.initialize` is required before using any other Klaviyo SDK functionality, even
if you are only using the SDK for push notifications and not analytics.

Expand All @@ -261,9 +264,9 @@ if you are only using the SDK for push notifications and not analytics.
provide code examples for requesting permission and handling the user's response.

#### Push tokens and multiple profiles
Klaviyo SDK will disassociate the device push token from the current profile whenever it is reset by calling
`setProfile` or `resetProfile`. You should call `setPushToken` again after resetting the currently tracked profile
to explicitly associate the device token to the new profile.
If a new profile was set using `setProfile` or if `resetProfile` was called and a new anonymous
profile was created, the push token will be automatically associated with the new profile without
any additional action (like setting token again) required. This functionality was added in release `3.0.0`.

### Receiving Push Notifications
`KlaviyoPushService` will handle displaying all notifications via the `onMessageReceived` method regardless of
Expand Down Expand Up @@ -487,6 +490,17 @@ the following metadata tag to your manifest file.
</manifest>
```

#### Proguard / R8 Issues

If you notice issues in the release build of your apps, you can try to manually add a couple rules
to your `proguard-rules.pro` to prevent obfuscation:
```
-keep class com.klaviyo.analytics.** { *; }
-keep class com.klaviyo.core.** { *; }
-keep class com.klaviyo.push-fcm.** { *; }
```


## Contributing
See the [contributing guide](.github/CONTRIBUTING.md) to learn how to contribute to the Klaviyo Android SDK.
We welcome your feedback in the [issues](https://github.com/klaviyo/klaviyo-android-sdk/issues) section of our public GitHub repository.
Expand Down
42 changes: 39 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,33 @@ tasks.register("clean", Delete) {
delete rootProject.layout.buildDirectory
}

static def readXmlValue(String filePath, String tagName, Project project) {
def xmlFile = new File(project.projectDir, filePath)
// Check if the file exists
if (!xmlFile.exists()) {
throw new FileNotFoundException("The XML file does not exist: ${xmlFile.absolutePath}")
}

// Parse the XML file
def xmlContent
try {
xmlContent = new XmlSlurper().parse(xmlFile)
} catch (Exception e) {
throw new RuntimeException("Failed to parse XML file: ${xmlFile.absolutePath}. Error: ${e.message}", e)
}

// Look for the string with the specific name attribute
def result = xmlContent.'string'.find { it.@name == tagName }

if (result == null) {
throw new IllegalArgumentException("No string found with the name '${tagName}' in the file: ${xmlFile.absolutePath}")
}

return result.text()
}

dokkaHtmlMultiModule {
def versionName = versionFor(project, "version.klaviyo.versionName") as String
def versionName = readXmlValue('sdk/core/src/main/res/values/strings.xml','klaviyo_sdk_version_override', project)
def oldVersionsDir = layout.buildDirectory.dir("../docs/")
outputDirectory = layout.buildDirectory.dir("../docs/${versionName}")
includes.from("README.md")
Expand Down Expand Up @@ -73,17 +98,28 @@ public class BumpVersion extends DefaultTask {
return nextVersion;
}

static String readXmlValue(String filePath, String tagName) {
def xmlFile = new File(filePath)
def xmlContent = new XmlSlurper().parse(xmlFile)
// Look for the string with the specific name attribute
def result = xmlContent.'string'.find { it.@name == tagName }
return result?.text() ?: ""
}

@TaskAction
public void bumpVersion() {
def currentVersion = versionFor(project, "version.klaviyo.versionName") as String
def currentVersion = readXmlValue('sdk/core/src/main/res/values/strings.xml','klaviyo_sdk_version_override')
println(currentVersion)
def nextVersion = this.getNextVersion()
def currentBuild = versionFor(project, "version.klaviyo.versionCode") as Integer
def nextBuild = currentBuild + 1
print("Changing semantic version number from $currentVersion to $nextVersion\n")
print("Auto-incrementing version code from $currentBuild to $nextBuild\n")

ant.replace(file:"versions.properties", token:"versionCode=$currentBuild", value:"versionCode=$nextBuild")
ant.replace(file:"versions.properties", token:"versionName=$currentVersion", value:"versionName=$nextVersion")
def file = new File('sdk/core/src/main/res/values/strings.xml')
def newName = file.text.replace(currentVersion,nextVersion)
file.text = newName
ant.replace(file:"README.md", token:"analytics:$currentVersion", value:"analytics:$nextVersion")
ant.replace(file:"README.md", token:"push-fcm:$currentVersion", value:"push-fcm:$nextVersion")
ant.replace(file:"docs/index.html", token:"$currentVersion", value:"$nextVersion")
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<!-- Redirect to latest version -->
<meta HTTP-EQUIV="REFRESH" content="0; url=./2.4.1/index.html">
<meta HTTP-EQUIV="REFRESH" content="0; url=./3.0.0/index.html">
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ android.enableJetifier=true
# URL to Klaviyo server
klaviyoServerUrl=https://a.klaviyo.com

# Klaviyo API Revision
klaviyoApiRevision=2023-07-15

# Group ID prefix for all our published modules
klaviyoGroupId=com.klaviyo
3 changes: 1 addition & 2 deletions sdk/analytics/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import static de.fayard.refreshVersions.core.Versions.versionFor

project.description = "Public analytics API functionality for the Klaviyo SDK suite"
evaluationDependsOn(":sdk")
Expand Down Expand Up @@ -30,7 +29,7 @@ afterEvaluate {
from components[ext.publishBuildVariant]
groupId = klaviyoGroupId
artifactId = "analytics"
version = versionFor(project, "version.klaviyo.versionName")
version = readXmlValue('src/main/res/values/strings.xml','klaviyo_sdk_version_override', project(":sdk:core"))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions sdk/analytics/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-keep class com.klaviyo.core.** { *; }
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import com.klaviyo.core.BuildConfig
import com.klaviyo.core.Registry
import com.klaviyo.core.config.KlaviyoConfig
import com.klaviyo.core.config.getPackageInfoCompat
import com.klaviyo.core.model.fetchOrCreate
import java.util.UUID
Expand Down Expand Up @@ -47,19 +47,17 @@ internal object DeviceProperties {
packageInfo.getVersionCodeCompat().toString()
}

val sdkVersion: String by lazy {
BuildConfig.VERSION
}
val sdkVersion: String
get() = KlaviyoConfig.sdkVersion

val sdkName: String by lazy {
"android"
}
val sdkName: String
get() = KlaviyoConfig.sdkName

val backgroundData: Boolean by lazy {
activityManager.isBackgroundRestrictedCompat()
val backgroundDataEnabled: Boolean by lazy {
!activityManager.isBackgroundRestrictedCompat()
}

val notificationPermission: Boolean
val notificationPermissionGranted: Boolean
get() = NotificationManagerCompat.from(Registry.config.applicationContext).areNotificationsEnabled()

val applicationId: String by lazy {
Expand Down
Loading

0 comments on commit 798d03d

Please sign in to comment.