Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3b9627e
add data
panasetskaya Jun 24, 2026
1e87075
cross sell bottom sheet changes
panasetskaya Jun 24, 2026
349e956
add contractId to the input
panasetskaya Jun 24, 2026
89d779a
add contractId to change tier and moving
panasetskaya Jun 24, 2026
fe8aa45
add contractId to change tier and moving
panasetskaya Jun 24, 2026
ae91f1c
todo for new contract Id in the moving flow
panasetskaya Jun 24, 2026
65439d8
ui adjustments
panasetskaya Jun 24, 2026
e9a0c38
remove todo
panasetskaya Jun 24, 2026
92a72e3
non-null recommendedAddon in Home
panasetskaya Jun 24, 2026
7280c65
fix tests
panasetskaya Jun 24, 2026
e4d1ed3
pass new newContractId in the moving flow
panasetskaya Jun 25, 2026
27bee4f
paddings
panasetskaya Jun 25, 2026
0ed86ca
fix test
panasetskaya Jun 25, 2026
264b1b5
Merge branch 'develop' into feat/xsell-addon-rec
panasetskaya Jun 25, 2026
1f8aaf4
add contractId for closed claim
panasetskaya Jun 26, 2026
91c931a
publish to staging
panasetskaya Jun 26, 2026
12747ad
FetchPolicy.NetworkOnly
panasetskaya Jun 26, 2026
37b1539
Merge branch 'develop' into feat/xsell-addon-rec
StylianosGakis Jul 2, 2026
cdd45a4
Run ktlint
StylianosGakis Jul 3, 2026
6f7d9f9
Show the default banner text over addon-only cross-sell sheets
StylianosGakis Jul 3, 2026
259d70c
Use the real imageLoader in the floating sheet cross-sells section
StylianosGakis Jul 3, 2026
6c2e886
Include recommendedAddon in the Home cross-sells action gate, test ad…
StylianosGakis Jul 3, 2026
a8f98c2
Rename submitChangeTierQuote param to contractId to match its usage
StylianosGakis Jul 3, 2026
4c5d1bc
Leave contractId absent in CrossSellInput for flows without one
StylianosGakis Jul 3, 2026
b9b963d
Don't show the cross-sell sheet when the response has nothing to show
StylianosGakis Jul 3, 2026
7b1d427
Merge branch 'develop' into feat/xsell-addon-rec
StylianosGakis Jul 3, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- develop
- eng/metro-nav3-pr2-nav2-to-nav3
- feat/xsell-addon-rec
workflow_dispatch:

concurrency:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ fragment ClaimFragment on Claim {
displayTitle
displayValue
}
contractId
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface ChangeTierRepository {

suspend fun addQuotesToStorage(quotes: List<TierDeductibleQuote>)

suspend fun submitChangeTierQuote(quoteId: String): Either<ErrorMessage, Unit>
suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either<ErrorMessage, Unit>

suspend fun getCurrentQuoteId(): String
}
Expand Down Expand Up @@ -74,7 +74,7 @@ internal class ChangeTierRepositoryImpl(
changeTierQuoteStorage.insertAll(quotes)
}

