Skip to content

Commit

Permalink
feat!: get current FCM token on SDK startup
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed May 9, 2022
1 parent 149c922 commit dda443d
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 4 deletions.
9 changes: 6 additions & 3 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ coverage:
# rules applied to only changes in a pull request. Not changes to the test coverage of the entire project.
patch:
default:
# Do not fail CI server if pull request doesn't introduce very many tests in it. It's annoying to make a pull request blocked because
# it does not contain very many tests in it. We care more about the test coverage of the whole project. For small pull requests that only
# change a couple lines of code, 'patch' check is common to fail and can be more annoying then useful.
# Do not fail CI server if pull request doesn't introduce very many tests in it. Some pull requests are small. Some are refactors where the
# code coverage decreases a small amount without causing harm to the code base.
# informational will generate a report and present it in the PR but will not block the PR from being merged.
informational: true
project:
default:
informational: true
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ abstract class BaseTest {
sdkConfig = cioConfig,
context = this@BaseTest.context
)

di.fileStorage.deleteAllSdkFiles()
di.sharedPreferenceRepository.clearAll()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.customer.messagingpush

import io.customer.messagingpush.di.fcmTokenProvider
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOInstance
import io.customer.sdk.CustomerIOModule
import io.customer.sdk.di.CustomerIOComponent

