diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 4e1f56326d..e9731178c5 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -3,7 +3,7 @@ on: push: branches: - develop - - eng/metro-nav3-pr2-nav2-to-nav3 + - feat/xsell-addon-rec workflow_dispatch: concurrency: diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql index 23e721610e..3e54af32d5 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql @@ -33,4 +33,5 @@ fragment ClaimFragment on Claim { displayTitle displayValue } + contractId } diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt index 6dad79cb54..400c167c86 100644 --- a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt @@ -28,7 +28,7 @@ interface ChangeTierRepository { suspend fun addQuotesToStorage(quotes: List) - suspend fun submitChangeTierQuote(quoteId: String): Either + suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either suspend fun getCurrentQuoteId(): String } @@ -74,7 +74,7 @@ internal class ChangeTierRepositoryImpl( changeTierQuoteStorage.insertAll(quotes) } - override suspend fun submitChangeTierQuote(quoteId: String): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either { apolloClient .mutation(ChangeTierDeductibleCommitIntentMutation(quoteId)) @@ -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), + ) } } diff --git a/app/data/data-changetier/src/test/kotlin/data/ChangeTierRepositoryImplTest.kt b/app/data/data-changetier/src/test/kotlin/data/ChangeTierRepositoryImplTest.kt index ff3cb83a7b..ec75f7c8bd 100644 --- a/app/data/data-changetier/src/test/kotlin/data/ChangeTierRepositoryImplTest.kt +++ b/app/data/data-changetier/src/test/kotlin/data/ChangeTierRepositoryImplTest.kt @@ -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 { @@ -74,7 +75,7 @@ class ChangeTierRepositoryImplTest { crossSellAfterFlowRepository = CrossSellAfterFlowRepositoryImpl(), changeTierQuoteStorage = storage, ) - val result = repository.submitChangeTierQuote(testId) + val result = repository.submitChangeTierQuote(testId, testContractId) assertThat(result) .isLeft() } @@ -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 diff --git a/app/data/data-cross-sell-after-claim-closed/src/main/kotlin/com/hedvig/android/data/cross/sell/after/claim/closed/CrossSellAfterClaimClosedRepository.kt b/app/data/data-cross-sell-after-claim-closed/src/main/kotlin/com/hedvig/android/data/cross/sell/after/claim/closed/CrossSellAfterClaimClosedRepository.kt index 58ebfca6c5..3bff8c1823 100644 --- a/app/data/data-cross-sell-after-claim-closed/src/main/kotlin/com/hedvig/android/data/cross/sell/after/claim/closed/CrossSellAfterClaimClosedRepository.kt +++ b/app/data/data-cross-sell-after-claim-closed/src/main/kotlin/com/hedvig/android/data/cross/sell/after/claim/closed/CrossSellAfterClaimClosedRepository.kt @@ -50,6 +50,7 @@ internal class CrossSellAfterClaimClosedRepositoryImpl( type = claim.claimType, typeOfContract = claim.productVariant?.typeOfContract, ), + contractId = claim.contractId, ), ) } diff --git a/app/data/data-cross-sell-after-flow/src/main/kotlin/com/hedvig/android/data/cross/sell/after/flow/CrossSellAfterFlowRepository.kt b/app/data/data-cross-sell-after-flow/src/main/kotlin/com/hedvig/android/data/cross/sell/after/flow/CrossSellAfterFlowRepository.kt index ad7b38b66d..91eda3fdf5 100644 --- a/app/data/data-cross-sell-after-flow/src/main/kotlin/com/hedvig/android/data/cross/sell/after/flow/CrossSellAfterFlowRepository.kt +++ b/app/data/data-cross-sell-after-flow/src/main/kotlin/com/hedvig/android/data/cross/sell/after/flow/CrossSellAfterFlowRepository.kt @@ -20,6 +20,8 @@ interface CrossSellAfterFlowRepository { sealed class CrossSellInfoType() { abstract val source: String + + abstract val contractId: String? protected abstract val extraInfo: Map? val attributes: Map get() = buildMap { @@ -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 = with(info) { @@ -52,7 +55,9 @@ sealed class CrossSellInfoType() { ) } - data object ChangeTier : CrossSellInfoType() { + data class ChangeTier( + override val contractId: String?, + ) : CrossSellInfoType() { override val source: String = "changeTier" override val extraInfo: Map? = null } @@ -60,14 +65,18 @@ sealed class CrossSellInfoType() { data object Addon : CrossSellInfoType() { override val source: String = "addon" override val extraInfo: Map? = null + override val contractId: String? = null } data object EditCoInsured : CrossSellInfoType() { override val source: String = "editCoInsured" override val extraInfo: Map? = 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? = null } diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt index 862c101340..5a4a1a7560 100644 --- a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt @@ -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) diff --git a/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt b/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt index 2aff04184e..6fd8c28ff8 100644 --- a/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt +++ b/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt @@ -47,7 +47,7 @@ internal class FakeChangeTierRepository() : ChangeTierRepository { override suspend fun addQuotesToStorage(quotes: List) { } - override suspend fun submitChangeTierQuote(quoteId: String): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either {} } diff --git a/app/feature/feature-cross-sell-sheet/build.gradle.kts b/app/feature/feature-cross-sell-sheet/build.gradle.kts index 9e27cf44f7..6c52abd945 100644 --- a/app/feature/feature-cross-sell-sheet/build.gradle.kts +++ b/app/feature/feature-cross-sell-sheet/build.gradle.kts @@ -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) @@ -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) } diff --git a/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql b/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql index 2181c30b7b..57e42e2751 100644 --- a/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql +++ b/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql @@ -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 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 f1168ec236..42af3f8ebe 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 @@ -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 @@ -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 @@ -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 { @@ -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) + } + }, ) }, ) @@ -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) } } @@ -151,6 +161,7 @@ internal class GetCrossSellSheetDataUseCaseImpl( override suspend fun invoke(source: CrossSellInput): Flow> { return apolloClient .query(BottomSheetCrossSellsQuery(source)) + .fetchPolicy(FetchPolicy.NetworkOnly) .safeFlow(::ErrorMessage) .map { response -> either { @@ -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, ) } } diff --git a/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellInfoTypeToCrossSellInputTest.kt b/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellInfoTypeToCrossSellInputTest.kt new file mode 100644 index 0000000000..3ad3d9eb5a --- /dev/null +++ b/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellInfoTypeToCrossSellInputTest.kt @@ -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()) + } +} diff --git a/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetPresenterTest.kt b/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetPresenterTest.kt new file mode 100644 index 0000000000..1a39e42b4a --- /dev/null +++ b/app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetPresenterTest.kt @@ -0,0 +1,84 @@ +package com.hedvig.android.feature.cross.sell.sheet + +import arrow.core.Either +import arrow.core.right +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.crosssells.CrossSellSheetData +import com.hedvig.android.crosssells.RecommendedAddon +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 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import octopus.type.CrossSellInput +import org.junit.Test + +internal class CrossSellSheetPresenterTest { + private val recommendedAddon = RecommendedAddon( + id = "addonId", + title = "Travel Insurance Plus", + buttonTitle = "See offer", + description = "For a safer trip abroad", + deepLink = "https://hedvig.com/addon", + pillowImageSmall = "smallSrc", + pillowImageLarge = "largeSrc", + ) + + @Test + fun `a response with nothing to show does not show the sheet`() = runTest { + val emptyData = CrossSellSheetData(null, emptyList(), null) + val presenter = CrossSellSheetPresenter( + FakeGetCrossSellSheetDataUseCase(emptyData), + FakeCrossSellAfterFlowRepository(CrossSellInfoType.EditCoInsured), + ) + + presenter.test(CrossSellSheetState.Loading) { + assertThat(awaitItem()).isEqualTo(CrossSellSheetState.Loading) + assertThat(awaitItem()).isEqualTo(CrossSellSheetState.DontShow) + } + } + + @Test + fun `a response with a recommendation shows the sheet`() = runTest { + val addonData = CrossSellSheetData(null, emptyList(), recommendedAddon) + val presenter = CrossSellSheetPresenter( + FakeGetCrossSellSheetDataUseCase(addonData), + FakeCrossSellAfterFlowRepository(CrossSellInfoType.EditCoInsured), + ) + + presenter.test(CrossSellSheetState.Loading) { + assertThat(awaitItem()).isEqualTo(CrossSellSheetState.Loading) + assertThat(awaitItem()).isEqualTo( + CrossSellSheetState.Content(addonData, CrossSellInfoType.EditCoInsured), + ) + } + } +} + +private class FakeGetCrossSellSheetDataUseCase( + private val data: CrossSellSheetData, +) : GetCrossSellSheetDataUseCase { + override suspend fun invoke(source: CrossSellInput): Flow> { + return flowOf(data.right()) + } +} + +private class FakeCrossSellAfterFlowRepository( + initialInfoType: CrossSellInfoType?, +) : CrossSellAfterFlowRepository { + private val infoType = MutableStateFlow(initialInfoType) + + override fun shouldShowCrossSellSheetWithInfo(): Flow = infoType + + override fun completedCrossSellTriggeringSelfServiceSuccessfully(type: CrossSellInfoType) { + infoType.value = type + } + + override fun showedCrossSellSheet(type: CrossSellInfoType?) { + infoType.value = null + } +} diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index c333001488..be16d2b2a1 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -58,6 +58,19 @@ query Home($claimsHistoryFlag: Boolean!) { otherCrossSells { ...HomeCrossSellFragment } + recommendedAddon { + id + title + description + buttonTitle + deepLink + pillowImageLarge { + src + } + pillowImageSmall { + src + } + } } memberActions { firstVetAction { diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index c4624d0ea3..8754f58e2f 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -14,6 +14,7 @@ import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.apollo.safeFlow 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.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.AddonBannerSource @@ -136,9 +137,21 @@ internal class GetHomeDataUseCaseImpl( val otherCrossSellsData = crossSellsData.otherCrossSells.map { it.toCrossSell() } + val recommendedAddon = crossSellsData.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, + ) + } val crossSells = CrossSellSheetData( recommendedCrossSell = recommendedCrossSell, otherCrossSells = otherCrossSellsData, + recommendedAddon = recommendedAddon, ) val showChatIcon = shouldShowChatButton( isInboxEnabledFromKillSwitch = inboxAlwaysAvailable, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index 4ee160b208..9b9c06da56 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -53,6 +53,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ImageAsset("", "", ""), ), ), + recommendedAddon = null, ), travelBannerInfo = null, showChatIcon = false, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index d0c15818c5..f573c89a06 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -771,6 +771,7 @@ private fun PreviewHomeScreen( ImageAsset("", "", ""), ), ), + recommendedAddon = null, ), crossSellRecommendationNotification = CrossSellRecommendationNotification( true, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index fafa5cb3bc..3a9bc6135a 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -218,6 +218,7 @@ private data class SuccessData( crossSellRecommendationNotification: CrossSellRecommendationNotification, ): SuccessData { val crossSellsAction = if (homeData.crossSells.recommendedCrossSell != null || + homeData.crossSells.recommendedAddon != null || homeData.crossSells.otherCrossSells.isNotEmpty() ) { HomeTopBarAction.CrossSellsAction(homeData.crossSells, crossSellRecommendationNotification) diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt index efd28833a0..ddd9231d9c 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt @@ -25,6 +25,8 @@ import com.hedvig.android.apollo.test.TestApolloClientRule import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.test.isRight +import com.hedvig.android.crosssells.CrossSellSheetData +import com.hedvig.android.crosssells.RecommendedAddon import com.hedvig.android.data.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.AddonBannerSource import com.hedvig.android.data.addons.data.GetAddonBannerInfoUseCase @@ -56,10 +58,13 @@ import octopus.type.buildChatMessageText import octopus.type.buildClaim import octopus.type.buildContract import octopus.type.buildConversation +import octopus.type.buildCrossSellV2 import octopus.type.buildLinkInfo import octopus.type.buildMember import octopus.type.buildMemberImportantMessage import octopus.type.buildPendingContract +import octopus.type.buildRecommendedAddonCrossSell +import octopus.type.buildStoryblokImageAsset import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -669,6 +674,57 @@ internal class GetHomeUseCaseTest { } } + @Test + fun `when a recommended addon is present, it is mapped into the cross sells data`() = runTest { + val getHomeDataUseCase = testUseCaseWithoutReminders() + + apolloClient.registerTestResponse( + HomeQuery(true), + HomeQuery.Data(OctopusFakeResolver) { + currentMember = buildMember { + crossSellV2 = buildCrossSellV2 { + recommendedAddon = buildRecommendedAddonCrossSell { + id = "addonId" + title = "Travel Insurance Plus" + description = "For a safer trip abroad" + buttonTitle = "See offer" + deepLink = "https://hedvig.com/addon" + pillowImageSmall = buildStoryblokImageAsset { src = "smallSrc" } + pillowImageLarge = buildStoryblokImageAsset { src = "largeSrc" } + } + } + } + }, + ) + apolloClient.registerTestResponse( + UnreadMessageCountQuery(), + UnreadMessageCountQuery.Data(OctopusFakeResolver), + ) + apolloClient.registerTestResponse( + CbmNumberOfChatMessagesQuery(), + CbmNumberOfChatMessagesQuery.Data(OctopusFakeResolver), + ) + + val result = getHomeDataUseCase.invoke(true).first() + + assertThat(result) + .isNotNull() + .isRight() + .prop(HomeData::crossSells) + .prop(CrossSellSheetData::recommendedAddon) + .isEqualTo( + RecommendedAddon( + id = "addonId", + title = "Travel Insurance Plus", + buttonTitle = "See offer", + description = "For a safer trip abroad", + deepLink = "https://hedvig.com/addon", + pillowImageSmall = "smallSrc", + pillowImageLarge = "largeSrc", + ), + ) + } + // Used as a convenience to get a use case without any enqueued apollo responses, but some sane defaults for the // other dependencies private fun testUseCaseWithoutReminders( diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 4f7fde4823..c997ec6627 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -16,6 +16,7 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.core.common.ApplicationScope 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 @@ -54,6 +55,16 @@ internal class HomePresenterTest { bundleProgress = null, ) + val testAddon = RecommendedAddon( + id = "addonId", + title = "Travel Insurance Plus", + buttonTitle = "See offer", + description = "For a safer trip abroad", + deepLink = "https://hedvig.com/addon", + pillowImageSmall = "smallSrc", + pillowImageLarge = "largeSrc", + ) + @Test fun `asking to refresh successfully asks for a fetch from the network`() = runTest { val getHomeDataUseCase = TestGetHomeDataUseCase() @@ -140,7 +151,7 @@ internal class HomePresenterTest { showChatIcon = true, hasUnseenChatMessages = false, showHelpCenter = false, - crossSells = CrossSellSheetData(testCrossSell, listOf()), + crossSells = CrossSellSheetData(testCrossSell, listOf(), null), firstVetSections = listOf(), travelBannerInfo = null, ).right(), @@ -166,7 +177,7 @@ internal class HomePresenterTest { isHelpCenterEnabled = false, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( - CrossSellSheetData(testCrossSell, listOf()), + CrossSellSheetData(testCrossSell, listOf(), null), crossSellRecommendationNotification = CrossSellRecommendationNotification (true, 1L), ), @@ -179,6 +190,47 @@ internal class HomePresenterTest { } } + @Test + fun `a recommended addon without any cross sells still shows the cross sells top bar action`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + ) + val addonOnlyCrossSells = CrossSellSheetData(null, listOf(), testAddon) + + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) + + getHomeDataUseCase.responseTurbine.add( + HomeData( + contractStatus = HomeData.ContractStatus.Active, + claimStatusCardsData = null, + veryImportantMessages = listOf(), + memberReminders = MemberReminders(), + showChatIcon = false, + hasUnseenChatMessages = false, + crossSells = addonOnlyCrossSells, + firstVetSections = listOf(), + showHelpCenter = false, + travelBannerInfo = null, + ).right(), + ) + assertThat(awaitItem()) + .isInstanceOf() + .prop(HomeUiState.Success::crossSellsAction) + .isEqualTo( + HomeTopBarAction.CrossSellsAction( + addonOnlyCrossSells, + CrossSellRecommendationNotification(true, 1L), + ), + ) + } + } + @Test fun `the notification member reminder must not show for the home presenter`() = runTest { val getHomeDataUseCase = TestGetHomeDataUseCase() @@ -203,7 +255,7 @@ internal class HomePresenterTest { ), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -280,7 +332,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = hasNotification, showHelpCenter = false, firstVetSections = listOf(), - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(), null), travelBannerInfo = null, ).right(), ) @@ -313,7 +365,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -365,7 +417,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf( firstVet, ), @@ -420,7 +472,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell)), + crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -438,7 +490,7 @@ internal class HomePresenterTest { chatAction = null, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( - CrossSellSheetData(testCrossSell, listOf(crossSell)), + CrossSellSheetData(testCrossSell, listOf(crossSell), null), crossSellRecommendationNotification = CrossSellRecommendationNotification (true, 1L), ), @@ -470,7 +522,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = true, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, emptyList()), + crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -516,7 +568,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, emptyList()), + crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -560,7 +612,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, showHelpCenter = false, firstVetSections = listOf(), - crossSells = CrossSellSheetData(null, emptyList()), + crossSells = CrossSellSheetData(null, emptyList(), null), travelBannerInfo = null, ) } diff --git a/app/feature/feature-movingflow/src/main/graphql/MutationMoveIntentCommit.graphql b/app/feature/feature-movingflow/src/main/graphql/MutationMoveIntentCommit.graphql index 04a152ffdf..57df297bd7 100644 --- a/app/feature/feature-movingflow/src/main/graphql/MutationMoveIntentCommit.graphql +++ b/app/feature/feature-movingflow/src/main/graphql/MutationMoveIntentCommit.graphql @@ -6,5 +6,6 @@ mutation MoveIntentV2Commit($intentId: ID!, $homeQuoteId: ID!, $excludedAddons: userError { message } + newContractId } } diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryViewModel.kt index 71a6eeff48..d86a46a1db 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryViewModel.kt @@ -177,7 +177,9 @@ internal class SummaryPresenter( } } else { crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( - CrossSellInfoType.MovingFlow, + CrossSellInfoType.MovingFlow( + moveIntentCommit.newContractId, + ), ) submitChangesWithData = null backstack.popUpTo(inclusive = true) diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt index 27a48fdede..d22667c1d6 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt @@ -279,8 +279,10 @@ private fun SelectedSurveyInfoBox( ProvideTextStyle( HedvigTheme.typography.label, ) { - HedvigMarkdownText(content = suggestion.description, - style = HedvigTheme.typography.label) + HedvigMarkdownText( + content = suggestion.description, + style = HedvigTheme.typography.label, + ) } }, priority = when (suggestion.type) { diff --git a/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt b/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt index 1659b8e997..850146c037 100644 --- a/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt +++ b/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt @@ -493,7 +493,7 @@ private class FakeChangeTierRepository() : ChangeTierRepository { override suspend fun addQuotesToStorage(quotes: List) { } - override suspend fun submitChangeTierQuote(quoteId: String): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either {} } diff --git a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt index e556d9ccfa..eadb8a14d9 100644 --- a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt +++ b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt @@ -1,11 +1,7 @@ package com.hedvig.android.crosssells -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.fadeIn import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,20 +23,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -50,7 +37,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import coil3.ImageLoader @@ -75,8 +61,10 @@ import com.hedvig.android.design.system.hedvig.LocalTextStyle import com.hedvig.android.design.system.hedvig.StepProgressItem import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState +import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.Campaign import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.Plus import com.hedvig.android.design.system.hedvig.placeholder.crossSellPainterFallback import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.placeholder.shimmer @@ -96,13 +84,26 @@ import hedvig.resources.TALKBACK_OPEN_EXTERNAL_LINK import hedvig.resources.cross_sell_get_price import hedvig.resources.general_close_button import hedvig.resources.insurance_tab_cross_sells_title -import kotlinx.coroutines.delay import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource data class CrossSellSheetData( val recommendedCrossSell: RecommendedCrossSell?, val otherCrossSells: List, + val recommendedAddon: RecommendedAddon?, +) { + val isEmpty: Boolean + get() = recommendedCrossSell == null && recommendedAddon == null && otherCrossSells.isEmpty() +} + +data class RecommendedAddon( + val id: String, + val title: String, + val buttonTitle: String, + val description: String, + val deepLink: String, + val pillowImageSmall: String, + val pillowImageLarge: String, ) data class RecommendedCrossSell( @@ -134,7 +135,8 @@ fun CrossSellFloatingBottomSheet( hedvigBottomSheetState = state, dragHandle = { CrossSellDragHandle( - text = state.data?.recommendedCrossSell?.bannerText, + text = state.data?.recommendedCrossSell?.bannerText + ?: state.data?.recommendedAddon?.let { stringResource(Res.string.CROSS_SELL_BANNER_TEXT) }, modifier = Modifier .padding(horizontal = 16.dp) .clip(HedvigTheme.shapes.cornerXLargeTop), @@ -154,6 +156,7 @@ fun CrossSellFloatingBottomSheet( onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader = imageLoader, + recommendedAddon = crossSellSheetData.recommendedAddon, ) }, ) @@ -166,11 +169,12 @@ fun CrossSellBottomSheet( imageLoader: ImageLoader, ) { val dragHandle: @Composable (() -> Unit)? = - if (state.data?.recommendedCrossSell != null) { + if (state.data?.recommendedCrossSell != null || state.data?.recommendedAddon != null) { { CrossSellDragHandle( contentPadding = PaddingValues(horizontal = 16.dp), - text = state.data?.recommendedCrossSell?.bannerText, + text = state.data?.recommendedCrossSell?.bannerText + ?: stringResource(Res.string.CROSS_SELL_BANNER_TEXT), ) } } else { @@ -183,6 +187,7 @@ fun CrossSellBottomSheet( CrossSellsSheetContent( recommendedCrossSell = crossSellSheetData.recommendedCrossSell, otherCrossSells = crossSellSheetData.otherCrossSells, + recommendedAddon = crossSellSheetData.recommendedAddon, onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader, @@ -196,6 +201,7 @@ fun CrossSellBottomSheet( private fun CrossSellsSheetContent( recommendedCrossSell: RecommendedCrossSell?, otherCrossSells: List, + recommendedAddon: RecommendedAddon?, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, imageLoader: ImageLoader, @@ -205,7 +211,17 @@ private fun CrossSellsSheetContent( verticalArrangement = Arrangement.spacedBy(40.dp), modifier = Modifier.padding(bottom = 24.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + Column { + Spacer(Modifier.height(48.dp)) + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -246,6 +262,7 @@ private fun CrossSellsSheetContent( @Composable private fun CrossSellsFloatingSheetContent( recommendedCrossSell: RecommendedCrossSell?, + recommendedAddon: RecommendedAddon?, otherCrossSells: List, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, @@ -265,7 +282,17 @@ private fun CrossSellsFloatingSheetContent( .padding(bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(40.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + Column { + Spacer(Modifier.height(48.dp)) + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -286,7 +313,7 @@ private fun CrossSellsFloatingSheetContent( onCrossSellClick = onCrossSellClick, withSubHeader = false, onSheetDismissed = dismissSheet, - imageLoader = rememberPreviewImageLoader(), + imageLoader = imageLoader, ) } } @@ -309,6 +336,89 @@ private fun CrossSellsFloatingSheetContent( } } +@Composable +private fun AddonRecommendationSection( + recommendedAddon: RecommendedAddon, + onButtonClick: (String) -> Unit, + dismissSheet: () -> Unit, + imageLoader: ImageLoader, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + val placeholder = crossSellPainterFallback(shape = HedvigTheme.shapes.cornerXXLarge) + Box { + AsyncImage( + model = recommendedAddon.pillowImageLarge, + contentDescription = EmptyContentDescription, + placeholder = placeholder, + error = placeholder, + fallback = placeholder, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(horizontal = 8.dp) + .size(140.dp), + ) + Row( + Modifier + .align(Alignment.TopEnd) + .padding(top = 8.dp), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .hedvigDropShadow(CircleShape) + .size(30.dp) + .background(HedvigTheme.colorScheme.fillNegative, CircleShape) + .border(1.dp, HedvigTheme.colorScheme.borderPrimary, CircleShape), + ) { + Icon( + imageVector = HedvigIcons.Plus, + contentDescription = EmptyContentDescription, + tint = HedvigTheme.colorScheme.fillPrimary, + modifier = Modifier.size(24.dp), + ) + } + Spacer(Modifier.width(12.dp)) + } + } + Spacer(Modifier.height(24.dp)) + val headingDescription = stringResource(Res.string.CROSS_SELL_TITLE) + + ": ${recommendedAddon.title}" + HedvigText( + text = recommendedAddon.title, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = headingDescription + heading() + }, + ) + HedvigText( + recommendedAddon.description, + style = LocalTextStyle.current.copy( + lineBreak = LineBreak.Heading, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ), + modifier = Modifier.padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(48.dp)) + HedvigButton( + text = recommendedAddon.buttonTitle, + onClick = { + onButtonClick(recommendedAddon.deepLink) + dismissSheet() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth(), + ) + } +} + @Composable private fun RecommendationSection( recommendedCrossSell: RecommendedCrossSell, @@ -815,6 +925,7 @@ private fun PreviewCrossSellsSheetContent( onCrossSellClick = {}, dismissSheet = {}, imageLoader = rememberPreviewImageLoader(), + recommendedAddon = null, ) } } @@ -829,7 +940,8 @@ private fun PreviewCrossSellsFloatingSheetContent( HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { CrossSellsFloatingSheetContent( - RecommendedCrossSell( + recommendedAddon = null, + recommendedCrossSell = RecommendedCrossSell( crossSell = CrossSell( "rh", "Car Insurance", @@ -844,7 +956,7 @@ private fun PreviewCrossSellsFloatingSheetContent( backgroundPillowImages = ("ds" to "ds"), bundleProgress = BundleProgress(1, 15), ).takeIf { case != TripleCase.THIRD }, - listOf( + otherCrossSells = listOf( CrossSell( "id", "title", @@ -853,8 +965,8 @@ private fun PreviewCrossSellsFloatingSheetContent( ImageAsset("", "", ""), ), ).takeIf { case != TripleCase.FIRST }.orEmpty(), - {}, - {}, + dismissSheet = {}, + onCrossSellClick = {}, imageLoader = rememberPreviewImageLoader(), ) } @@ -929,3 +1041,30 @@ private fun PreviewCrossSellDragHandle() { } } } + +@HedvigPreview +@Composable +private fun PreviewRecommendedAddon( + @PreviewParameter(TripleBooleanCollectionPreviewParameterProvider::class) case: TripleCase, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + CrossSellsSheetContent( + imageLoader = rememberPreviewImageLoader(), + recommendedCrossSell = null, + otherCrossSells = emptyList(), + recommendedAddon = RecommendedAddon( + id = "ifsf", + title = "Addon title", + buttonTitle = "Check the addon", + description = "Best addon in the world", + deepLink = "deep", + pillowImageSmall = "src", + pillowImageLarge = "src", + ), + onCrossSellClick = {}, + dismissSheet = {}, + ) + } + } +}