Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import com.hedvig.android.core.common.di.HedvigViewModel
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.core.demomode.DemoSwitcher
import com.hedvig.android.crosssells.BundleProgress
import com.hedvig.android.crosssells.CrossSellFlowSource
import com.hedvig.android.crosssells.CrossSellImpressionTracker
import com.hedvig.android.crosssells.CrossSellSheetData
import com.hedvig.android.crosssells.CrossSellUserFlow
import com.hedvig.android.crosssells.RecommendedAddon
import com.hedvig.android.crosssells.RecommendedCrossSell
import com.hedvig.android.crosssells.crossSellSheetShown
import com.hedvig.android.data.contract.CrossSell
import com.hedvig.android.data.contract.ImageAsset
import com.hedvig.android.data.cross.sell.after.flow.CrossSellAfterFlowRepository
Expand Down Expand Up @@ -55,9 +59,10 @@ import octopus.type.UserFlow
internal class CrossSellSheetViewModel(
getCrossSellSheetDataUseCase: GetCrossSellSheetDataUseCase,
crossSellAfterFlowRepository: CrossSellAfterFlowRepository,
crossSellImpressionTracker: CrossSellImpressionTracker,
) : MoleculeViewModel<CrossSellSheetEvent, CrossSellSheetState>(
CrossSellSheetState.Loading,
CrossSellSheetPresenter(getCrossSellSheetDataUseCase, crossSellAfterFlowRepository),
CrossSellSheetPresenter(getCrossSellSheetDataUseCase, crossSellAfterFlowRepository, crossSellImpressionTracker),
)

