diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53abf2183b..3c4c234aa7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -383,6 +383,8 @@ dependencies { implementation(deps.amplitude) implementation(deps.appsflyer) implementation(deps.appsflyer.oaid) + implementation(deps.customerio.analytics) + implementation(deps.customerio.messaging) implementation("com.android.installreferrer:installreferrer:2.2") implementation(deps.spongecastle.core) implementation(deps.lottie) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 7e7ecd4baa..8cf20b1b8a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -66,6 +66,7 @@ import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient +import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemAppLoggerInitializer @@ -410,6 +411,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory)) + factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) factory.addFilter(oneTimeEventFilter) factory.addFilter(AppsFlyerEventFilter()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt new file mode 100644 index 0000000000..5c81ccfd73 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.UserIdHolder + +/** + * Client interface for Customer.io SDK operations. + */ +interface CustomerIoAnalyticsClient : UserIdHolder \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt new file mode 100644 index 0000000000..2c34c4518b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder + +/** + * Customer.io analytics handler. + */ +class CustomerIoAnalyticsHandler( + private val client: CustomerIoAnalyticsClient, +) : AnalyticsHandler, AnalyticsUserIdHandler { + + override fun id(): String = ID + + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + + override fun send(event: AnalyticsEvent) { + // No-op: product events are not sent to Customer.io. + // Triggers are configured to come from Amplitude directly. + } + + companion object { + const val ID = "CustomerIO" + } + + class Builder : AnalyticsHandlerBuilder { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? { + return if (data.logConfig.isCustomerIoLogEnabled) { + CustomerIoAnalyticsHandler(client = CustomerIoLogClient()) + } else if (data.config.customerIoCdpApiKey.isNotBlank()) { + CustomerIoAnalyticsHandler( + client = CustomerIoClient( + application = data.application, + cdpApiKey = data.config.customerIoCdpApiKey, + ), + ) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt new file mode 100644 index 0000000000..834244e2ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import android.app.Application +import io.customer.messagingpush.ModuleMessagingPushFCM +import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder +import timber.log.Timber + +/** + * Real Customer.io SDK client. + * + * Initializes the SDK with the given CDP API key and provides: + * - User identification (identify / clearIdentify) + * + * Auto-tracking of application lifecycle events is disabled since it is not needed. + * Auto-tracking of screen views is disabled. + */ +internal class CustomerIoClient( + application: Application, + cdpApiKey: String, +) : CustomerIoAnalyticsClient { + + init { + CustomerIOBuilder( + applicationContext = application, + cdpApiKey = cdpApiKey, + ) + .trackApplicationLifecycleEvents(false) + .autoTrackActivityScreens(false) + .addCustomerIOModule(ModuleMessagingPushFCM()) + .build() + + Timber.d("CustomerIO SDK initialized") + } + + override fun setUserId(userId: String) { + CustomerIO.instance().identify(userId = userId) + } + + override fun clearUserId() { + CustomerIO.instance().clearIdentify() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt new file mode 100644 index 0000000000..62a36d09f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import timber.log.Timber + +/** + * Log client for Customer.io (used in debug mode). + * + * Logs all operations to Timber instead of sending them to Customer.io. + */ +internal class CustomerIoLogClient : CustomerIoAnalyticsClient { + + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId") + } + + override fun clearUserId() { + Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId") + this.userId = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 97026725ea..7c584a3bd6 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import io.customer.messagingpush.CustomerIOFirebaseMessagingService import timber.log.Timber @SuppressLint("MissingFirebaseInstanceTokenRefresh") @@ -15,11 +16,15 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) Timber.d("New FCM token received: $token") + + CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 22545b5329..b8624c9ec8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -28,4 +28,5 @@ data class EnvironmentConfig( val bffStaticTokenDev: String? = null, val gaslessTxApiKeyDev: String? = null, val gaslessTxApiKey: String? = null, + val customerIoCdpApiKey: String = "", ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 1a8068cfb5..840894f1ad 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -17,4 +17,5 @@ object AnalyticsHandlersLogConfig { val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isCustomerIoLogEnabled: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 68f64e61cf..7169e359a2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -103,6 +103,7 @@ usedesk = "4.4.0" sumsub = "1.38.0" haze = "1.7.1" kotlinpoet = "1.18.1" +customerio = "4.6.3" # endregion Other libraries # region Tools @@ -313,4 +314,6 @@ usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } +customerio-analytics = { module = "io.customer.android:datapipelines", version.ref = "customerio" } +customerio-messaging = { module = "io.customer.android:messaging-push-fcm", version.ref = "customerio" } # endregion Other