override suspend fun submitChangeTierQuote(quoteId: String): Either<ErrorMessage, Unit> {
override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either<ErrorMessage, Unit> {
return either {
apolloClient
.mutation(ChangeTierDeductibleCommitIntentMutation(quoteId))
Expand All @@ -84,7 +84,9 @@ internal class ChangeTierRepositoryImpl(
logcat(ERROR) { "Tried to submit change tier quoteId: $quoteId but got error: $left" }
}
.bind()
crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully(CrossSellInfoType.ChangeTier)
crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully(
CrossSellInfoType.ChangeTier(contractId),
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ChangeTierRepositoryImplTest {
val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP)

private val testId = "testId"
private val testContractId = "testContractId"

private val apolloClientWithBadResponseToSubmit: ApolloClient
get() = testApolloClientRule.apolloClient.apply {
Expand Down Expand Up @@ -74,7 +75,7 @@ class ChangeTierRepositoryImplTest {
crossSellAfterFlowRepository = CrossSellAfterFlowRepositoryImpl(),
changeTierQuoteStorage = storage,
)
val result = repository.submitChangeTierQuote(testId)
val result = repository.submitChangeTierQuote(testId, testContractId)
assertThat(result)
.isLeft()
}
Expand All @@ -98,10 +99,10 @@ class ChangeTierRepositoryImplTest {
}
},
)
val result = repository.submitChangeTierQuote(testId)
val result = repository.submitChangeTierQuote(testId, testContractId)
assertThat(result).isRight().isEqualTo(Unit)
assertThat(crossSellAfterFlowRepository.shouldShowCrossSellSheetWithInfo().first())
.isEqualTo(CrossSellInfoType.ChangeTier)
.isEqualTo(CrossSellInfoType.ChangeTier(testContractId))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal class CrossSellAfterClaimClosedRepositoryImpl(
type = claim.claimType,
typeOfContract = claim.productVariant?.typeOfContract,
),
contractId = claim.contractId,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface CrossSellAfterFlowRepository {

sealed class CrossSellInfoType() {
abstract val source: String

abstract val contractId: String?
protected abstract val extraInfo: Map<String, Any?>?
val attributes: Map<String, Any?>
get() = buildMap {
Expand All @@ -31,6 +33,7 @@ sealed class CrossSellInfoType() {

data class ClosedClaim(
val info: ClaimInfo,
override val contractId: String?,
) : CrossSellInfoType() {
override val source: String = "closedClaim"
override val extraInfo: Map<String, Any?> = with(info) {
Expand All @@ -52,22 +55,28 @@ sealed class CrossSellInfoType() {
)
}

data object ChangeTier : CrossSellInfoType() {
data class ChangeTier(
override val contractId: String?,
) : CrossSellInfoType() {
override val source: String = "changeTier"
override val extraInfo: Map<String, Any?>? = null
}

data object Addon : CrossSellInfoType() {
override val source: String = "addon"
override val extraInfo: Map<String, Any?>? = null
override val contractId: String? = null
}

data object EditCoInsured : CrossSellInfoType() {
override val source: String = "editCoInsured"
override val extraInfo: Map<String, Any?>? = null
override val contractId: String? = null
}

data object MovingFlow : CrossSellInfoType() {
data class MovingFlow(
override val contractId: String?,
) : CrossSellInfoType() {
override val source: String = "movingFlow"
override val extraInfo: Map<String, Any?>? = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private class SummaryPresenter(
if (submitIteration > 0) {
val previousState = currentState
currentState = MakingChanges
tierRepository.submitChangeTierQuote(params.quoteIdToSubmit).fold(
tierRepository.submitChangeTierQuote(params.quoteIdToSubmit, params.insuranceId).fold(
ifLeft = {
currentState = previousState
backstack.add(SubmitFailureKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ internal class FakeChangeTierRepository() : ChangeTierRepository {
override suspend fun addQuotesToStorage(quotes: List<TierDeductibleQuote>) {
}

override suspend fun submitChangeTierQuote(quoteId: String): Either<ErrorMessage, Unit> {
override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either<ErrorMessage, Unit> {
return either {}
}

Expand Down
11 changes: 11 additions & 0 deletions app/feature/feature-cross-sell-sheet/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ hedvig {
viewModels()
}

android {
testOptions.unitTests.isReturnDefaultValues = true
}

dependencies {
implementation(libs.apollo.runtime)
implementation(libs.apollo.normalizedCache)
implementation(libs.arrow.core)
implementation(libs.arrow.fx)
implementation(projects.apolloCore)
Expand All @@ -23,4 +28,10 @@ dependencies {
implementation(projects.dataCrossSellAfterFlow)
implementation(projects.designSystemHedvig)
implementation(projects.moleculePublic)

testImplementation(libs.assertK)
testImplementation(libs.coroutines.test)
testImplementation(libs.junit)
testImplementation(libs.turbine)
testImplementation(projects.moleculeTest)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
query BottomSheetCrossSells($input: CrossSellInput!) {
currentMember {
crossSellV2(input: $input) {
recommendedAddon {
id
title
description
buttonTitle
deepLink
pillowImageLarge {
src
}
pillowImageSmall {
src
}
}
recommendedCrossSell {
crossSell {
...CrossSellFragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import arrow.core.left
import arrow.core.raise.either
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Optional
import com.apollographql.apollo.cache.normalized.FetchPolicy
import com.apollographql.apollo.cache.normalized.fetchPolicy
import com.hedvig.android.apollo.ErrorMessage
import com.hedvig.android.apollo.safeFlow
import com.hedvig.android.core.common.ErrorMessage
Expand All @@ -21,6 +23,7 @@ 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.CrossSellSheetData
import com.hedvig.android.crosssells.RecommendedAddon
import com.hedvig.android.crosssells.RecommendedCrossSell
import com.hedvig.android.data.contract.CrossSell
import com.hedvig.android.data.contract.ImageAsset
Expand Down Expand Up @@ -71,7 +74,7 @@ internal sealed interface CrossSellSheetState {
data class Content(val crossSellSheetData: CrossSellSheetData, val infoType: CrossSellInfoType) : CrossSellSheetState
}

private class CrossSellSheetPresenter(
internal class CrossSellSheetPresenter(
private val getCrossSellSheetDataUseCase: GetCrossSellSheetDataUseCase,
private val crossSellAfterFlowRepository: CrossSellAfterFlowRepository,
) : MoleculePresenter<CrossSellSheetEvent, CrossSellSheetState> {
Expand Down Expand Up @@ -100,7 +103,13 @@ private class CrossSellSheetPresenter(
.mapLatest { result ->
result.fold(
ifLeft = { error -> CrossSellSheetState.Error(error) },
ifRight = { data -> CrossSellSheetState.Content(data, infoType) },
ifRight = { data ->
if (data.isEmpty) {
CrossSellSheetState.DontShow
} else {
CrossSellSheetState.Content(data, infoType)
}
},
)
},
)
Expand All @@ -118,14 +127,15 @@ internal fun CrossSellInfoType.toCrossSellSource(): CrossSellInput {
userFlow = UserFlow.SMART_X_SELL,
flowSource = Optional.present(flowSource),
experiments = emptyList(),
contractId = Optional.presentIfNotNull(this.contractId),
)
}
return when (this) {
CrossSellInfoType.Addon -> smartCrossSellInput(FlowSource.ADDON)
CrossSellInfoType.ChangeTier -> smartCrossSellInput(FlowSource.CHANGE_TIER)
is CrossSellInfoType.ChangeTier -> smartCrossSellInput(FlowSource.CHANGE_TIER)
is CrossSellInfoType.ClosedClaim -> smartCrossSellInput(FlowSource.CLOSED_CLAIM)
CrossSellInfoType.EditCoInsured -> smartCrossSellInput(FlowSource.EDIT_COINSURED)
CrossSellInfoType.MovingFlow -> smartCrossSellInput(FlowSource.MOVING)
is CrossSellInfoType.MovingFlow -> smartCrossSellInput(FlowSource.MOVING)
}
}

Expand All @@ -151,6 +161,7 @@ internal class GetCrossSellSheetDataUseCaseImpl(
override suspend fun invoke(source: CrossSellInput): Flow<Either<ErrorMessage, CrossSellSheetData>> {
return apolloClient
.query(BottomSheetCrossSellsQuery(source))
.fetchPolicy(FetchPolicy.NetworkOnly)
.safeFlow(::ErrorMessage)
.map { response ->
either {
Expand Down Expand Up @@ -178,9 +189,21 @@ internal class GetCrossSellSheetDataUseCaseImpl(
val otherCrossSellsData = allData.otherCrossSells.map {
it.toCrossSell()
}
val recommendedAddon = allData.recommendedAddon?.let {
RecommendedAddon(
id = it.id,
title = it.title,
buttonTitle = it.buttonTitle,
description = it.description,
deepLink = it.deepLink,
pillowImageSmall = it.pillowImageSmall.src,
pillowImageLarge = it.pillowImageLarge.src,
)
}
CrossSellSheetData(
recommendedCrossSell = recommendedData,
otherCrossSells = otherCrossSellsData,
recommendedAddon = recommendedAddon,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.hedvig.android.feature.cross.sell.sheet

import assertk.assertThat
import assertk.assertions.isEqualTo
import com.apollographql.apollo.api.Optional
import com.hedvig.android.data.cross.sell.after.flow.CrossSellInfoType
import octopus.type.FlowSource
import octopus.type.UserFlow
import org.junit.Test

class CrossSellInfoTypeToCrossSellInputTest {
@Test
fun `a flow that changed a contract passes that contract id to the cross sell input`() {
val input = CrossSellInfoType.ChangeTier("contractId").toCrossSellSource()

assertThat(input.userFlow).isEqualTo(UserFlow.SMART_X_SELL)
assertThat(input.flowSource).isEqualTo(Optional.present(FlowSource.CHANGE_TIER))
assertThat(input.contractId).isEqualTo(Optional.present("contractId"))
}

@Test
fun `a flow without an associated contract leaves the contract id absent in the cross sell input`() {
val input = CrossSellInfoType.Addon.toCrossSellSource()

assertThat(input.flowSource).isEqualTo(Optional.present(FlowSource.ADDON))
assertThat(input.contractId).isEqualTo(Optional.absent())
}

@Test
fun `a moving flow which did not report a new contract id leaves the contract id absent`() {
val input = CrossSellInfoType.MovingFlow(null).toCrossSellSource()

assertThat(input.flowSource).isEqualTo(Optional.present(FlowSource.MOVING))
assertThat(input.contractId).isEqualTo(Optional.absent())
}
}
Loading
Loading