class ModuleMessagingPushFCM internal constructor(
private val overrideCustomerIO: CustomerIOInstance?,
private val overrideDiGraph: CustomerIOComponent?
) : CustomerIOModule {

constructor() : this(overrideCustomerIO = null, overrideDiGraph = null)

private val customerIO: CustomerIOInstance
get() = overrideCustomerIO ?: CustomerIO.instance()
private val diGraph: CustomerIOComponent
get() = overrideDiGraph ?: CustomerIO.instance().diGraph

private val fcmTokenProvider by lazy { diGraph.fcmTokenProvider }

override val moduleName: String
get() = "MessagingPushFCM"

override fun initialize() {
getCurrentFcmToken()
}

/**
* FCM only provides a push device token once through the [CustomerIOFirebaseMessagingService] when there is a new token assigned to the device. After that, it's up to you to get the device token.
*
* This can cause edge cases where a customer might never get a device token assigned to a profile. https://github.com/customerio/customerio-android/issues/61
*
* To fix this, it's recommended that each time your app starts up, you get the current push token and register it to the SDK. We do it for you automatically here as long as you initialize the MessagingPush module with the SDK.
*/
private fun getCurrentFcmToken() {
fcmTokenProvider.getCurrentToken { token ->
token?.let { token -> customerIO.registerDeviceToken(token) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.customer.messagingpush.di

import io.customer.messagingpush.provider.FCMTokenProvider
import io.customer.messagingpush.provider.FCMTokenProviderImpl
import io.customer.sdk.di.CustomerIOComponent

/*
This file contains a series of extensions to the common module's Dependency injection (DI) graph. All extensions in this file simply add internal classes for this module into the DI graph.
The use of extensions was chosen over creating a separate graph class for each module. This simplifies the SDK code as well as automated tests code dramatically.
*/

internal val CustomerIOComponent.fcmTokenProvider: FCMTokenProvider
get() = override() ?: FCMTokenProviderImpl(logger)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.customer.messagingpush.provider

import com.google.firebase.messaging.FirebaseMessaging
import io.customer.sdk.util.Logger

/**
* Wrapper around FCM SDK to make the code base more testable.
*/
internal interface FCMTokenProvider {
fun getCurrentToken(onComplete: (String?) -> Unit)
}

/**
* This class should be as small as possible as possible because it can't be tested with automated tests. QA testing, only.
*/
class FCMTokenProviderImpl(private val logger: Logger) : FCMTokenProvider {

override fun getCurrentToken(onComplete: (String?) -> Unit) {
logger.debug("getting current FCM device token...")

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val existingDeviceToken = task.result
logger.debug("got current FCM token: $existingDeviceToken")

onComplete(existingDeviceToken)
} else {
logger.debug("got current FCM token: null")

onComplete(null)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.customer.messagingpush

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.customer.common_test.BaseTest
import io.customer.messagingpush.provider.FCMTokenProvider
import io.customer.sdk.CustomerIOInstance
import io.customer.sdk.utils.random
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
internal class ModuleMessagingPushFCMTest : BaseTest() {

private val customerIOMock: CustomerIOInstance = mock()
private val fcmTokenProviderMock: FCMTokenProvider = mock()
private lateinit var module: ModuleMessagingPushFCM

@Before
override fun setup() {
super.setup()

di.overrideDependency(FCMTokenProvider::class.java, fcmTokenProviderMock)

module = ModuleMessagingPushFCM(overrideCustomerIO = customerIOMock, overrideDiGraph = di)
}

@Test
fun initialize_givenNoFCMTokenAvailable_expectDoNotRegisterToken() {
whenever(fcmTokenProviderMock.getCurrentToken(any())).thenAnswer {
val callback = it.getArgument<(String?) -> Unit>(0)
callback(null)
}

module.initialize()

verify(customerIOMock, never()).registerDeviceToken(anyOrNull())
}

@Test
fun initialize_givenFCMTokenAvailable_expectRegisterToken() {
val givenToken = String.random

whenever(fcmTokenProviderMock.getCurrentToken(any())).thenAnswer {
val callback = it.getArgument<(String?) -> Unit>(0)
callback(givenToken)
}

module.initialize()

verify(customerIOMock).registerDeviceToken(givenToken)
}
}
12 changes: 12 additions & 0 deletions sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class CustomerIO internal constructor(
private var urlHandler: CustomerIOUrlHandler? = null
private var shouldAutoRecordScreenViews: Boolean = false
private var autoTrackDeviceAttributes: Boolean = true
private var modules: MutableMap<String, CustomerIOModule> = mutableMapOf()
private var logLevel = CioLogLevel.ERROR
private var trackingApiUrl: String? = null

Expand Down Expand Up @@ -151,6 +152,11 @@ class CustomerIO internal constructor(
return this
}

fun addCustomerIOModule(module: CustomerIOModule): Builder {
modules[module.moduleName] = module
return this
}

fun build(): CustomerIO {

if (apiKey.isEmpty()) {
Expand All @@ -177,12 +183,18 @@ class CustomerIO internal constructor(

val diGraph = CustomerIOComponent(sdkConfig = config, context = appContext)
val client = CustomerIO(diGraph)
val logger = diGraph.logger

activityLifecycleCallback = CustomerIOActivityLifecycleCallbacks(client, config)
appContext.registerActivityLifecycleCallbacks(activityLifecycleCallback)

instance = client

modules.forEach {
logger.debug("initializing SDK module ${it.value.moduleName}...")
it.value.initialize()
}

return client
}
}
Expand Down
11 changes: 11 additions & 0 deletions sdk/src/main/java/io/customer/sdk/CustomerIOModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.customer.sdk

/**
* A module is optional Customer.io SDK that you can install in your app.
*
* This interface allows the base SDK to initialize all of the SDKs installed in an app and begin to communicate with them.
*/
interface CustomerIOModule {
val moduleName: String
fun initialize()
}
61 changes: 61 additions & 0 deletions sdk/src/sharedTest/java/io/customer/sdk/CustomerIOTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class CustomerIOTest : BaseTest() {
Expand Down Expand Up @@ -110,4 +112,63 @@ class CustomerIOTest : BaseTest() {

verify(profileRepositoryMock).addCustomProfileAttributes(givenAttributes)
}

@Test
fun build_givenModule_expectInitializeModule() {
val givenModule: CustomerIOModule = mock<CustomerIOModule>().apply {
whenever(this.moduleName).thenReturn(String.random)
}

val client = CustomerIO.Builder(
siteId = String.random,
apiKey = String.random,
appContext = application
).addCustomerIOModule(givenModule).build()

verify(givenModule).initialize()
}

@Test
fun build_givenMultipleModules_expectInitializeAllModules() {
val givenModule1: CustomerIOModule = mock<CustomerIOModule>().apply {
whenever(this.moduleName).thenReturn(String.random)
}
val givenModule2: CustomerIOModule = mock<CustomerIOModule>().apply {
whenever(this.moduleName).thenReturn(String.random)
}

val client = CustomerIO.Builder(
siteId = String.random,
apiKey = String.random,
appContext = application
)
.addCustomerIOModule(givenModule1)
.addCustomerIOModule(givenModule2)
.build()

verify(givenModule1).initialize()
verify(givenModule2).initialize()
}

@Test
fun build_givenMultipleModulesOfSameType_expectOnlyInitializeOneModuleInstance() {
val givenModule1: CustomerIOModule = mock<CustomerIOModule>().apply {
whenever(this.moduleName).thenReturn("shared-module-name")
}
val givenModule2: CustomerIOModule = mock<CustomerIOModule>().apply {
whenever(this.moduleName).thenReturn("shared-module-name")
}

val client = CustomerIO.Builder(
siteId = String.random,
apiKey = String.random,
appContext = application
)
.addCustomerIOModule(givenModule1)
.addCustomerIOModule(givenModule2)
.build()

verify(givenModule1, never()).initialize()
verify(givenModule2).initialize()
}
}

0 comments on commit dda443d

Please sign in to comment.