internal sealed interface CrossSellSheetEvent {
Expand All @@ -77,6 +82,7 @@ internal sealed interface CrossSellSheetState {
internal class CrossSellSheetPresenter(
private val getCrossSellSheetDataUseCase: GetCrossSellSheetDataUseCase,
private val crossSellAfterFlowRepository: CrossSellAfterFlowRepository,
private val crossSellImpressionTracker: CrossSellImpressionTracker,
) : MoleculePresenter<CrossSellSheetEvent, CrossSellSheetState> {
@Composable
override fun MoleculePresenterScope<CrossSellSheetEvent>.present(
Expand All @@ -87,7 +93,15 @@ internal class CrossSellSheetPresenter(
CollectEvents { event ->
when (event) {
is CrossSellSheetEvent.CrossSellSheetShown -> {
crossSellAfterFlowRepository.showedCrossSellSheet((state as? CrossSellSheetState.Content)?.infoType)
val content = state as? CrossSellSheetState.Content
crossSellAfterFlowRepository.showedCrossSellSheet(content?.infoType)
if (content != null) {
crossSellImpressionTracker.crossSellSheetShown(
data = content.crossSellSheetData,
userFlow = CrossSellUserFlow.SmartXSell,
flowSource = content.infoType.toFlowSource(),
)
}
}
}
}
Expand Down Expand Up @@ -121,6 +135,16 @@ internal class CrossSellSheetPresenter(
}
}

internal fun CrossSellInfoType.toFlowSource(): CrossSellFlowSource {
return when (this) {
CrossSellInfoType.Addon -> CrossSellFlowSource.Addon
is CrossSellInfoType.ChangeTier -> CrossSellFlowSource.ChangeTier
is CrossSellInfoType.ClosedClaim -> CrossSellFlowSource.ClosedClaim
CrossSellInfoType.EditCoInsured -> CrossSellFlowSource.EditCoInsured
is CrossSellInfoType.MovingFlow -> CrossSellFlowSource.Moving
}
}

internal fun CrossSellInfoType.toCrossSellSource(): CrossSellInput {
val smartCrossSellInput: (FlowSource) -> CrossSellInput = { flowSource ->
CrossSellInput(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import arrow.core.right
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.crosssells.CrossSellFlowSource
import com.hedvig.android.crosssells.CrossSellImpressionTracker
import com.hedvig.android.crosssells.CrossSellSheetData
import com.hedvig.android.crosssells.CrossSellType
import com.hedvig.android.crosssells.CrossSellUserFlow
import com.hedvig.android.crosssells.RecommendedAddon
import com.hedvig.android.data.contract.CrossSell
import com.hedvig.android.data.contract.ImageAsset
import com.hedvig.android.data.cross.sell.after.flow.CrossSellAfterFlowRepository
import com.hedvig.android.data.cross.sell.after.flow.CrossSellInfoType
import com.hedvig.android.molecule.test.test
Expand Down Expand Up @@ -36,6 +42,7 @@ internal class CrossSellSheetPresenterTest {
val presenter = CrossSellSheetPresenter(
FakeGetCrossSellSheetDataUseCase(emptyData),
FakeCrossSellAfterFlowRepository(CrossSellInfoType.EditCoInsured),
RecordingCrossSellImpressionTracker(),
)

presenter.test(CrossSellSheetState.Loading) {
Expand All @@ -50,6 +57,7 @@ internal class CrossSellSheetPresenterTest {
val presenter = CrossSellSheetPresenter(
FakeGetCrossSellSheetDataUseCase(addonData),
FakeCrossSellAfterFlowRepository(CrossSellInfoType.EditCoInsured),
RecordingCrossSellImpressionTracker(),
)

presenter.test(CrossSellSheetState.Loading) {
Expand All @@ -59,6 +67,72 @@ internal class CrossSellSheetPresenterTest {
)
}
}

@Test
fun `showing the sheet tracks an impression per shown offer`() = runTest {
val newPromise = CrossSell(
id = "cs1",
title = "Car",
subtitle = "Car insurance",
storeUrl = "url",
pillowImage = ImageAsset(id = "img", src = "src", description = "alt"),
)
val data = CrossSellSheetData(
recommendedCrossSell = null,
otherCrossSells = listOf(newPromise),
recommendedAddon = recommendedAddon,
)
val tracker = RecordingCrossSellImpressionTracker()
val presenter = CrossSellSheetPresenter(
FakeGetCrossSellSheetDataUseCase(data),
FakeCrossSellAfterFlowRepository(CrossSellInfoType.EditCoInsured),
tracker,
)

presenter.test(CrossSellSheetState.Loading) {
assertThat(awaitItem()).isEqualTo(CrossSellSheetState.Loading)
assertThat(awaitItem()).isEqualTo(CrossSellSheetState.Content(data, CrossSellInfoType.EditCoInsured))
sendEvent(CrossSellSheetEvent.CrossSellSheetShown)
assertThat(awaitItem()).isEqualTo(CrossSellSheetState.DontShow)
}

assertThat(tracker.shown).isEqualTo(
listOf(
RecordingCrossSellImpressionTracker.Impression(
CrossSellUserFlow.SmartXSell,
CrossSellType.Addon,
"addonId",
CrossSellFlowSource.EditCoInsured,
),
RecordingCrossSellImpressionTracker.Impression(
CrossSellUserFlow.SmartXSell,
CrossSellType.NewPromise,
"cs1",
CrossSellFlowSource.EditCoInsured,
),
),
)
}
}

private class RecordingCrossSellImpressionTracker : CrossSellImpressionTracker {
data class Impression(
val userFlow: CrossSellUserFlow,
val crossSellType: CrossSellType,
val offerId: String,
val flowSource: CrossSellFlowSource?,
)

val shown = mutableListOf<Impression>()

override fun crossSellShown(
userFlow: CrossSellUserFlow,
crossSellType: CrossSellType,
offerId: String,
flowSource: CrossSellFlowSource?,
) {
shown += Impression(userFlow, crossSellType, offerId, flowSource)
}
}

private class FakeGetCrossSellSheetDataUseCase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onVisibilityChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
Expand All @@ -88,8 +89,11 @@ import com.google.accompanist.permissions.isGranted
import com.hedvig.android.compose.ui.plus
import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider
import com.hedvig.android.crosssells.BundleProgress
import com.hedvig.android.crosssells.CROSS_SELL_IMPRESSION_MIN_DURATION_MS
import com.hedvig.android.crosssells.CROSS_SELL_IMPRESSION_MIN_FRACTION_VISIBLE
import com.hedvig.android.crosssells.CrossSellBottomSheet
import com.hedvig.android.crosssells.CrossSellSheetData
import com.hedvig.android.crosssells.CrossSellType
import com.hedvig.android.crosssells.CrossSellsSection
import com.hedvig.android.crosssells.PillowRow
import com.hedvig.android.crosssells.RecommendedCrossSell
Expand Down Expand Up @@ -245,6 +249,15 @@ internal fun HomeDestination(
deleteDraftClaim = { draftId -> viewModel.emit(HomeEvent.DeleteDraftClaim(draftId)) },
navigateToFirstVet = navigateToFirstVet,
markCrossSellsNotificationAsSeen = { viewModel.emit(HomeEvent.MarkCardCrossSellsAsSeen) },
onCrossSellsShown = { viewModel.emit(HomeEvent.CrossSellsShown(it)) },
onHomeCrossSellImpression = { crossSell ->
viewModel.emit(HomeEvent.HomeCrossSellImpression(crossSell.id, CrossSellType.NewPromise))
},
onHomeAddonImpression = { bannerInfo ->
// Addon banners carry no per-offer id, so the flow type (lowercased to match enum-value casing)
// identifies the addon offer.
viewModel.emit(HomeEvent.HomeCrossSellImpression(bannerInfo.flowType.name.lowercase(), CrossSellType.Addon))
},
navigateToContactInfo = navigateToContactInfo,
navigateToChipIdScreen = navigateToChipId,
setEpochDayWhenLastToolTipShown = { epochDay ->
Expand Down Expand Up @@ -279,6 +292,9 @@ private fun HomeScreen(
navigateToContactInfo: () -> Unit,
navigateToChipIdScreen: () -> Unit,
markCrossSellsNotificationAsSeen: () -> Unit,
onCrossSellsShown: (CrossSellSheetData) -> Unit,
onHomeCrossSellImpression: (CrossSell) -> Unit = {},
onHomeAddonImpression: (AddonBannerInfo) -> Unit = {},
setEpochDayWhenLastToolTipShown: (Long) -> Unit,
imageLoader: ImageLoader,
navigateToTravelCertificate: () -> Unit,
Expand All @@ -296,6 +312,7 @@ private fun HomeScreen(
CrossSellBottomSheet(
state = crossSellBottomSheetState,
markCrossSellsNotificationAsSeen = markCrossSellsNotificationAsSeen,
onCrossSellsShown = onCrossSellsShown,
onCrossSellClick = openCrossSellUrl,
imageLoader = imageLoader,
)
Expand Down Expand Up @@ -415,6 +432,8 @@ private fun HomeScreen(
imageLoader = imageLoader,
navigateToTravelCertificate = navigateToTravelCertificate,
navigateToAddonPurchaseFlow = navigateToAddonPurchaseFlow,
onHomeCrossSellImpression = onHomeCrossSellImpression,
onHomeAddonImpression = onHomeAddonImpression,
)
}
}
Expand Down Expand Up @@ -575,6 +594,8 @@ private fun HomeScreenSuccess(
imageLoader: ImageLoader,
navigateToTravelCertificate: () -> Unit,
navigateToAddonPurchaseFlow: (List<String>) -> Unit,
onHomeCrossSellImpression: (CrossSell) -> Unit = {},
onHomeAddonImpression: (AddonBannerInfo) -> Unit = {},
modifier: Modifier = Modifier,
) {
val consumedWindowInsets = remember { MutableWindowInsets() }
Expand Down Expand Up @@ -874,6 +895,7 @@ private fun HomeScreenSuccess(
OffersSection(
recommendedCrossSell = recommended,
onCrossSellClick = openCrossSellUrl,
onImpression = { onHomeCrossSellImpression(recommended.crossSell) },
imageLoader = imageLoader,
horizontalInsets = horizontalInsets,
)
Expand All @@ -882,12 +904,14 @@ private fun HomeScreenSuccess(
HomeSection.DiscoverInsurances -> DiscoverInsurancesSection(
crossSells = uiState.crossSellsPartition.discoverCrossSells,
onCrossSellClick = openCrossSellUrl,
onCrossSellImpression = onHomeCrossSellImpression,
imageLoader = imageLoader,
)

HomeSection.Addons -> AddonsSection(
addonBannerInfos = uiState.addonBannerInfos,
navigateToAddonPurchaseFlow = navigateToAddonPurchaseFlow,
onAddonImpression = onHomeAddonImpression,
horizontalInsets = horizontalInsets,
imageLoader = imageLoader,
)
Expand Down Expand Up @@ -1090,12 +1114,19 @@ private fun OffersSection(
onCrossSellClick: (String) -> Unit,
imageLoader: ImageLoader,
horizontalInsets: PaddingValues,
onImpression: () -> Unit = {},
) {
val crossSell = recommendedCrossSell.crossSell
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.onVisibilityChanged(
minDurationMs = CROSS_SELL_IMPRESSION_MIN_DURATION_MS,
minFractionVisible = CROSS_SELL_IMPRESSION_MIN_FRACTION_VISIBLE,
) { visible ->
if (visible) onImpression()
}
.padding(horizontal = 16.dp)
.padding(horizontalInsets),
) {
Expand Down Expand Up @@ -1283,6 +1314,7 @@ private fun AddonsSection(
navigateToAddonPurchaseFlow: (List<String>) -> Unit,
horizontalInsets: PaddingValues,
imageLoader: ImageLoader,
onAddonImpression: (AddonBannerInfo) -> Unit = {},
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
Expand All @@ -1305,7 +1337,14 @@ private fun AddonsSection(
buttonText = stringResource(Res.string.ADDON_FLOW_LEARN_MORE_BUTTON),
onButtonClick = { navigateToAddonPurchaseFlow(addon.eligibleInsurancesIds) },
imageLoader = imageLoader,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.onVisibilityChanged(
minDurationMs = CROSS_SELL_IMPRESSION_MIN_DURATION_MS,
minFractionVisible = CROSS_SELL_IMPRESSION_MIN_FRACTION_VISIBLE,
) { visible ->
if (visible) onAddonImpression(addon)
},
buttonSize = ButtonSize.Small,
buttonShape = HedvigTheme.shapes.cornerFull,
)
Expand All @@ -1318,6 +1357,7 @@ private fun DiscoverInsurancesSection(
crossSells: List<CrossSell>,
onCrossSellClick: (String) -> Unit,
imageLoader: ImageLoader,
onCrossSellImpression: ((CrossSell) -> Unit)? = null,
) {
CrossSellsSection(
title = stringResource(Res.string.CROSS_SELL_SUBTITLE),
Expand All @@ -1328,6 +1368,7 @@ private fun DiscoverInsurancesSection(
imageLoader = imageLoader,
buttonSize = ButtonSize.Small,
buttonShape = HedvigTheme.shapes.cornerFull,
onCrossSellImpression = onCrossSellImpression,
)
}

Expand Down Expand Up @@ -1371,13 +1412,15 @@ private fun WelcomeMessage(firstName: String, modifier: Modifier = Modifier) {
private fun CrossSellBottomSheet(
state: HedvigBottomSheetState<CrossSellSheetData>,
markCrossSellsNotificationAsSeen: () -> Unit,
onCrossSellsShown: (CrossSellSheetData) -> Unit,
onCrossSellClick: (String) -> Unit,
imageLoader: ImageLoader,
) {
LaunchedEffect(state) {
snapshotFlow { state.isVisible }.distinctUntilChanged().collect { isVisible ->
if (isVisible) {
markCrossSellsNotificationAsSeen()
state.data?.let(onCrossSellsShown)
}
}
}
Expand Down Expand Up @@ -1506,6 +1549,7 @@ private fun PreviewHomeScreen(
deleteDraftClaim = {},
navigateToFirstVet = {},
markCrossSellsNotificationAsSeen = {},
onCrossSellsShown = {},
navigateToContactInfo = {},
navigateToChipIdScreen = {},
setEpochDayWhenLastToolTipShown = {},
Expand Down Expand Up @@ -1542,6 +1586,7 @@ private fun PreviewHomeScreenWithError() {
deleteDraftClaim = {},
navigateToFirstVet = {},
markCrossSellsNotificationAsSeen = {},
onCrossSellsShown = {},
navigateToContactInfo = {},
navigateToChipIdScreen = {},
setEpochDayWhenLastToolTipShown = {},
Expand Down Expand Up @@ -1600,6 +1645,7 @@ private fun PreviewHomeScreenAllHomeTextTypes(
deleteDraftClaim = {},
navigateToFirstVet = {},
markCrossSellsNotificationAsSeen = {},
onCrossSellsShown = {},
navigateToContactInfo = {},
navigateToChipIdScreen = {},
setEpochDayWhenLastToolTipShown = {},
Expand Down
Loading
Loading