From 9498448ea9ba54ff0bcce901b0aa50f7af7728ec Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 23 Jun 2026 13:10:04 +0200 Subject: [PATCH 1/8] Add Firebase analytics interface and Android impl --- app/app/build.gradle.kts | 1 + .../core/tracking/EventTrackingClient.kt | 24 +++++++ .../tracking-firebase/build.gradle.kts | 15 +++++ .../src/main/AndroidManifest.xml | 2 + .../firebase/EventTrackingInitializer.kt | 39 ++++++++++++ .../firebase/FirebaseEventTrackingClient.kt | 62 +++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt create mode 100644 app/tracking/tracking-firebase/build.gradle.kts create mode 100644 app/tracking/tracking-firebase/src/main/AndroidManifest.xml create mode 100644 app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/EventTrackingInitializer.kt create mode 100644 app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index cf7cb57fa2..00eab56cd0 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -238,6 +238,7 @@ dependencies { implementation(projects.tierComparison) implementation(projects.trackingCore) implementation(projects.trackingDatadog) + implementation(projects.trackingFirebase) implementation(projects.uiForceUpgrade) // OkHttp for ProGuard rules only - not available at compile time diff --git a/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt b/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt new file mode 100644 index 0000000000..565d070cbd --- /dev/null +++ b/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt @@ -0,0 +1,24 @@ +package com.hedvig.android.core.tracking + +/** + * Product analytics event tracking, distinct from [RumLogger] (which feeds Datadog RUM). The single binding is backed + * by Firebase Analytics in production and is silenced in demo mode by disabling collection. + * + * This type is currently bound only on Android. iOS has its own independent native analytics client. To drive shared + * (KMP) analytics through this interface on iOS, the iOS side would need to pass a native implementation into the + * `IosGraph` when it builds the DI graph (`initDiGraph` in `shareddi`) — i.e. iOS hands us an object satisfying this + * interface that bridges to their native tracker, contributed as the iOS binding. Until that exists, injecting this in + * common code would fail to resolve when compiling the iOS graph (Metro is compile-time DI, so it fails loudly, not as + * a silent no-op). + */ +interface EventTrackingClient { + fun setCollectionEnabled(enabled: Boolean) + + fun trackEvent(name: String, parameters: Map = emptyMap()) + + fun trackScreen(name: String, parameters: Map = emptyMap()) + + fun setUserId(userId: String?) + + fun setUserProperty(name: String, value: String?) +} diff --git a/app/tracking/tracking-firebase/build.gradle.kts b/app/tracking/tracking-firebase/build.gradle.kts new file mode 100644 index 0000000000..9ead318b44 --- /dev/null +++ b/app/tracking/tracking-firebase/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("hedvig.android.library") + id("hedvig.gradle.plugin") +} + +dependencies { + implementation(platform(libs.firebase.bom)) + implementation(libs.coroutines.core) + implementation(libs.firebase.analytics) + implementation(projects.authCorePublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreDemoMode) + implementation(projects.initializable) + implementation(projects.trackingCore) +} diff --git a/app/tracking/tracking-firebase/src/main/AndroidManifest.xml b/app/tracking/tracking-firebase/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/app/tracking/tracking-firebase/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/EventTrackingInitializer.kt b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/EventTrackingInitializer.kt new file mode 100644 index 0000000000..d139508d31 --- /dev/null +++ b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/EventTrackingInitializer.kt @@ -0,0 +1,39 @@ +package com.hedvig.android.tracking.firebase + +import com.hedvig.android.auth.MemberIdService +import com.hedvig.android.core.common.ApplicationScope +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.core.common.di.IoDispatcher +import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.core.tracking.EventTrackingClient +import com.hedvig.android.initializable.Initializable +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +@ContributesIntoSet(AppScope::class) +@SingleIn(AppScope::class) +@Inject +internal class EventTrackingInitializer( + private val eventTrackingClient: EventTrackingClient, + private val demoManager: DemoManager, + private val memberIdService: MemberIdService, + private val applicationScope: ApplicationScope, + @IoDispatcher private val coroutineContext: CoroutineContext, +) : Initializable { + override fun initialize() { + applicationScope.launch(coroutineContext) { + demoManager.isDemoMode().distinctUntilChanged().collect { isDemoMode -> + eventTrackingClient.setCollectionEnabled(!isDemoMode) + } + } + applicationScope.launch(coroutineContext) { + memberIdService.getMemberId().collect { memberId -> + eventTrackingClient.setUserId(memberId) + } + } + } +} diff --git a/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt new file mode 100644 index 0000000000..38fb721f10 --- /dev/null +++ b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt @@ -0,0 +1,62 @@ +package com.hedvig.android.tracking.firebase + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.core.tracking.EventTrackingClient +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +@Inject +internal class FirebaseEventTrackingClient( + applicationContext: Context, +) : EventTrackingClient { + @SuppressLint("MissingPermission") + private val firebaseAnalytics = FirebaseAnalytics.getInstance(applicationContext) + + override fun setCollectionEnabled(enabled: Boolean) { + firebaseAnalytics.setAnalyticsCollectionEnabled(enabled) + } + + override fun trackEvent(name: String, parameters: Map) { + firebaseAnalytics.logEvent(name, parameters.toBundle()) + } + + override fun trackScreen(name: String, parameters: Map) { + val bundle = parameters.toBundle() + bundle.putString(FirebaseAnalytics.Param.SCREEN_NAME, name) + firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle) + } + + override fun setUserId(userId: String?) { + firebaseAnalytics.setUserId(userId) + } + + override fun setUserProperty(name: String, value: String?) { + firebaseAnalytics.setUserProperty(name, value) + } +} + +// Not androidx's bundleOf: Firebase Analytics only accepts String/Long/Double params, so we coerce into those +// (Int->Long, Float->Double, everything else->String) rather than preserve types and throw on the unexpected. +private fun Map.toBundle(): Bundle { + val bundle = Bundle() + for ((key, value) in this) { + when (value) { + null -> Unit + is String -> bundle.putString(key, value) + is Int -> bundle.putLong(key, value.toLong()) + is Long -> bundle.putLong(key, value) + is Float -> bundle.putDouble(key, value.toDouble()) + is Double -> bundle.putDouble(key, value) + is Boolean -> bundle.putString(key, value.toString()) + else -> bundle.putString(key, value.toString()) + } + } + return bundle +} From 773972e4ef8764553175b5f83cfd14056c4ab363 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 23 Jun 2026 13:18:15 +0200 Subject: [PATCH 2/8] Add simple tracking on current destination --- .../com/hedvig/android/app/MainActivity.kt | 5 +++++ .../com/hedvig/android/app/ui/HedvigApp.kt | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt index bbacee25fc..098685b1a0 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt @@ -39,6 +39,7 @@ import com.hedvig.android.core.appreview.WaitUntilAppReviewDialogShouldBeOpenedU import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.demomode.DemoManager import com.hedvig.android.core.rive.RiveInitializer +import com.hedvig.android.core.tracking.EventTrackingClient import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.language.LanguageLaunchCheckUseCase @@ -103,6 +104,9 @@ class MainActivity : AppCompatActivity() { @Inject private lateinit var currentDestinationHolder: CurrentDestinationHolder + @Inject + private lateinit var eventTrackingClient: EventTrackingClient + @Inject private lateinit var serializersModules: Set @@ -245,6 +249,7 @@ class MainActivity : AppCompatActivity() { getMemberAuthorizationCodeUseCase = getMemberAuthorizationCodeUseCase, missedPaymentNotificationService = missedPaymentNotificationService, currentDestinationHolder = currentDestinationHolder, + eventTrackingClient = eventTrackingClient, ) } } diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt index b260fe18ad..257eaf58e5 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt @@ -60,6 +60,7 @@ import com.hedvig.android.compose.ui.LocalSharedTransitionScope import com.hedvig.android.core.appreview.WaitUntilAppReviewDialogShouldBeOpenedUseCase import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.core.tracking.EventTrackingClient import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.design.system.hedvig.DemoModeLabel import com.hedvig.android.design.system.hedvig.Surface @@ -83,6 +84,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import org.jetbrains.compose.resources.stringResource @@ -110,8 +112,10 @@ internal fun HedvigApp( getMemberAuthorizationCodeUseCase: GetMemberAuthorizationCodeUseCase, missedPaymentNotificationService: MissedPaymentNotificationService, currentDestinationHolder: CurrentDestinationHolder, + eventTrackingClient: EventTrackingClient, ) { ReportCurrentDestinationEffect(backstackController, currentDestinationHolder) + TrackScreenViewEffect(backstackController, eventTrackingClient) val hedvigAppState = rememberHedvigAppState( backstackController = backstackController, windowSizeClass = windowSizeClass, @@ -287,6 +291,22 @@ private fun ReportCurrentDestinationEffect( } } +/** + * Sends a Firebase `screen_view` whenever the destination on top of the rendered stack changes, deriving the screen + * name from the key type (the `{Feature}Key` suffix is dropped). + */ +@Composable +private fun TrackScreenViewEffect(backstackController: BackstackController, eventTrackingClient: EventTrackingClient) { + LaunchedEffect(backstackController, eventTrackingClient) { + snapshotFlow { backstackController.currentDestination } + .filterNotNull() + .collect { destination -> + val screenName = destination::class.simpleName?.removeSuffix("Key") ?: destination.toString() + eventTrackingClient.trackScreen(screenName) + } + } +} + /** * Temporary measure as both design systems need to live side-by-side. * When everything can come from com.hedvig.android.design.system.hedvig, then this can potentially be removed. From f9ceb8ddc45f73aa2faedffab81011a32616d479 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 23 Jun 2026 13:22:34 +0200 Subject: [PATCH 3/8] Add screen class in firebase analytics screen tracking --- .../src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt | 5 +++-- .../com/hedvig/android/core/tracking/EventTrackingClient.kt | 2 +- .../android/tracking/firebase/FirebaseEventTrackingClient.kt | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt index 257eaf58e5..0cde40ec36 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt @@ -301,8 +301,9 @@ private fun TrackScreenViewEffect(backstackController: BackstackController, even snapshotFlow { backstackController.currentDestination } .filterNotNull() .collect { destination -> - val screenName = destination::class.simpleName?.removeSuffix("Key") ?: destination.toString() - eventTrackingClient.trackScreen(screenName) + val screenClass = destination::class.simpleName ?: destination.toString() + val screenName = screenClass.removeSuffix("Key") + eventTrackingClient.trackScreen(name = screenName, screenClass = screenClass) } } } diff --git a/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt b/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt index 565d070cbd..a2284c31a6 100644 --- a/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt +++ b/app/tracking/tracking-core/src/commonMain/kotlin/com/hedvig/android/core/tracking/EventTrackingClient.kt @@ -16,7 +16,7 @@ interface EventTrackingClient { fun trackEvent(name: String, parameters: Map = emptyMap()) - fun trackScreen(name: String, parameters: Map = emptyMap()) + fun trackScreen(name: String, screenClass: String? = null, parameters: Map = emptyMap()) fun setUserId(userId: String?) diff --git a/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt index 38fb721f10..3b2cb3fca6 100644 --- a/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt +++ b/app/tracking/tracking-firebase/src/main/kotlin/com/hedvig/android/tracking/firebase/FirebaseEventTrackingClient.kt @@ -27,9 +27,12 @@ internal class FirebaseEventTrackingClient( firebaseAnalytics.logEvent(name, parameters.toBundle()) } - override fun trackScreen(name: String, parameters: Map) { + override fun trackScreen(name: String, screenClass: String?, parameters: Map) { val bundle = parameters.toBundle() bundle.putString(FirebaseAnalytics.Param.SCREEN_NAME, name) + if (screenClass != null) { + bundle.putString(FirebaseAnalytics.Param.SCREEN_CLASS, screenClass) + } firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle) } From 2dfdf25aa72a8604a14692e6d4608e6ed6c34c76 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 23 Jun 2026 14:06:40 +0200 Subject: [PATCH 4/8] download latest google-services.json for debug app --- app/app/src/debug/google-services.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/src/debug/google-services.json b/app/app/src/debug/google-services.json index 73bca87b36..e6aaca5231 100644 --- a/app/app/src/debug/google-services.json +++ b/app/app/src/debug/google-services.json @@ -2,7 +2,7 @@ "project_info": { "project_number": "190369814613", "project_id": "hedvig-dev", - "storage_bucket": "hedvig-dev.appspot.com" + "storage_bucket": "hedvig-dev.firebasestorage.app" }, "client": [ { From 188cf7110fe31afefeafbb7e34215cceae75e2b0 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 23 Jun 2026 14:07:13 +0200 Subject: [PATCH 5/8] Add parameter extracton on screen tracking Add TrackedScreen marker interface to allow specific screens to opt-into special tracking handling --- .../com/hedvig/android/app/MainActivity.kt | 5 ++ .../navigation/ScreenParameterExtractor.kt | 63 +++++++++++++++++++ .../com/hedvig/android/app/ui/HedvigApp.kt | 22 +++++-- .../android/navigation/common/HedvigNavKey.kt | 10 +++ 4 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 app/app/src/main/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractor.kt diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt index 098685b1a0..5933b9d267 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt @@ -30,6 +30,7 @@ import com.hedvig.android.app.crosssell.GetMemberAuthorizationCodeUseCase import com.hedvig.android.app.externalnavigator.ExternalNavigatorImpl import com.hedvig.android.app.navigation.CurrentDestinationHolder import com.hedvig.android.app.navigation.NavRetainedViewModel +import com.hedvig.android.app.navigation.ScreenParameterExtractor import com.hedvig.android.app.ui.HedvigApp import com.hedvig.android.app.urihandler.ExternalDeepLinkHandler import com.hedvig.android.auth.AuthTokenService @@ -107,6 +108,9 @@ class MainActivity : AppCompatActivity() { @Inject private lateinit var eventTrackingClient: EventTrackingClient + @Inject + private lateinit var screenParameterExtractor: ScreenParameterExtractor + @Inject private lateinit var serializersModules: Set @@ -250,6 +254,7 @@ class MainActivity : AppCompatActivity() { missedPaymentNotificationService = missedPaymentNotificationService, currentDestinationHolder = currentDestinationHolder, eventTrackingClient = eventTrackingClient, + screenParameterExtractor = screenParameterExtractor, ) } } diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractor.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractor.kt new file mode 100644 index 0000000000..967bc4b6f8 --- /dev/null +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractor.kt @@ -0,0 +1,63 @@ +package com.hedvig.android.app.navigation + +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.navigation.common.HedvigNavKey +import com.hedvig.android.navigation.common.TrackedScreen +import com.hedvig.android.navigation.compose.merge +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import kotlinx.serialization.PolymorphicSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.modules.SerializersModule + +/** + * Derives the analytics parameters attached to a destination's `screen_view`. By default it reflects + * the key's own [kotlinx.serialization]-serialized properties (every `@Serializable` `val` becomes a + * parameter), reusing the same merged [SerializersModule]s that persist the back stack — so any key + * that survives process death automatically has reportable parameters with no per-screen wiring. A key + * may instead implement [TrackedScreen] to take over entirely. + * + * Parameters ride along the single `screen_view` event keyed by screen name, so they act as breakdown + * dimensions rather than fragmenting a screen into separate entries. + */ +@SingleIn(AppScope::class) +@Inject +internal class ScreenParameterExtractor( + serializersModules: Set, +) { + private val json = Json { + serializersModule = serializersModules.merge() + encodeDefaults = true + } + + fun parametersFor(destination: HedvigNavKey): Map { + if (destination is TrackedScreen) { + return destination.screenParameters + } + val element = runCatching { + json.encodeToJsonElement(PolymorphicSerializer(HedvigNavKey::class), destination) + }.getOrNull() as? JsonObject ?: return emptyMap() + return element + .filterKeys { it != CLASS_DISCRIMINATOR } + .mapValues { (_, value) -> value.toPrimitiveOrNull() } + } + + // The default polymorphic discriminator kotlinx.serialization writes to identify the concrete key + // type; it duplicates the screen name/class, so it is dropped from the reported parameters. + private companion object { + const val CLASS_DISCRIMINATOR = "type" + } +} + +private fun JsonElement.toPrimitiveOrNull(): Any? = when (this) { + is JsonNull -> null + is JsonPrimitive -> if (isString) content else (booleanOrNull ?: longOrNull ?: doubleOrNull ?: content) + else -> toString() +} diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt index 0cde40ec36..2d8b39d678 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt @@ -47,6 +47,7 @@ import com.hedvig.android.app.GlobalHedvigSnackBar import com.hedvig.android.app.crosssell.GetMemberAuthorizationCodeUseCase import com.hedvig.android.app.navigation.BackstackController import com.hedvig.android.app.navigation.CurrentDestinationHolder +import com.hedvig.android.app.navigation.ScreenParameterExtractor import com.hedvig.android.app.navigation.hedvigEntryProvider import com.hedvig.android.app.navigation.shouldFadeThrough import com.hedvig.android.app.urihandler.AuthorizationCodeUriHandler @@ -113,9 +114,10 @@ internal fun HedvigApp( missedPaymentNotificationService: MissedPaymentNotificationService, currentDestinationHolder: CurrentDestinationHolder, eventTrackingClient: EventTrackingClient, + screenParameterExtractor: ScreenParameterExtractor, ) { ReportCurrentDestinationEffect(backstackController, currentDestinationHolder) - TrackScreenViewEffect(backstackController, eventTrackingClient) + TrackScreenViewEffect(backstackController, eventTrackingClient, screenParameterExtractor) val hedvigAppState = rememberHedvigAppState( backstackController = backstackController, windowSizeClass = windowSizeClass, @@ -293,17 +295,27 @@ private fun ReportCurrentDestinationEffect( /** * Sends a Firebase `screen_view` whenever the destination on top of the rendered stack changes, deriving the screen - * name from the key type (the `{Feature}Key` suffix is dropped). + * name from the key type (the `{Feature}Key` suffix is dropped) and the parameters from + * [ScreenParameterExtractor]. Parameters ride along the single `screen_view` event keyed by screen name, acting as + * breakdown dimensions rather than fragmenting a screen into separate entries. */ @Composable -private fun TrackScreenViewEffect(backstackController: BackstackController, eventTrackingClient: EventTrackingClient) { - LaunchedEffect(backstackController, eventTrackingClient) { +private fun TrackScreenViewEffect( + backstackController: BackstackController, + eventTrackingClient: EventTrackingClient, + screenParameterExtractor: ScreenParameterExtractor, +) { + LaunchedEffect(backstackController, eventTrackingClient, screenParameterExtractor) { snapshotFlow { backstackController.currentDestination } .filterNotNull() .collect { destination -> val screenClass = destination::class.simpleName ?: destination.toString() val screenName = screenClass.removeSuffix("Key") - eventTrackingClient.trackScreen(name = screenName, screenClass = screenClass) + eventTrackingClient.trackScreen( + name = screenName, + screenClass = screenClass, + parameters = screenParameterExtractor.parametersFor(destination), + ) } } } diff --git a/app/navigation/navigation-common/src/commonMain/kotlin/com/hedvig/android/navigation/common/HedvigNavKey.kt b/app/navigation/navigation-common/src/commonMain/kotlin/com/hedvig/android/navigation/common/HedvigNavKey.kt index 4926a32a41..474ff428bc 100644 --- a/app/navigation/navigation-common/src/commonMain/kotlin/com/hedvig/android/navigation/common/HedvigNavKey.kt +++ b/app/navigation/navigation-common/src/commonMain/kotlin/com/hedvig/android/navigation/common/HedvigNavKey.kt @@ -25,3 +25,13 @@ interface SuppressesChatPushNotification * `destinationToExcludeFromSavingState`. */ interface DeliberateLogoutOrigin + +/** + * Opt-in override for the analytics parameters sent with a key's `screen_view` event. By default, the + * tracker reflects a key's own serialized properties as parameters; a key implementing this takes full + * control instead, returning exactly the parameters to attach. Use it when the serialized shape isn't + * what you want reported — to rename, drop, derive, or coarsen high-cardinality values. + */ +interface TrackedScreen { + val screenParameters: Map +} From 5875d0e56910976526cb2ca508b392a21254655b Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 24 Jun 2026 14:34:08 +0200 Subject: [PATCH 6/8] Add `ScreenParameterExtractor` tests --- .../ScreenParameterExtractorTest.kt | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/app/src/test/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractorTest.kt diff --git a/app/app/src/test/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractorTest.kt b/app/app/src/test/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractorTest.kt new file mode 100644 index 0000000000..69d693f56d --- /dev/null +++ b/app/app/src/test/kotlin/com/hedvig/android/app/navigation/ScreenParameterExtractorTest.kt @@ -0,0 +1,90 @@ +package com.hedvig.android.app.navigation + +import assertk.assertThat +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import com.hedvig.android.navigation.common.HedvigNavKey +import com.hedvig.android.navigation.common.TrackedScreen +import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import org.junit.Test + +internal class ScreenParameterExtractorTest { + private val extractor = ScreenParameterExtractor( + setOf( + SerializersModule { + polymorphic(HedvigNavKey::class) { + subclass(SimpleKey::class) + subclass(EmptyKey::class) + subclass(NullableKey::class) + subclass(NestedKey::class) + subclass(OverridingKey::class) + } + }, + ), + ) + + @Test + fun `primitive properties are flattened and coerced to Firebase-compatible types`() { + val params = extractor.parametersFor(SimpleKey(id = "abc", count = 42, enabled = true)) + + assertThat(params).isEqualTo( + mapOf( + "id" to "abc", + "count" to 42L, // Int coerces to Long + "enabled" to true, + ), + ) + } + + @Test + fun `the polymorphic type discriminator is dropped`() { + val params = extractor.parametersFor(EmptyKey) + + assertThat(params).isEmpty() + } + + @Test + fun `null-valued properties are preserved as null`() { + val params = extractor.parametersFor(NullableKey(maybe = null)) + + assertThat(params).isEqualTo(mapOf("maybe" to null)) + } + + @Test + fun `nested objects fall back to their JSON string`() { + val params = extractor.parametersFor(NestedKey(inner = Inner(a = "x"))) + + assertThat(params).isEqualTo(mapOf("inner" to """{"a":"x"}""")) + } + + @Test + fun `a TrackedScreen takes over with its own parameters, ignoring serialized shape`() { + val params = extractor.parametersFor(OverridingKey(secret = "should-not-leak")) + + assertThat(params).isEqualTo(mapOf("custom" to "value")) + } +} + +@Serializable +private data class SimpleKey(val id: String, val count: Int, val enabled: Boolean) : HedvigNavKey + +@Serializable +private data object EmptyKey : HedvigNavKey + +@Serializable +private data class NullableKey(val maybe: String?) : HedvigNavKey + +@Serializable +private data class NestedKey(val inner: Inner) : HedvigNavKey + +@Serializable +private data class Inner(val a: String) + +@Serializable +private data class OverridingKey(val secret: String) : HedvigNavKey, TrackedScreen { + override val screenParameters: Map + get() = mapOf("custom" to "value") +} From f47f3001537c392c2fff360272227874b0c1292e Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 24 Jun 2026 15:17:53 +0200 Subject: [PATCH 7/8] Run lint --- .../lint-baseline-tracking-firebase.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 hedvig-lint/lint-baseline/lint-baseline-tracking-firebase.xml diff --git a/hedvig-lint/lint-baseline/lint-baseline-tracking-firebase.xml b/hedvig-lint/lint-baseline/lint-baseline-tracking-firebase.xml new file mode 100644 index 0000000000..37588e7a01 --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-tracking-firebase.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + From a4aa07748d7ee5e5f2627d6369b054bef55d1523 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 24 Jun 2026 15:55:41 +0200 Subject: [PATCH 8/8] Run ktlint --- .../android/data/contract/ContractType.kt | 2 +- .../feature/chat/inbox/InboxDestination.kt | 4 +- .../feature/chat/inbox/MarkdownPlainText.kt | 2 +- .../feature/claim/chat/data/ClaimIntentExt.kt | 8 ++-- .../claim/chat/ui/ClaimChatDestination.kt | 8 ++-- .../sell/sheet/CrossSellSheetViewModel.kt | 2 +- .../ContractDetailDestination.kt | 20 ++++++---- .../UpcomingChangesBottomSheetContent.kt | 2 +- .../insurancedetail/yourinfo/YourInfoTab.kt | 4 +- .../android/feature/payments/PreviewData.kt | 2 +- .../payments/data/PaymentConnection.kt | 2 +- .../feature/payments/data/PaymentOverview.kt | 2 +- .../data/GetUpcomingPaymentUseCase.kt | 40 ++++++++++++------- .../ui/payments/PaymentsDestination.kt | 13 +++--- .../payments/ui/payments/PaymentsPresenter.kt | 5 ++- .../GetConnectPaymentReminderUseCase.kt | 6 +-- .../GetConnectPaymentReminderUseCaseTest.kt | 2 +- 17 files changed, 72 insertions(+), 52 deletions(-) diff --git a/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt index 018d0c7bed..8dd30a09d1 100644 --- a/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt +++ b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt @@ -134,7 +134,7 @@ fun ContractType.isTrialContract() = when (this) { SE_DOG_PREMIUM, SE_DOG_STANDARD, UNKNOWN, - ContractType.SE_QASA_LANDLORD + ContractType.SE_QASA_LANDLORD, -> false SE_CAR_TRIAL_FULL, diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt index da8da27e1e..de361a4e25 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt @@ -424,7 +424,7 @@ private fun ConversationCard( HedvigText( text = formattedLastMessageSent, style = HedvigTheme.typography.label, - color = HedvigTheme.colorScheme.textSecondary + color = HedvigTheme.colorScheme.textSecondary, ) } } @@ -576,7 +576,7 @@ private val mockInboxConversation2 = InboxConversation( private val mockInboxConversation3 = InboxConversation( conversationId = "3", header = Header.ServiceConversation, - latestMessage = Text ("Thank you! Happy to hear that!",Sender.MEMBER, Clock.System.now()), + latestMessage = Text("Thank you! Happy to hear that!", Sender.MEMBER, Clock.System.now()), hasNewMessages = false, createdAt = Clock.System.now(), isClosed = true, diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt index c00b1f2ef6..b2fb7d5ab3 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt @@ -21,4 +21,4 @@ internal fun String.markdownToPlainText(): String { .replace("&", "&") // decode last so we don't re-interpret a decoded entity .replace(Regex("\\s+"), " ") .trim() -} \ No newline at end of file +} diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 191359eb00..9848835497 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -182,9 +182,11 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): ) } - is DeflectionMessageFragment -> StepContent.DeflectMessage( - message = message - ) + is DeflectionMessageFragment -> { + StepContent.DeflectMessage( + message = message, + ) + } else -> { logcat { "ClaimIntentStepContentFragment: Unknown step" } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index 4da169403e..b998d9d84f 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -404,7 +404,7 @@ private fun ClaimChatScreenContent( } else { navigateUp() } - } + }, ) } if (showScrollArrow) { @@ -505,7 +505,7 @@ private fun ClaimChatScrollableContent( } else { Modifier }, - closeFlow = closeFlow + closeFlow = closeFlow, ) } } @@ -767,7 +767,7 @@ private fun StepBottomContent( appPackageId: String, imageLoader: ImageLoader, openAppSettings: () -> Unit, - closeFlow: ()-> Unit, + closeFlow: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier) { @@ -906,7 +906,7 @@ private fun StepBottomContent( closeFlow() }, enabled = true, - buttonStyle = ButtonDefaults.ButtonStyle.Secondary + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, ) } } diff --git a/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt b/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt index 93862f4833..f1168ec236 100644 --- a/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt +++ b/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt @@ -14,8 +14,8 @@ import com.apollographql.apollo.api.Optional import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.ErrorMessage -import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.core.common.di.ActivityRetainedScope +import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.core.common.di.HedvigViewModel import com.hedvig.android.core.demomode.DemoManager import com.hedvig.android.core.demomode.DemoSwitcher diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt index 6d6fff47a3..29df5515bc 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt @@ -287,7 +287,7 @@ private fun ContractDetailScreen( ) { pageIndex -> when (pageIndex) { 0 -> { - val priceInfoForBottomSheet = if (cost!=null) { + val priceInfoForBottomSheet = if (cost != null) { PriceInfoForBottomSheet( displayItems = buildList { add( @@ -300,9 +300,9 @@ private fun ContractDetailScreen( add( addon.addonVariant.displayName to stringResource( - Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, - addon.premium.toString(), - ), + Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, + addon.premium.toString(), + ), ) } cost.discounts.forEach { discount -> @@ -312,7 +312,9 @@ private fun ContractDetailScreen( totalGross = cost.monthlyGross, totalNet = cost.monthlyNet, ) - } else null + } else { + null + } YourInfoTab( contractId = contract.id, coverageItems = contract.displayItems, @@ -355,10 +357,12 @@ private fun ContractDetailScreen( contractHolderDisplayName = contract.contractHolderDisplayName, contractHolderSSN = contract.contractHolderSSN, priceToShow = cost?.monthlyNet, - showPriceInfoIcon = cost!=null && (cost.monthlyNet != cost.monthlyGross || - contract.basePremium != cost.monthlyNet), + showPriceInfoIcon = cost != null && ( + cost.monthlyNet != cost.monthlyGross || + contract.basePremium != cost.monthlyNet + ), onInfoIconClick = { - if (priceInfoForBottomSheet!=null) { + if (priceInfoForBottomSheet != null) { costBreakdownBottomSheetState.show(priceInfoForBottomSheet) } }, diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt index c511c649c7..653ce7c214 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt @@ -74,7 +74,7 @@ internal fun UpcomingChangesBottomSheetContent( onInfoIconClick = { priceInfoBottomSheetState.show(it) }, - isFirstRow = sections.isEmpty() + isFirstRow = sections.isEmpty(), ) } Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt index 636435fa57..937adc7a5e 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt @@ -702,8 +702,8 @@ internal fun PriceRow( ) { HorizontalItemsWithMaximumSpaceTaken( modifier = modifier - .then ( - if (isFirstRow) Modifier else Modifier.horizontalDivider(DividerPosition.Top) + .then( + if (isFirstRow) Modifier else Modifier.horizontalDivider(DividerPosition.Top), ), startSlot = { Row( diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt index 44849f0661..da7403e007 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt @@ -281,7 +281,7 @@ internal val paymentOverViewPreviewData: PaymentOverview ongoingCharges = listOf(OngoingCharge("id", LocalDate.fromEpochDays(401), UiMoney(200.0, UiCurrencyCode.SEK))), paymentConnection = PaymentConnection.Active, isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)), - memberType = MemberType.STANDARD_MEMBER + memberType = MemberType.STANDARD_MEMBER, ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt index 5646b106e1..7a858beb23 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt @@ -11,7 +11,7 @@ sealed interface PaymentConnection { val terminationDateIfNotConnected: LocalDate?, ) : PaymentConnection - data object NeedsPayoutSetup: PaymentConnection + data object NeedsPayoutSetup : PaymentConnection data object Unknown : PaymentConnection } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt index 51369543ef..3952ee0e73 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt @@ -9,7 +9,7 @@ data class PaymentOverview( val ongoingCharges: List, val paymentConnection: PaymentConnection, val isManualChargeAllowed: ManualChargeToPrompt?, - val memberType: MemberType + val memberType: MemberType, ) { data class OngoingCharge( val id: String, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt index a980677b6a..7f04aa9ce6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt @@ -77,7 +77,7 @@ internal data class GetUpcomingPaymentUseCaseImpl( val paymentMethods = result.currentMember.paymentMethods val payinMethod = paymentMethods.defaultPayinMethod ?: paymentMethods.payinMethods.find { it.isDefault } - logcat {"Mariia: payinMethod $payinMethod"} + logcat { "Mariia: payinMethod $payinMethod" } val payoutMethod = paymentMethods.defaultPayoutMethod ?: paymentMethods.payoutMethods.find { it.isDefault } if (payinMethod == null) { @@ -90,35 +90,47 @@ internal data class GetUpcomingPaymentUseCaseImpl( .firstOrNull() when (memberType) { - - MemberType.STANDARD_MEMBER -> return@run NeedsPayinSetup( - firstKnownTerminationDateForContractTerminatedDueToMissedPayments, - ) + MemberType.STANDARD_MEMBER -> { + return@run NeedsPayinSetup( + firstKnownTerminationDateForContractTerminatedDueToMissedPayments, + ) + } MemberType.QASA_ONLY_MEMBER -> { if (payoutMethod == null) { return@run PaymentConnection.NeedsPayoutSetup - } else return@run PaymentConnection.Active + } else { + return@run PaymentConnection.Active + } } - MemberType.STANDARD_TO_QASA_MEMBER -> TODO() + MemberType.STANDARD_TO_QASA_MEMBER -> { + TODO() + } } } when (payinMethod.status) { MemberPaymentMethodStatus.ACTIVE -> { - logcat {"Mariia: MemberPaymentMethodStatus.ACTIVE"} - logcat {"Mariia: payoutMethod $payoutMethod"} + logcat { "Mariia: MemberPaymentMethodStatus.ACTIVE" } + logcat { "Mariia: payoutMethod $payoutMethod" } if (payoutMethod == null) { return@run PaymentConnection.NeedsPayoutSetup - } else return@run PaymentConnection.Active + } else { + return@run PaymentConnection.Active + } } - MemberPaymentMethodStatus.PENDING -> PaymentConnection.Pending - MemberPaymentMethodStatus.UNKNOWN__ -> PaymentConnection.Unknown + MemberPaymentMethodStatus.PENDING -> { + PaymentConnection.Pending + } + + MemberPaymentMethodStatus.UNKNOWN__ -> { + PaymentConnection.Unknown + } } }, isManualChargeAllowed = isManualChargeAllowed, - memberType = memberType + memberType = memberType, ) } } @@ -153,7 +165,7 @@ internal class GetUpcomingPaymentUseCaseDemo( emptyList(), PaymentConnection.Unknown, isManualChargeAllowed = null, - memberType = MemberType.STANDARD_MEMBER + memberType = MemberType.STANDARD_MEMBER, ).right() } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt index 9437a42547..5d674040e5 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -347,7 +347,7 @@ private fun PaymentsContent( ConnectedPaymentInfo.Unknown, is ConnectedPaymentInfo.Active, - -> {} + -> {} } } @@ -440,7 +440,7 @@ private fun PaymentsListItems( val showDiscountsItem = when (uiState.memberType) { MemberType.QASA_ONLY_MEMBER, MemberType.STANDARD_TO_QASA_MEMBER, - -> false + -> false MemberType.STANDARD_MEMBER -> true } @@ -464,12 +464,12 @@ private fun PaymentsListItems( HorizontalDivider(modifier = listItemsSideSpacingModifier) } - val showPaymentHistoryItem = when (uiState.memberType) { MemberType.QASA_ONLY_MEMBER -> false + MemberType.STANDARD_MEMBER, MemberType.STANDARD_TO_QASA_MEMBER, - -> true + -> true } if (showPaymentHistoryItem) { PaymentsListItem( @@ -492,9 +492,10 @@ private fun PaymentsListItems( val showPaymentDetailsItem = when (uiState.memberType) { MemberType.QASA_ONLY_MEMBER -> false + MemberType.STANDARD_MEMBER, MemberType.STANDARD_TO_QASA_MEMBER, - -> uiState.connectedPaymentInfo is ConnectedPaymentInfo.Active + -> uiState.connectedPaymentInfo is ConnectedPaymentInfo.Active } if (showPaymentDetailsItem) { PaymentsListItem( @@ -518,7 +519,7 @@ private fun PaymentsListItems( val showPayoutItem = when (uiState.memberType) { MemberType.QASA_ONLY_MEMBER, MemberType.STANDARD_TO_QASA_MEMBER, - -> true + -> true MemberType.STANDARD_MEMBER -> showPayoutButton } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt index ff386900df..c37975999b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt @@ -125,6 +125,7 @@ private fun PaymentConnection.toConnectedPaymentInfo(): ConnectedPaymentInfo { ) Unknown -> ConnectedPaymentInfo.Unknown + PaymentConnection.NeedsPayoutSetup -> ConnectedPaymentInfo.NeedsPayoutSetup } } @@ -145,7 +146,7 @@ internal sealed interface PaymentsUiState { val ongoingCharges: List, val connectedPaymentInfo: ConnectedPaymentInfo, val showPayoutButton: Boolean, - val memberType: MemberType + val memberType: MemberType, ) : PaymentsUiState { sealed interface UpcomingPayment { data object NoUpcomingPayment : UpcomingPayment @@ -180,7 +181,7 @@ internal sealed interface PaymentsUiState { data object Active : ConnectedPaymentInfo - data object NeedsPayoutSetup: ConnectedPaymentInfo + data object NeedsPayoutSetup : ConnectedPaymentInfo } } } diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt index 2bfd778bd3..71021f87a8 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt @@ -26,7 +26,8 @@ internal interface GetConnectPaymentReminderUseCase { @SingleIn(AppScope::class) @Inject internal class GetConnectPaymentReminderUseCaseImpl( - private val apolloClient: ApolloClient) : GetConnectPaymentReminderUseCase { + private val apolloClient: ApolloClient, +) : GetConnectPaymentReminderUseCase { override suspend fun invoke(): Either { return either { val result = apolloClient.query(GetPayinMethodStatusQuery()) @@ -48,9 +49,8 @@ internal class GetConnectPaymentReminderUseCaseImpl( when (missingConnection) { MissingPaymentConnection.PAYIN -> return@either PaymentReminder.ShowConnectPaymentReminder MissingPaymentConnection.PAYOUT -> return@either PaymentReminder.ShowConnectPayoutReminder - MissingPaymentConnection.UNKNOWN__ , null -> raise(ConnectPaymentReminderError.DomainError.AlreadySetup) + MissingPaymentConnection.UNKNOWN__, null -> raise(ConnectPaymentReminderError.DomainError.AlreadySetup) } - }.onLeft { if (it !is ConnectPaymentReminderError.DomainError) { logcat { "GetConnectPaymentReminderUseCase failed with error:$it" } diff --git a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt index 10568df5eb..511d4e5763 100644 --- a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt +++ b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt @@ -129,4 +129,4 @@ class GetConnectPaymentReminderUseCaseTest { .isLeft() .isInstanceOf() } -} \ No newline at end of file +}