Skip to content

Commit

Permalink
Merge branch 'master' into rel/1.2.0
Browse files Browse the repository at this point in the history
* master:
  Helper methods for developers who use a custom workflow to handle notifications (#81)
  Fixed the behavior of external_id (#87)

# Conflicts:
#	versions.gradle
  • Loading branch information
Evan Masseau committed Jun 30, 2023
2 parents fed3c23 + 8501d74 commit ddfb250
Show file tree
Hide file tree
Showing 12 changed files with 87 additions and 60 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,14 @@ open class YourPushService : FirebaseMessagingService() {
}
```

**A note on 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`.
#### Custom Notification Display
If you wish to fully customize the display of notifications, we provide a set of `RemoteMessage`
extensions such as `import com.klaviyo.pushFcm.KlaviyoRemoteMessage.body` to access all the properties sent from Klaviyo.
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)`.

#### 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.

Expand Down
41 changes: 20 additions & 21 deletions sdk/analytics/src/main/java/com/klaviyo/analytics/Klaviyo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -284,30 +284,29 @@ object Klaviyo {
* @param intent the [Intent] from opening a notification
*/
fun handlePush(intent: Intent?) = apply {
val payload = intent?.extras?.let { extras ->
extras.keySet().associateWith { key -> extras.getString(key, "") }
} ?: emptyMap()

if (isKlaviyoPush(payload)) {
val event = Event(
EventType.OPENED_PUSH,
payload.mapKeys {
EventKey.CUSTOM(it.key)
}
)

getPushToken()?.let { event[EventKey.PUSH_TOKEN] = it }

createEvent(event)
} else {
Registry.log.info("Non-klaviyo intent ignored")
if (intent?.isKlaviyoIntent != true) {
Registry.log.info("Non-Klaviyo intent ignored")
return@apply
}

val event = Event(EventType.OPENED_PUSH)
val extras = intent.extras

extras?.keySet()?.forEach { key ->
if (key.contains("com.klaviyo")) {
val eventKey = EventKey.CUSTOM(key.replace("com.klaviyo.", ""))
event[eventKey] = extras.getString(key, "")
}
}

getPushToken()?.let { event[EventKey.PUSH_TOKEN] = it }

createEvent(event)
}

/**
* Checks whether a push notification payload originated from Klaviyo
*
* @param payload The String:String data from the push message, or intent extras
* Checks whether a notification intent originated from Klaviyo
*/
fun isKlaviyoPush(payload: Map<String, String>) = payload.containsKey("_k")
val Intent.isKlaviyoIntent: Boolean
get() = this.getStringExtra("com.klaviyo._k")?.isNotEmpty() ?: false
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sealed class EventKey(name: String) : Keyword(name) {
/**
* For [EventType.OPENED_PUSH] events, append the device token as an event property
*/
object PUSH_TOKEN : EventKey("push_token")
internal object PUSH_TOKEN : EventKey("push_token")

class CUSTOM(propertyName: String) : EventKey(propertyName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package com.klaviyo.analytics.model
sealed class EventType(name: String) : Keyword(name) {

// Push-related
object OPENED_PUSH : EventType("\$opened_push")
internal object OPENED_PUSH : EventType("\$opened_push")

// Product viewing events
object VIEWED_PRODUCT : EventType("\$viewed_product")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ sealed class ProfileKey(name: String) : Keyword(name) {
*/
internal fun specialKey(): String = when (this) {
ANONYMOUS_ID -> "\$anonymous"
EXTERNAL_ID, EMAIL, PHONE_NUMBER -> "$$name"
EXTERNAL_ID -> "\$id"
EMAIL, PHONE_NUMBER -> "$$name"
else -> name
}
}
59 changes: 30 additions & 29 deletions sdk/analytics/src/test/java/com/klaviyo/analytics/KlaviyoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -245,63 +245,64 @@ internal class KlaviyoTest : BaseTest() {
assertEquals(Klaviyo.getPushToken(), PUSH_TOKEN)
}

private val stubPushPayload = mapOf(
"body" to "Message body",
"_k" to """{
"Push Platform": "android",
"$\flow": "",
"$\message": "01GK4P5W6AV4V3APTJ727JKSKQ",
"$\variation": "",
"Message Name": "check_push_pipeline",
"Message Type": "campaign",
"c": "6U7nPA",
"cr": "31698553996657051350694345805149781",
"m": "01GK4P5W6AV4V3APTJ727JKSKQ",
"t": "1671205224",
"timestamp": "2022-12-16T15:40:24.049427+00:00",
"x": "manual"
}"""
)

private fun mockIntent(payload: Map<String, String>): Intent {
// Mocking an intent to return the stub push payload...
val intent = mockk<Intent>()
val bundle = mockk<Bundle>()
var gettingKey = ""
every { intent.extras } returns bundle
every { bundle.keySet() } returns payload.keys
every {
intent.getStringExtra(
match { s ->
gettingKey = s // there must be a better way to do this...
true
}
)
} answers { payload[gettingKey] }
every {
bundle.getString(
match { s ->
gettingKey = s // there must be a better way to do this...
payload.containsKey(s)
true
},
String()
)
} returns (payload[gettingKey] ?: "")
} answers { payload[gettingKey] }

return intent
}

@Test
fun `Identifies push payload origin`() {
// Handle push intent
assertEquals(true, Klaviyo.isKlaviyoPush(stubPushPayload))
assertEquals(false, Klaviyo.isKlaviyoPush(mapOf("other" to "3rd party push")))
}

@Test
fun `Handling opened push Intent enqueues $opened_push API Call`() {
// Handle push intent
Klaviyo.handlePush(mockIntent(stubPushPayload))
val stubIntentExtras = mapOf(
"com.klaviyo.body" to "Message body",
"com.klaviyo._k" to """{
"Push Platform": "android",
"$\flow": "",
"$\message": "01GK4P5W6AV4V3APTJ727JKSKQ",
"$\variation": "",
"Message Name": "check_push_pipeline",
"Message Type": "campaign",
"c": "6U7nPA",
"cr": "31698553996657051350694345805149781",
"m": "01GK4P5W6AV4V3APTJ727JKSKQ",
"t": "1671205224",
"timestamp": "2022-12-16T15:40:24.049427+00:00",
"x": "manual"
}"""
)

Klaviyo.handlePush(mockIntent(stubIntentExtras))

verify { apiClientMock.enqueueEvent(any(), any()) }
}

@Test
fun `Non-klaviyo push payload is ignored`() {
// doesn't have _k, klaviyo tracking params
Klaviyo.handlePush(mockIntent(mapOf("other" to "3rd party push")))
Klaviyo.handlePush(mockIntent(mapOf("com.other.package.message" to "3rd party push")))
Klaviyo.handlePush(null)

verify(inverse = true) { apiClientMock.enqueueEvent(any(), any()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class KeywordsTest {
assert(custom != ProfileKey.CUSTOM(expectedCustomKey + "1"))

assertEquals("\$anonymous", ProfileKey.ANONYMOUS_ID.specialKey())
assertEquals("\$external_id", ProfileKey.EXTERNAL_ID.specialKey())
assertEquals("\$id", ProfileKey.EXTERNAL_ID.specialKey())
assertEquals("\$email", ProfileKey.EMAIL.specialKey())
assertEquals("\$phone_number", ProfileKey.PHONE_NUMBER.specialKey())
assertEquals("custom", ProfileKey.CUSTOM("custom").specialKey())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal class EventApiRequestTest : BaseTest() {
private val stubEvent: Event = Event(EventType.CUSTOM("Test Event"))

private val stubProfile = Profile()
.setExternalId(EXTERNAL_ID)
.setAnonymousId(ANON_ID)
.setEmail(EMAIL)
.setPhoneNumber(PHONE)
Expand Down Expand Up @@ -57,6 +58,7 @@ internal class EventApiRequestTest : BaseTest() {
compareJson(requestJson, revivedRequest.toJson())
}

private val externalId = "\$id"
private val emailKey = "\$email"
private val anonKey = "\$anonymous"
private val phoneKey = "\$phone_number"
Expand All @@ -72,6 +74,7 @@ internal class EventApiRequestTest : BaseTest() {
"name": "${stubEvent.type}"
},
"profile": {
"$externalId": "$EXTERNAL_ID",
"$emailKey": "$EMAIL",
"$anonKey": "$ANON_ID",
"$phoneKey": "$PHONE"
Expand All @@ -98,6 +101,7 @@ internal class EventApiRequestTest : BaseTest() {
"name": "${stubEvent.type}"
},
"profile": {
"$externalId": "$EXTERNAL_ID",
"$emailKey": "$EMAIL",
"$anonKey": "$ANON_ID",
"$phoneKey": "$PHONE"
Expand Down Expand Up @@ -128,6 +132,7 @@ internal class EventApiRequestTest : BaseTest() {
"name": "${stubEvent.type}"
},
"profile": {
"$externalId": "$EXTERNAL_ID",
"$emailKey": "$EMAIL",
"$anonKey": "$ANON_ID",
"$phoneKey": "$PHONE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal class PushTokenApiRequestTest : BaseTest() {
val props = request.body?.optJSONObject("properties")

assertEquals(API_KEY, request.body?.optString("token"))
assertEquals(EXTERNAL_ID, props?.optString("\$external_id"))
assertEquals(EXTERNAL_ID, props?.optString("\$id"))
assertEquals(EMAIL, props?.optString("\$email"))
assertEquals(PHONE, props?.optString("\$phone_number"))
assertEquals(ANON_ID, props?.optString("\$anonymous"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.klaviyo.core.Registry
import com.klaviyo.pushFcm.KlaviyoRemoteMessage.appendKlaviyoExtras
import com.klaviyo.pushFcm.KlaviyoRemoteMessage.body
import com.klaviyo.pushFcm.KlaviyoRemoteMessage.channel_description
import com.klaviyo.pushFcm.KlaviyoRemoteMessage.channel_id
Expand Down Expand Up @@ -143,14 +144,14 @@ class KlaviyoNotification(private val message: RemoteMessage) {
// Create intent to open the activity and/or deep link if specified
// Else fall back on the default launcher intent for the package
val action = message.clickAction?.let {
message.toIntent().apply {
Intent().appendKlaviyoExtras(message).apply {
action = message.clickAction
data = message.deepLink
setPackage(pkgName)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
} ?: Registry.config.applicationContext.packageManager.getLaunchIntentForPackage(pkgName)?.apply {
putExtras(message.toIntent())
appendKlaviyoExtras(message)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ import com.klaviyo.core.Registry
*/
object KlaviyoRemoteMessage {

/**
* Append requisite data from a remote message to an intent
* for displaying a notification
*
* @param message
*/
fun Intent.appendKlaviyoExtras(message: RemoteMessage) = apply {
if (message.isKlaviyoMessage) {
message.data.forEach {
this.putExtra("com.klaviyo.${it.key}", it.value)
}
}
}

/**
* Parse channel ID or fallback on default
*/
Expand Down
2 changes: 1 addition & 1 deletion versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ext {
targetSDKVersion = 33

// project versioning
versionCode = 4
versionCode = 5
versionName = '1.2.0'

// dependencies
Expand Down

0 comments on commit ddfb250

Please sign in to comment.