diff --git a/app/src/main/java/io/customer/example/MainActivity.kt b/app/src/main/java/io/customer/example/MainActivity.kt index 3a04921a..177238d2 100644 --- a/app/src/main/java/io/customer/example/MainActivity.kt +++ b/app/src/main/java/io/customer/example/MainActivity.kt @@ -28,10 +28,17 @@ class MainActivity : AppCompatActivity() { // log events // makeEventsRequests() + // add custom attributes + makeAddCustomDeviceAttributesRequest() + // register device makeRegisterDeviceRequest() } + private fun makeAddCustomDeviceAttributesRequest() { + CustomerIO.instance().deviceAttributes.putAll(mapOf("bingo" to "heyaa")) + } + private fun makeRegisterDeviceRequest() { CustomerIO.instance().registerDeviceToken("token").enqueue(outputCallback) } @@ -99,7 +106,7 @@ class MainActivity : AppCompatActivity() { when ( val result = CustomerIO.instance().identify( - identifier = "support-ticket-test", + identifier = "device-attri", mapOf("created_at" to 1642659790) ).execute() ) { diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt index 63377a02..270bb6d4 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt @@ -48,6 +48,7 @@ class CustomerIO internal constructor( private var timeout = 6000L private var urlHandler: CustomerIOUrlHandler? = null private var shouldAutoRecordScreenViews: Boolean = false + private var autoTrackDeviceAttributes: Boolean = true private lateinit var activityLifecycleCallback: CustomerIOActivityLifecycleCallbacks @@ -66,6 +67,11 @@ class CustomerIO internal constructor( return this } + fun autoTrackDeviceAttributes(shouldTrackDeviceAttributes: Boolean): Builder { + this.autoTrackDeviceAttributes = shouldTrackDeviceAttributes + return this + } + /** * Override url/deep link handling * @@ -92,7 +98,8 @@ class CustomerIO internal constructor( region = region, timeout = timeout, urlHandler = urlHandler, - autoTrackScreenViews = shouldAutoRecordScreenViews + autoTrackScreenViews = shouldAutoRecordScreenViews, + autoTrackDeviceAttributes = autoTrackDeviceAttributes, ) val customerIoComponent = @@ -178,7 +185,15 @@ class CustomerIO internal constructor( * is no active customer, this will fail to register the device */ fun registerDeviceToken(deviceToken: String): Action = - api.registerDeviceToken(deviceToken, store.deviceStore.buildDeviceAttributes()) + api.registerDeviceToken(deviceToken, collectDeviceAttributes()) + + private fun collectDeviceAttributes(): Map { + return if (config.autoTrackDeviceAttributes) { + deviceAttributes + store.deviceStore.buildDeviceAttributes() + } else { + deviceAttributes + } + } /** * Delete the currently registered device token @@ -198,6 +213,12 @@ class CustomerIO internal constructor( deviceToken = deviceToken ) + /** + * Use to provide additional and custom device attributes + * apart from the ones the SDK is programmed to send to customer workspace. + */ + val deviceAttributes: MutableMap = mutableMapOf() + private fun recordScreenViews(activity: Activity, attributes: Map): Action { val packageManager = activity.packageManager return try { diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt b/sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt index 739ce641..a843e751 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt @@ -3,11 +3,12 @@ package io.customer.sdk import io.customer.sdk.data.communication.CustomerIOUrlHandler import io.customer.sdk.data.model.Region -class CustomerIOConfig( +data class CustomerIOConfig( val siteId: String, val apiKey: String, val region: Region, val timeout: Long, val urlHandler: CustomerIOUrlHandler?, - val autoTrackScreenViews: Boolean + val autoTrackScreenViews: Boolean, + val autoTrackDeviceAttributes: Boolean, ) diff --git a/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt index fee87cdf..d78db290 100644 --- a/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt +++ b/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt @@ -8,7 +8,7 @@ interface ApplicationStore { val customerAppName: String val customerAppVersion: String - val isPushSubscribed: Boolean + val isPushEnabled: Boolean } internal class ApplicationStoreImp(val context: Context) : ApplicationStore { @@ -19,7 +19,7 @@ internal class ApplicationStoreImp(val context: Context) : ApplicationStore { get() = appInfo.first override val customerAppVersion: String get() = appInfo.second - override val isPushSubscribed: Boolean + override val isPushEnabled: Boolean get() = NotificationManagerCompat.from(context).areNotificationsEnabled() private fun getAppInformation(): Pair { diff --git a/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt index 3542920e..365ccecd 100644 --- a/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt +++ b/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt @@ -17,7 +17,7 @@ interface BuildStore { // Android SDK Version: 21 val deviceOSVersion: Int - // Device locale: en + // Device locale: en-US val deviceLocale: String } @@ -32,5 +32,5 @@ internal class BuildStoreImp : BuildStore { override val deviceOSVersion: Int get() = Build.VERSION.SDK_INT override val deviceLocale: String - get() = Locale.getDefault().language + get() = Locale.getDefault().toLanguageTag() } diff --git a/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt index c9a261e2..3a8d2b2c 100644 --- a/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt +++ b/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt @@ -39,8 +39,8 @@ internal class DeviceStoreImp( get() = applicationStore.customerAppName override val customerAppVersion: String get() = applicationStore.customerAppVersion - override val isPushSubscribed: Boolean - get() = applicationStore.isPushSubscribed + override val isPushEnabled: Boolean + get() = applicationStore.isPushEnabled override val customerIOVersion: String get() = version @@ -60,7 +60,7 @@ internal class DeviceStoreImp( "app_version" to customerAppVersion, "cio_sdk_version" to customerIOVersion, "device_locale" to deviceLocale, - "push_subscribed" to isPushSubscribed + "push_enabled" to isPushEnabled ) } } diff --git a/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt b/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt index 6f92d8e1..5f760a37 100644 --- a/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt +++ b/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt @@ -1,18 +1,24 @@ package io.customer.sdk +import android.net.Uri import io.customer.base.data.ErrorResult import io.customer.base.error.ErrorDetail import io.customer.base.error.StatusCode import io.customer.base.utils.ActionUtils.Companion.getEmptyAction import io.customer.base.utils.ActionUtils.Companion.getErrorAction +import io.customer.sdk.MockCustomerIOBuilder.Companion.defaultConfig +import io.customer.sdk.data.communication.CustomerIOUrlHandler +import io.customer.sdk.data.model.Region import io.customer.sdk.data.request.MetricEvent import io.customer.sdk.utils.verifyError import io.customer.sdk.utils.verifySuccess -import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBeEqualTo import org.junit.Before import org.junit.Test import org.mockito.Mockito.`when` import org.mockito.kotlin.any +import org.mockito.kotlin.verify internal class CustomerIOTest : BaseTest() { @@ -28,13 +34,44 @@ internal class CustomerIOTest : BaseTest() { } @Test - fun `verify SDK configuration is correct`() { - customerIO.config.siteId `should be equal to` MockCustomerIOBuilder.siteId - customerIO.config.apiKey `should be equal to` MockCustomerIOBuilder.apiKey - customerIO.config.timeout `should be equal to` MockCustomerIOBuilder.timeout.toLong() - customerIO.config.region `should be equal to` MockCustomerIOBuilder.region - customerIO.config.urlHandler `should be equal to` MockCustomerIOBuilder.urlHandler - customerIO.config.autoTrackScreenViews `should be equal to` MockCustomerIOBuilder.shouldAutoRecordScreenViews + fun `verify SDK default configuration is correct`() { + customerIO.config.siteId shouldBeEqualTo MockCustomerIOBuilder.siteId + customerIO.config.apiKey shouldBeEqualTo MockCustomerIOBuilder.apiKey + customerIO.config.timeout shouldBeEqualTo MockCustomerIOBuilder.timeout.toLong() + customerIO.config.region shouldBeEqualTo MockCustomerIOBuilder.region + customerIO.config.urlHandler shouldBeEqualTo MockCustomerIOBuilder.urlHandler + customerIO.config.autoTrackScreenViews shouldBeEqualTo MockCustomerIOBuilder.shouldAutoRecordScreenViews + customerIO.config.autoTrackDeviceAttributes shouldBeEqualTo MockCustomerIOBuilder.autoTrackDeviceAttributes + } + + @Test + fun verify_onUpdatingBuilderConfigurations_expectCustomerIOOConfigToBeUpdated() { + + val mockCustomerIOBuilder = + MockCustomerIOBuilder( + defaultConfig.copy( + autoTrackDeviceAttributes = false, + autoTrackScreenViews = true, + siteId = "new-id", + apiKey = "new-key", + region = Region.EU, + urlHandler = object : CustomerIOUrlHandler { + override fun handleCustomerIOUrl(uri: Uri): Boolean { + return true + } + }, + timeout = 9000 + ) + ) + customerIO = mockCustomerIOBuilder.build() + + customerIO.config.siteId shouldNotBeEqualTo defaultConfig.siteId + customerIO.config.apiKey shouldNotBeEqualTo defaultConfig.apiKey + customerIO.config.timeout shouldNotBeEqualTo defaultConfig.timeout + customerIO.config.region shouldNotBeEqualTo defaultConfig.region + customerIO.config.urlHandler shouldNotBeEqualTo defaultConfig.urlHandler + customerIO.config.autoTrackScreenViews shouldNotBeEqualTo defaultConfig.autoTrackScreenViews + customerIO.config.autoTrackDeviceAttributes shouldNotBeEqualTo defaultConfig.autoTrackDeviceAttributes } @Test @@ -156,6 +193,56 @@ internal class CustomerIOTest : BaseTest() { verifySuccess(response, Unit) } + @Test + fun verify_bothDefaultAndCustomAttributesGetsAdded_withRegisterToken() { + + `when`( + mockCustomerIO.api.registerDeviceToken( + any(), + any(), + ) + ).thenReturn(getEmptyAction()) + + customerIO.deviceAttributes.putAll(mapOf("test" to "value")) + + val expectedToken = "token" + + val expectedAttributes = customerIO.deviceAttributes + deviceStore.buildDeviceAttributes() + + val response = customerIO.registerDeviceToken(expectedToken).execute() + + verify(mockCustomerIO.api).registerDeviceToken(expectedToken, expectedAttributes) + + verifySuccess(response, Unit) + } + + @Test + fun verify_defaultAttributesGetsSkipped_onBasisOfConfig() { + + val mockCustomerIO = + MockCustomerIOBuilder(MockCustomerIOBuilder.defaultConfig.copy(autoTrackDeviceAttributes = false)) + customerIO = mockCustomerIO.build() + + `when`(mockCustomerIO.store.deviceStore).thenReturn(deviceStore) + + `when`( + mockCustomerIO.api.registerDeviceToken( + any(), + any(), + ) + ).thenReturn(getEmptyAction()) + + customerIO.deviceAttributes.putAll(mapOf("test" to "value")) + + val expectedToken = "token" + + val expectedAttributes = mapOf("test" to "value") + + customerIO.registerDeviceToken(expectedToken).execute() + + verify(mockCustomerIO.api).registerDeviceToken(expectedToken, expectedAttributes) + } + @Test fun `verify SDK returns error when adding device request fails`() { `when`( diff --git a/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt b/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt index 0132b095..e6003203 100644 --- a/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt +++ b/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt @@ -5,7 +5,7 @@ import io.customer.sdk.data.model.Region import io.customer.sdk.data.store.CustomerIOStore import org.mockito.kotlin.mock -internal class MockCustomerIOBuilder { +internal class MockCustomerIOBuilder(private val customerIOConfig: CustomerIOConfig = defaultConfig) { lateinit var api: CustomerIOApi lateinit var store: CustomerIOStore @@ -18,17 +18,20 @@ internal class MockCustomerIOBuilder { const val timeout = 6000 val urlHandler = null const val shouldAutoRecordScreenViews = false - } + const val autoTrackDeviceAttributes = true - fun build(): CustomerIO { - val customerIOConfig = CustomerIOConfig( + val defaultConfig = CustomerIOConfig( apiKey = "mock-key", siteId = "mock-site", region = Region.US, timeout = 6000, urlHandler = urlHandler, - autoTrackScreenViews = shouldAutoRecordScreenViews + autoTrackScreenViews = shouldAutoRecordScreenViews, + autoTrackDeviceAttributes = autoTrackDeviceAttributes ) + } + + fun build(): CustomerIO { api = mock() store = mock() diff --git a/sdk/src/test/java/io/customer/sdk/store/DeviceStoreTest.kt b/sdk/src/test/java/io/customer/sdk/store/DeviceStoreTest.kt index ce024c96..e2c1c936 100644 --- a/sdk/src/test/java/io/customer/sdk/store/DeviceStoreTest.kt +++ b/sdk/src/test/java/io/customer/sdk/store/DeviceStoreTest.kt @@ -13,7 +13,7 @@ internal class DeviceStoreTest : BaseTest() { deviceStore.deviceModel `should be equal to` "Pixel 6" deviceStore.deviceManufacturer `should be equal to` "Google" deviceStore.deviceOSVersion `should be equal to` 30 - deviceStore.deviceLocale `should be equal to` "en" + deviceStore.deviceLocale `should be equal to` "en-US" } @Test @@ -36,8 +36,8 @@ internal class DeviceStoreTest : BaseTest() { "device_model" to "Pixel 6", "app_version" to "1.0", "cio_sdk_version" to "1.0.0-alpha.6", - "device_locale" to "en", - "push_subscribed" to true + "device_locale" to "en-US", + "push_enabled" to true ) resultDeviceAttributes shouldBeEqualTo expectedDeviceAttributes diff --git a/sdk/src/test/java/io/customer/sdk/utils/DeviceStoreStub.kt b/sdk/src/test/java/io/customer/sdk/utils/DeviceStoreStub.kt index 1e940609..0e59766d 100644 --- a/sdk/src/test/java/io/customer/sdk/utils/DeviceStoreStub.kt +++ b/sdk/src/test/java/io/customer/sdk/utils/DeviceStoreStub.kt @@ -20,14 +20,14 @@ class DeviceStoreStub { override val deviceOSVersion: Int get() = 30 override val deviceLocale: String - get() = Locale.US.language + get() = Locale.US.toLanguageTag() }, applicationStore = object : ApplicationStore { override val customerAppName: String get() = "User App" override val customerAppVersion: String get() = "1.0" - override val isPushSubscribed: Boolean + override val isPushEnabled: Boolean get() = true }, version = "1.0.0-alpha.6"