Skip to content

Commit

Permalink
feat: added support for device attributes (customerio#71)
Browse files Browse the repository at this point in the history
* Added support for device attributes
  • Loading branch information
Shahroz16 authored Mar 17, 2022
1 parent 8418b3e commit 5fedf26
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 55 deletions.
9 changes: 8 additions & 1 deletion app/src/main/java/io/customer/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ class MainActivity : AppCompatActivity() {
// makeAsynchronousRequest()

// log events
makeEventsRequests()
// makeEventsRequests()

// register device
makeRegisterDeviceRequest()
}

private fun makeRegisterDeviceRequest() {
CustomerIO.instance().registerDeviceToken("token").enqueue(outputCallback)
}

private val outputCallback = Action.Callback<Unit> { result ->
Expand Down
1 change: 1 addition & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation Dependencies.moshi
implementation Dependencies.retrofitMoshiConverter
implementation Dependencies.okhttpLoggingInterceptor
implementation Dependencies.androidxCoreKtx

kapt(Dependencies.moshiCodeGen)

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class CustomerIO internal constructor(
* is no active customer, this will fail to register the device
*/
fun registerDeviceToken(deviceToken: String): Action<Unit> =
api.registerDeviceToken(deviceToken)
api.registerDeviceToken(deviceToken, store.deviceStore.buildDeviceAttributes())

/**
* Delete the currently registered device token
Expand Down
10 changes: 7 additions & 3 deletions sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ internal class CustomerIOClient(
}
}

override fun registerDeviceToken(deviceToken: String): Action<Unit> {
override fun registerDeviceToken(
deviceToken: String,
deviceAttributes: Map<String, Any>
): Action<Unit> {
val identifier = preferenceRepository.getIdentifier()
return object : Action<Unit> {
val action by lazy {
pushNotificationRepository.registerDeviceToken(
identifier,
deviceToken
identifier = identifier,
deviceToken = deviceToken,
attributes = deviceAttributes
)
}

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/main/java/io/customer/sdk/api/CustomerIOApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal interface CustomerIOApi {
fun identify(identifier: String, attributes: Map<String, Any>): Action<Unit>
fun track(name: String, attributes: Map<String, Any>): Action<Unit>
fun clearIdentify()
fun registerDeviceToken(deviceToken: String): Action<Unit>
fun registerDeviceToken(deviceToken: String, deviceAttributes: Map<String, Any>): Action<Unit>
fun deleteDeviceToken(): Action<Unit>
fun trackMetric(deliveryID: String, event: MetricEvent, deviceToken: String): Action<Unit>
fun screen(name: String, attributes: Map<String, Any>): Action<Unit>
Expand Down
3 changes: 2 additions & 1 deletion sdk/src/main/java/io/customer/sdk/data/request/Device.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import com.squareup.moshi.JsonClass
internal data class Device(
@field:Json(name = "id") val token: String,
val platform: String = "android",
val lastUsed: Long
val lastUsed: Long,
val attributes: Map<String, Any>
)

@JsonClass(generateAdapter = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package io.customer.sdk.data.store

import android.content.Context
import androidx.core.app.NotificationManagerCompat

interface ApplicationStore {
// Customer App information
val customerAppName: String
val customerAppVersion: String

val isPushSubscribed: Boolean
}

internal class ApplicationStoreImp(val context: Context) : ApplicationStore {
Expand All @@ -16,6 +19,8 @@ internal class ApplicationStoreImp(val context: Context) : ApplicationStore {
get() = appInfo.first
override val customerAppVersion: String
get() = appInfo.second
override val isPushSubscribed: Boolean
get() = NotificationManagerCompat.from(context).areNotificationsEnabled()

private fun getAppInformation(): Pair<String, String> {
val appName: String = try {
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.customer.sdk.data.store

import android.os.Build
import java.util.*

interface BuildStore {

Expand All @@ -15,6 +16,9 @@ interface BuildStore {

// Android SDK Version: 21
val deviceOSVersion: Int

// Device locale: en
val deviceLocale: String
}

internal class BuildStoreImp : BuildStore {
Expand All @@ -27,4 +31,6 @@ internal class BuildStoreImp : BuildStore {
get() = Build.MANUFACTURER
override val deviceOSVersion: Int
get() = Build.VERSION.SDK_INT
override val deviceLocale: String
get() = Locale.getDefault().language
}
16 changes: 16 additions & 0 deletions sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface DeviceStore : BuildStore, ApplicationStore {
* `Customer.io Android Client/1.0.0-alpha.6`
*/
fun buildUserAgent(): String
fun buildDeviceAttributes(): Map<String, Any>
}

internal class DeviceStoreImp(
Expand All @@ -32,10 +33,14 @@ internal class DeviceStoreImp(
get() = buildStore.deviceManufacturer
override val deviceOSVersion: Int
get() = buildStore.deviceOSVersion
override val deviceLocale: String
get() = buildStore.deviceLocale
override val customerAppName: String
get() = applicationStore.customerAppName
override val customerAppVersion: String
get() = applicationStore.customerAppVersion
override val isPushSubscribed: Boolean
get() = applicationStore.isPushSubscribed
override val customerIOVersion: String
get() = version

Expand All @@ -47,4 +52,15 @@ internal class DeviceStoreImp(
append(" $customerAppName/$customerAppVersion")
}
}

override fun buildDeviceAttributes(): Map<String, Any> {
return mapOf(
"device_os" to deviceOSVersion,
"device_model" to deviceModel,
"app_version" to customerAppVersion,
"cio_sdk_version" to customerIOVersion,
"device_locale" to deviceLocale,
"push_subscribed" to isPushSubscribed
)
}
}
3 changes: 2 additions & 1 deletion sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ internal class CustomerIOComponent(
),
pushNotificationRepository = PushNotificationRepositoryImp(
customerService = buildRetrofitApi<CustomerService>(),
pushService = buildRetrofitApi<PushService>()
pushService = buildRetrofitApi<PushService>(),
attributesRepository = attributesRepository
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,32 @@ import io.customer.sdk.data.request.MetricEvent
import java.util.*

internal interface PushNotificationRepository {
fun registerDeviceToken(identifier: String?, deviceToken: String): Action<Unit>
fun registerDeviceToken(
identifier: String?,
deviceToken: String,
attributes: Map<String, Any>
): Action<Unit>

fun deleteDeviceToken(identifier: String?, deviceToken: String?): Action<Unit>
fun trackMetric(deliveryID: String, event: MetricEvent, deviceToken: String): Action<Unit>
}

internal class PushNotificationRepositoryImp(
private val customerService: CustomerService,
private val pushService: PushService
private val pushService: PushService,
private val attributesRepository: AttributesRepository
) : PushNotificationRepository {

override fun registerDeviceToken(
identifier: String?,
deviceToken: String
deviceToken: String,
attributes: Map<String, Any>
): Action<Unit> {
val device = Device(token = deviceToken, lastUsed = Date().getUnixTimestamp())
val device = Device(
token = deviceToken,
lastUsed = Date().getUnixTimestamp(),
attributes = attributesRepository.mapToJson(attributes)
)
return when {
identifier == null -> {
return ActionUtils.getUnidentifiedUserAction()
Expand Down
21 changes: 21 additions & 0 deletions sdk/src/test/java/io/customer/sdk/BaseTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.customer.sdk

import io.customer.sdk.data.moshi.CustomerIOParser
import io.customer.sdk.data.moshi.CustomerIOParserImpl
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.repository.AttributesRepository
import io.customer.sdk.repository.MoshiAttributesRepositoryImp
import io.customer.sdk.utils.DeviceStoreStub
import org.junit.Before

internal open class BaseTest {

val deviceStore: DeviceStore = DeviceStoreStub().deviceStore
private val parser: CustomerIOParser = CustomerIOParserImpl()
lateinit var attributesRepository: AttributesRepository

@Before
fun baseSetup() {
attributesRepository = MoshiAttributesRepositoryImp(parser)
}
}
19 changes: 14 additions & 5 deletions sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

internal class CustomerIOClientTest {
internal class CustomerIOClientTest : BaseTest() {

private val preferenceRepository: PreferenceRepository = mock()
private val identityRepository: IdentityRepository = mock()
Expand Down Expand Up @@ -91,10 +91,13 @@ internal class CustomerIOClientTest {
preferenceRepository.getIdentifier()
).thenReturn("identify")

`when`(pushNotificationRepository.registerDeviceToken(any(), any()))
`when`(pushNotificationRepository.registerDeviceToken(any(), any(), any()))
.thenReturn(ActionUtils.getEmptyAction())

val result = customerIOClient.registerDeviceToken("token").execute()
val result = customerIOClient.registerDeviceToken(
"token",
deviceStore.buildDeviceAttributes()
).execute()

verifySuccess(result, Unit)
}
Expand All @@ -105,10 +108,16 @@ internal class CustomerIOClientTest {
preferenceRepository.getIdentifier()
).thenReturn("identify")

`when`(pushNotificationRepository.registerDeviceToken(any(), any()))
`when`(
pushNotificationRepository.registerDeviceToken(
any(),
any(),
any()
)
)
.thenReturn(ActionUtils.getEmptyAction())

customerIOClient.registerDeviceToken("token").execute()
customerIOClient.registerDeviceToken("token", deviceStore.buildDeviceAttributes()).execute()

verify(preferenceRepository, times(1)).saveDeviceToken("token")
}
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any

internal class CustomerIOTest {
internal class CustomerIOTest : BaseTest() {

private lateinit var customerIO: CustomerIO
lateinit var mockCustomerIO: MockCustomerIOBuilder
Expand All @@ -23,6 +23,8 @@ internal class CustomerIOTest {
fun setUp() {
mockCustomerIO = MockCustomerIOBuilder()
customerIO = mockCustomerIO.build()

`when`(mockCustomerIO.store.deviceStore).thenReturn(deviceStore)
}

@Test
Expand Down Expand Up @@ -145,6 +147,7 @@ internal class CustomerIOTest {
`when`(
mockCustomerIO.api.registerDeviceToken(
any(),
any(),
)
).thenReturn(getEmptyAction())

Expand All @@ -158,6 +161,7 @@ internal class CustomerIOTest {
`when`(
mockCustomerIO.api.registerDeviceToken(
any(),
any(),
)
)
.thenReturn(getErrorAction(errorResult = ErrorResult(error = ErrorDetail(statusCode = StatusCode.BadRequest))))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.customer.sdk.repositories

import io.customer.base.error.StatusCode
import io.customer.sdk.BaseTest
import io.customer.sdk.api.service.CustomerService
import io.customer.sdk.api.service.PushService
import io.customer.sdk.data.request.MetricEvent
Expand All @@ -16,7 +17,7 @@ import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.mock

internal class PushNotificationRepositoryTest {
internal class PushNotificationRepositoryTest : BaseTest() {

private val mockCustomerService: CustomerService = mock()
private val mockPushService: PushService = mock()
Expand All @@ -27,22 +28,29 @@ internal class PushNotificationRepositoryTest {
fun setup() {
pushNotificationRepository = PushNotificationRepositoryImp(
customerService = mockCustomerService,
pushService = mockPushService
pushService = mockPushService,
attributesRepository = attributesRepository
)
}

@Test
fun `Register device returns error if user is not identified`() {

val result = pushNotificationRepository.registerDeviceToken(null, "token").execute()
val result = pushNotificationRepository.registerDeviceToken(
null,
"token", deviceStore.buildDeviceAttributes()
).execute()

verifyError(result, StatusCode.UnIdentifiedUser)
}

@Test
fun `Register device returns error if token is blank`() {

val result = pushNotificationRepository.registerDeviceToken("identifier", "").execute()
val result = pushNotificationRepository.registerDeviceToken(
"identifier",
"", emptyMap()
).execute()

verifyError(result, StatusCode.InvalidToken)
}
Expand All @@ -53,7 +61,11 @@ internal class PushNotificationRepositoryTest {
mockCustomerService.addDevice(any(), any())
).thenReturn(MockRetrofitSuccess(Unit).toCustomerIoCall())

val result = pushNotificationRepository.registerDeviceToken("identifier", "token").execute()
val result = pushNotificationRepository.registerDeviceToken(
"identifier",
"token",
deviceStore.buildDeviceAttributes()
).execute()

verifySuccess(result, Unit)
}
Expand All @@ -64,7 +76,10 @@ internal class PushNotificationRepositoryTest {
mockCustomerService.addDevice(any(), any())
).thenReturn(MockRetrofitError<Unit>(500).toCustomerIoCall())

val result = pushNotificationRepository.registerDeviceToken("identifier", "token").execute()
val result = pushNotificationRepository.registerDeviceToken(
"identifier",
"token", deviceStore.buildDeviceAttributes()
).execute()

verifyError(result, StatusCode.InternalServerError)
}
Expand Down
Loading

0 comments on commit 5fedf26

Please sign in to comment.