From 3b9627e81f42bd200c31b98e195f1a9af94d825b Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 11:08:14 +0200 Subject: [PATCH 01/23] add data --- .../android/apollo/octopus/schema.graphqls | 101 +++++++++++++++++- .../QueryBottomSheetCrossSells.graphql | 13 +++ .../sell/sheet/CrossSellSheetViewModel.kt | 13 +++ .../home/home/data/GetHomeDataUseCase.kt | 1 + .../home/home/data/GetHomeDataUseCaseDemo.kt | 1 + .../feature/home/home/ui/HomeDestination.kt | 1 + .../feature/home/home/ui/HomePresenterTest.kt | 22 ++-- .../hedvig/android/crosssells/CrossSells.kt | 11 ++ 8 files changed, 151 insertions(+), 12 deletions(-) diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index 3e30d7a249..921e297030 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -442,6 +442,22 @@ type AgreementDisplayItem { displaySubtitle: String displayValue: String! } +input AnswersInput { + relationship: String + isPregnant: Boolean + hasKids: Boolean + hasCat: Boolean + hasDog: Boolean + yourAge: String + partnerAge: String + isStudent: Boolean + housingType: String + houseAge: String + isRental: Boolean + hasCar: Boolean + carAge: String + wantsAllRisk: Boolean +} enum AppPlatform { IOS ANDROID @@ -1003,7 +1019,7 @@ type ClaimIntentStep { """ A union of all the different kinds of "step content". """ -union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentSelect|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentFileUpload|ClaimIntentStepContentSummary|ClaimIntentStepContentDeflection|ClaimIntentStepContentDeflectionMessage +union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentSelect|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentFileUpload|ClaimIntentStepContentSummary|ClaimIntentStepContentDeflection|ClaimIntentStepContentDeflectionMessage|ClaimIntentStepContentInformation """ An audio recording step is one where the user is meant to record some audio. Submitted using `Mutation.claimIntentSubmitAudio`. @@ -1181,6 +1197,22 @@ This typically will be backed by a String - but other formats could appear. """ scalar ClaimIntentStepContentFormFieldValue """ +A read and acknowledge notice if the member needs to be shown some urgent information (if water is still leaking -> turn off the pipe). Not terminal +Submitted using `Mutation.claimIntentSubmitInformation`. +""" +type ClaimIntentStepContentInformation { + notice: String! + """ + So we can show a white/red border if info/critical respectively. + """ + severity: ClaimIntentStepContentInformationSeverity! + buttonTitle: String! +} +enum ClaimIntentStepContentInformationSeverity { + INFO + CRITICAL +} +""" A select step is one that contains a choice to select one of several alternatives. It can be seen as a special-case form with nicer rendering. Submitted using `Mutation.claimIntentSubmitSelect`. @@ -1268,6 +1300,9 @@ input ClaimIntentSubmitFormInput { stepId: ID! fields: [ClaimIntentFormSubmitInputField!]! } +input ClaimIntentSubmitInformationInput { + stepId: ID! +} input ClaimIntentSubmitSelectInput { stepId: ID! selectedId: ID! @@ -1662,6 +1697,10 @@ input CrossSellInput { A/B test experiments for attribution tracking """ experiments: [CrossSellExperimentInput!]! + """ + The contract that was changed in the flow. When set, enables addon recommendations for that contract. + """ + contractId: ID } enum CrossSellSource { HOME @@ -1677,6 +1716,10 @@ type CrossSellV2 { """ recommendedCrossSell: RecommendedCrossSell """ + Addon recommendation for the contract in input.contractId (null when not applicable) + """ + recommendedAddon: RecommendedAddonCrossSell + """ Other available cross-sells (behavior varies by userFlow) """ otherCrossSells: [CrossSell!]! @@ -2433,6 +2476,28 @@ type InsuranceEvidenceOutput { insuranceEvidenceInformation: InsuranceEvidenceInformation userError: UserError } +input InsuranceGuideInput { + """ + Email of the user. + """ + email: String + """ + Id of the insurance guide shopSession. + """ + shopSessionId: UUID! + """ + Insurance recommendation form answers. + """ + answers: AnswersInput! + """ + Insurance recommendations. + """ + recommendations: [RecommendationInput!]! +} +type InsuranceGuideOutput { + success: Boolean! + userError: UserError +} input InsurelyInitiateIframeDataCollectionInput { collectionId: String! partner: String @@ -3655,6 +3720,10 @@ type Mutation { """ claimIntentSubmitTask(input: ClaimIntentSubmitTaskInput!): ClaimIntentMutationOutput! """ + Acknowledge a step containing a `ClaimIntentStepContentInformation`, continuing the flow. + """ + claimIntentSubmitInformation(input: ClaimIntentSubmitInformationInput!): ClaimIntentMutationOutput! + """ Submit a step containing a `ClaimIntentStepContentSummary`. """ claimIntentSubmitSummary(input: ClaimIntentSubmitSummaryInput!): ClaimIntentMutationOutput! @@ -3729,6 +3798,10 @@ type Mutation { """ productOffersCancellationRequestedUpdate(productOfferIds: [UUID!]!, requested: Boolean!): ProductOffersMutationOutput! """ + Called to save insurance recommendation form and save recommendation email. + """ + insuranceGuide(input: InsuranceGuideInput!): InsuranceGuideOutput! + """ Create a `PriceIntent`. The `input.productName` has to be the name of a product among the ones listed at `Query.availableProducts`. The choice of product also affects which data will be needed for the intent to be confirmed and to produce offers. @@ -4305,6 +4378,10 @@ type ProductOffer { """ priceIntentId: UUID """ + Link to the price calculator for this offer (resumes the shop session). + """ + priceCalculatorLink: String + """ UI-safe masked form data used to generate the offer. PII fields (street, zipCode, city) are masked when address came from registration address lookup. """ priceIntentData: PricingFormData! @@ -4706,6 +4783,28 @@ type RecommendationExternalInsurance { """ dataCollectionId: String! } +input RecommendationInput { + productId: String! + name: String! + tier: String + productPageUrl: String! + priceCalculatorUrl: String! +} +""" +A recommendation to add an addon to an existing contract. The button opens the addon flow via an app deep link. +""" +type RecommendedAddonCrossSell { + id: ID! + title: String! + description: String! + buttonTitle: String! + """ + App deep link to the addon flow for this contract + """ + deepLink: String! + pillowImageSmall: StoryblokImageAsset! + pillowImageLarge: StoryblokImageAsset! +} type RecommendedCrossSell { crossSell: CrossSell! bannerText: String! 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 cceaeaf094..9fc2ba0058 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 @@ -22,6 +22,7 @@ import com.hedvig.android.core.demomode.ProdOrDemoProvider import com.hedvig.android.core.demomode.Provider 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 @@ -177,9 +178,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-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 6c4fe1dbda..6ff5712235 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 @@ -145,6 +145,7 @@ internal class GetHomeDataUseCaseImpl( val crossSells = CrossSellSheetData( recommendedCrossSell = recommendedCrossSell, otherCrossSells = otherCrossSellsData, + recommendedAddon = null ) 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..a3089d99df 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("", "", ""), ), ), + 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..427ca167a8 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/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 27b90ac1d9..8793eec876 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 @@ -140,7 +140,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 +166,7 @@ internal class HomePresenterTest { isHelpCenterEnabled = false, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( - CrossSellSheetData(testCrossSell, listOf()), + CrossSellSheetData(testCrossSell, listOf(),null), crossSellRecommendationNotification = CrossSellRecommendationNotification (true, 1L), ), @@ -203,7 +203,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 +280,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = hasNotification, showHelpCenter = false, firstVetSections = listOf(), - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(),null), travelBannerInfo = null, ).right(), ) @@ -313,7 +313,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 +365,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf()), + crossSells = CrossSellSheetData(null, listOf(),null), firstVetSections = listOf( firstVet, ), @@ -420,7 +420,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 +438,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 +470,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 +516,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 +560,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/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..7fc17d5ef8 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 @@ -103,6 +103,17 @@ import org.jetbrains.compose.resources.stringResource data class CrossSellSheetData( val recommendedCrossSell: RecommendedCrossSell?, val otherCrossSells: List, + val recommendedAddon: RecommendedAddon? +) + +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( From 1e870753e1eddb12e8bbf8921d37bd97148c3da9 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 13:46:36 +0200 Subject: [PATCH 02/23] cross sell bottom sheet changes --- .../hedvig/android/crosssells/CrossSells.kt | 156 +++++++++++++++--- 1 file changed, 133 insertions(+), 23 deletions(-) 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 7fc17d5ef8..8d58b3d835 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,14 +84,13 @@ 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 recommendedAddon: RecommendedAddon?, ) data class RecommendedAddon( @@ -165,6 +152,7 @@ fun CrossSellFloatingBottomSheet( onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader = imageLoader, + recommendedAddon = crossSellSheetData.recommendedAddon ) }, ) @@ -194,6 +182,7 @@ fun CrossSellBottomSheet( CrossSellsSheetContent( recommendedCrossSell = crossSellSheetData.recommendedCrossSell, otherCrossSells = crossSellSheetData.otherCrossSells, + recommendedAddon = crossSellSheetData.recommendedAddon, onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader, @@ -207,6 +196,7 @@ fun CrossSellBottomSheet( private fun CrossSellsSheetContent( recommendedCrossSell: RecommendedCrossSell?, otherCrossSells: List, + recommendedAddon: RecommendedAddon?, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, imageLoader: ImageLoader, @@ -216,7 +206,14 @@ private fun CrossSellsSheetContent( verticalArrangement = Arrangement.spacedBy(40.dp), modifier = Modifier.padding(bottom = 24.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, //todo: check! + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -257,6 +254,7 @@ private fun CrossSellsSheetContent( @Composable private fun CrossSellsFloatingSheetContent( recommendedCrossSell: RecommendedCrossSell?, + recommendedAddon: RecommendedAddon?, otherCrossSells: List, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, @@ -276,7 +274,14 @@ private fun CrossSellsFloatingSheetContent( .padding(bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(40.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, //todo: check! + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -320,6 +325,82 @@ 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 + .size(96.dp), + ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .align(Alignment.TopEnd) + .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(16.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, @@ -826,6 +907,7 @@ private fun PreviewCrossSellsSheetContent( onCrossSellClick = {}, dismissSheet = {}, imageLoader = rememberPreviewImageLoader(), + recommendedAddon = null ) } } @@ -840,7 +922,8 @@ private fun PreviewCrossSellsFloatingSheetContent( HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { CrossSellsFloatingSheetContent( - RecommendedCrossSell( + recommendedAddon = null, + recommendedCrossSell = RecommendedCrossSell( crossSell = CrossSell( "rh", "Car Insurance", @@ -855,7 +938,7 @@ private fun PreviewCrossSellsFloatingSheetContent( backgroundPillowImages = ("ds" to "ds"), bundleProgress = BundleProgress(1, 15), ).takeIf { case != TripleCase.THIRD }, - listOf( + otherCrossSells = listOf( CrossSell( "id", "title", @@ -864,8 +947,8 @@ private fun PreviewCrossSellsFloatingSheetContent( ImageAsset("", "", ""), ), ).takeIf { case != TripleCase.FIRST }.orEmpty(), - {}, - {}, + dismissSheet = {}, + onCrossSellClick = {}, imageLoader = rememberPreviewImageLoader(), ) } @@ -940,3 +1023,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 = {}, + ) + } + } +} From 349e95662154d70d9d6ef7b48e64ea32e94ef6f2 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 13:53:59 +0200 Subject: [PATCH 03/23] add contractId to the input --- .../sell/after/flow/CrossSellAfterFlowRepository.kt | 13 +++++++++++-- .../cross/sell/sheet/CrossSellSheetViewModel.kt | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) 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..1fae97d7dd 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 { @@ -33,6 +35,7 @@ sealed class CrossSellInfoType() { val info: ClaimInfo, ) : CrossSellInfoType() { override val source: String = "closedClaim" + override val contractId: String? = null //todo! override val extraInfo: Map = with(info) { buildMap { this.put("id", id) @@ -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 //todo! } data object EditCoInsured : CrossSellInfoType() { override val source: String = "editCoInsured" override val extraInfo: Map? = null + override val contractId: String? = null //todo! } - 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-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 9fc2ba0058..3992dd01c1 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 @@ -120,14 +120,15 @@ internal fun CrossSellInfoType.toCrossSellSource(): CrossSellInput { userFlow = UserFlow.SMART_X_SELL, flowSource = Optional.present(flowSource), experiments = emptyList(), + contractId = Optional.present(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) } } From 89d779afc173751f79c35a1bd0b4bd40f33e8f30 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 14:06:50 +0200 Subject: [PATCH 04/23] add contractId to change tier and moving --- .../android/data/changetier/data/ChangeTierRepository.kt | 7 ++++--- .../feature/movingflow/ui/summary/SummaryViewModel.kt | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) 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..2731fd2948 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, insuranceId: 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, insuranceId: String,): Either { return either { apolloClient .mutation(ChangeTierDeductibleCommitIntentMutation(quoteId)) @@ -84,7 +84,8 @@ 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(insuranceId),) } } 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..76de59a4da 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 @@ -108,6 +108,7 @@ internal class SummaryPresenter( LaunchedEffect(Unit) { movingFlowRepository.movingFlowState().collect { movingFlowState -> val movingFlowQuotes = movingFlowState?.movingFlowQuotes + val contractId = movingFlowState?.moveFromAddressId if (movingFlowQuotes == null) { logcat(LogPriority.ERROR) { "In moving flow summary, no moving flow quotes found." } summaryInfo = SummaryInfoState.Error.MissingOngoingMovingFlow @@ -123,6 +124,7 @@ internal class SummaryPresenter( SummaryInfo( moveHomeQuote = matchingMoveHomeQuote, moveMtaQuotes = moveMtaQuotes, + currentInsuranceId = contractId ), ) } @@ -176,8 +178,9 @@ internal class SummaryPresenter( submitChangesError = SubmitError.WithMessage(userErrorMessage) } } else { + val currentContractId = (summaryInfo as? SummaryInfoState.Content)?.summaryInfo?.currentInsuranceId crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( - CrossSellInfoType.MovingFlow, + CrossSellInfoType.MovingFlow(currentContractId), ) submitChangesWithData = null backstack.popUpTo(inclusive = true) @@ -352,6 +355,7 @@ internal sealed interface SummaryEvent { internal data class SummaryInfo( val moveHomeQuote: MoveHomeQuote, val moveMtaQuotes: List, + val currentInsuranceId: String? ) private data class SubmitChangesData( From fe8aa457afeaa876919b57bf203c33c2bc244e33 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 14:27:42 +0200 Subject: [PATCH 05/23] add contractId to change tier and moving --- .../feature/change/tier/ui/stepsummary/SummaryViewModel.kt | 2 +- .../android/feature/movingflow/ui/summary/SummaryDestination.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt index 9a7140c9c9..a0dfa13e94 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt @@ -499,6 +499,7 @@ private class SummaryUiStateProvider : PreviewParameterProvider ), ), ), + currentInsuranceId = "sds" ), isSubmitting = false, submitError = null, From ae91f1ced172683439a4492b6d115a8d0a0b4224 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 14:32:21 +0200 Subject: [PATCH 06/23] todo for new contract Id in the moving flow --- .../feature/movingflow/ui/summary/SummaryViewModel.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 76de59a4da..40e3346646 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 @@ -108,7 +108,6 @@ internal class SummaryPresenter( LaunchedEffect(Unit) { movingFlowRepository.movingFlowState().collect { movingFlowState -> val movingFlowQuotes = movingFlowState?.movingFlowQuotes - val contractId = movingFlowState?.moveFromAddressId if (movingFlowQuotes == null) { logcat(LogPriority.ERROR) { "In moving flow summary, no moving flow quotes found." } summaryInfo = SummaryInfoState.Error.MissingOngoingMovingFlow @@ -124,7 +123,6 @@ internal class SummaryPresenter( SummaryInfo( moveHomeQuote = matchingMoveHomeQuote, moveMtaQuotes = moveMtaQuotes, - currentInsuranceId = contractId ), ) } @@ -178,9 +176,11 @@ internal class SummaryPresenter( submitChangesError = SubmitError.WithMessage(userErrorMessage) } } else { - val currentContractId = (summaryInfo as? SummaryInfoState.Content)?.summaryInfo?.currentInsuranceId crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( - CrossSellInfoType.MovingFlow(currentContractId), + CrossSellInfoType.MovingFlow( + null + // moveIntentCommit.newContractId //todo!!! + ), ) submitChangesWithData = null backstack.popUpTo(inclusive = true) @@ -354,8 +354,7 @@ internal sealed interface SummaryEvent { internal data class SummaryInfo( val moveHomeQuote: MoveHomeQuote, - val moveMtaQuotes: List, - val currentInsuranceId: String? + val moveMtaQuotes: List ) private data class SubmitChangesData( From 65439d88ad889bb32512c303d309056556e6d65d Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 15:09:12 +0200 Subject: [PATCH 07/23] ui adjustments --- .../ui/summary/SummaryDestination.kt | 3 +- .../hedvig/android/crosssells/CrossSells.kt | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt index a0dfa13e94..8ac5438947 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt @@ -498,8 +498,7 @@ private class SummaryUiStateProvider : PreviewParameterProvider ), ), ), - ), - currentInsuranceId = "sds" + ) ), isSubmitting = false, submitError = null, 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 8d58b3d835..dd2d30c919 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 @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalLayoutDirection @@ -61,6 +62,7 @@ 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.debugBorder 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 @@ -207,6 +209,7 @@ private fun CrossSellsSheetContent( modifier = Modifier.padding(bottom = 24.dp), ) { if (recommendedAddon != null) { + Spacer(Modifier.height(36.dp)) AddonRecommendationSection( recommendedAddon, onButtonClick = onCrossSellClick, //todo: check! @@ -348,23 +351,30 @@ private fun AddonRecommendationSection( imageLoader = imageLoader, contentScale = ContentScale.Crop, modifier = Modifier - .size(96.dp), + .padding(8.dp) + .size(140.dp), ) - Box( - contentAlignment = Alignment.Center, - modifier = Modifier + Row( + Modifier .align(Alignment.TopEnd) - .hedvigDropShadow(CircleShape) - .size(30.dp) - .background(HedvigTheme.colorScheme.fillNegative, CircleShape) - .border(1.dp, HedvigTheme.colorScheme.borderPrimary, CircleShape), + .padding(top = 16.dp) ) { - Icon( - imageVector = HedvigIcons.Plus, - contentDescription = EmptyContentDescription, - tint = HedvigTheme.colorScheme.fillPrimary, - modifier = Modifier.size(16.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(16.dp)) } } Spacer(Modifier.height(24.dp)) From e9a0c38ffa9d056bd0ce002d195e679f6a39bb3f Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 15:14:33 +0200 Subject: [PATCH 08/23] remove todo --- .../main/kotlin/com/hedvig/android/crosssells/CrossSells.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dd2d30c919..56cdb36969 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 @@ -212,7 +212,7 @@ private fun CrossSellsSheetContent( Spacer(Modifier.height(36.dp)) AddonRecommendationSection( recommendedAddon, - onButtonClick = onCrossSellClick, //todo: check! + onButtonClick = onCrossSellClick, dismissSheet = dismissSheet, imageLoader = imageLoader, ) @@ -280,7 +280,7 @@ private fun CrossSellsFloatingSheetContent( if (recommendedAddon != null) { AddonRecommendationSection( recommendedAddon, - onButtonClick = onCrossSellClick, //todo: check! + onButtonClick = onCrossSellClick, dismissSheet = dismissSheet, imageLoader = imageLoader, ) From 92a72e379b8bb7ef042d0c67828df5bc60b7be7b Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 15:22:15 +0200 Subject: [PATCH 09/23] non-null recommendedAddon in Home --- .../src/main/graphql/QueryHome.graphql | 13 +++++++++++++ .../feature/home/home/data/GetHomeDataUseCase.kt | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) 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 6ff5712235..6bcea261e3 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 @@ -15,6 +15,7 @@ import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.demomode.Provider 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 @@ -142,10 +143,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 = null + recommendedAddon = recommendedAddon ) val showChatIcon = shouldShowChatButton( isInboxEnabledFromKillSwitch = inboxAlwaysAvailable, From 7280c654e57aab2506497109c68d888834cbccb5 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 15:59:35 +0200 Subject: [PATCH 10/23] fix tests --- .../feature-choose-tier/src/test/kotlin/CommonTestdata.kt | 2 +- .../step/survey/TerminationSurveyPresenterTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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..797a1bc128 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, insuranceId: String): Either { return either {} } 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..ffbd207994 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, insuranceId: String): Either { return either {} } From e4d1ed345eacc57438c0a36ef26b419ff2b997eb Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 25 Jun 2026 13:03:23 +0200 Subject: [PATCH 11/23] pass new newContractId in the moving flow --- .../com/hedvig/android/apollo/octopus/schema.graphqls | 5 +++++ .../src/main/graphql/MutationMoveIntentCommit.graphql | 1 + .../feature/movingflow/ui/summary/SummaryViewModel.kt | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index 921e297030..fc9701f8a2 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -888,6 +888,7 @@ type Claim { isUploadingFilesEnabled: Boolean! infoText: String displayItems: [ClaimDisplayItem!]! + contractId: String """ Terms & conditions for the claim found using claims contractId and dateOfOccurrence, otherwise null. """ @@ -3514,6 +3515,10 @@ type MoveIntentMutationOutput { Fail case """ userError: UserError + """ + Id of the new contract if it was created after finalizing the move + """ + newContractId: ID } type MoveIntentQuoteCost { quoteId: ID! 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 40e3346646..dd7a654679 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 @@ -178,8 +178,7 @@ internal class SummaryPresenter( } else { crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( CrossSellInfoType.MovingFlow( - null - // moveIntentCommit.newContractId //todo!!! + moveIntentCommit.newContractId ), ) submitChangesWithData = null From 27bee4fcaac2261f676805bf75f08c662435b7b6 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 25 Jun 2026 13:18:47 +0200 Subject: [PATCH 12/23] paddings --- .../kotlin/com/hedvig/android/crosssells/CrossSells.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 56cdb36969..c29b21e85d 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 @@ -209,7 +209,7 @@ private fun CrossSellsSheetContent( modifier = Modifier.padding(bottom = 24.dp), ) { if (recommendedAddon != null) { - Spacer(Modifier.height(36.dp)) + Spacer(Modifier.height(16.dp)) AddonRecommendationSection( recommendedAddon, onButtonClick = onCrossSellClick, @@ -351,13 +351,13 @@ private fun AddonRecommendationSection( imageLoader = imageLoader, contentScale = ContentScale.Crop, modifier = Modifier - .padding(8.dp) + .padding(horizontal = 8.dp) .size(140.dp), ) Row( Modifier .align(Alignment.TopEnd) - .padding(top = 16.dp) + .padding(top = 8.dp) ) { Box( contentAlignment = Alignment.Center, @@ -374,7 +374,7 @@ private fun AddonRecommendationSection( modifier = Modifier.size(24.dp), ) } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) } } Spacer(Modifier.height(24.dp)) From 0ed86ca2274e2e037229c9b6000b95c6df89096f Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 25 Jun 2026 13:34:18 +0200 Subject: [PATCH 13/23] fix test --- .../src/test/kotlin/data/ChangeTierRepositoryImplTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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..e6c66aeaa4 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 testInsuranceId = "testInsuranceId" 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, testInsuranceId) assertThat(result) .isLeft() } @@ -98,10 +99,10 @@ class ChangeTierRepositoryImplTest { } }, ) - val result = repository.submitChangeTierQuote(testId) + val result = repository.submitChangeTierQuote(testId, testInsuranceId) assertThat(result).isRight().isEqualTo(Unit) assertThat(crossSellAfterFlowRepository.shouldShowCrossSellSheetWithInfo().first()) - .isEqualTo(CrossSellInfoType.ChangeTier) + .isEqualTo(CrossSellInfoType.ChangeTier(testInsuranceId)) } @Test From 1f8aaf46e3b5003f3a21df0ac3d1d0ffe010d7cc Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 26 Jun 2026 09:24:47 +0200 Subject: [PATCH 14/23] add contractId for closed claim --- .../apollo/octopus/graphql/FragmentClaimFragment.graphql | 1 + .../claim/closed/CrossSellAfterClaimClosedRepository.kt | 2 ++ .../cross/sell/after/flow/CrossSellAfterFlowRepository.kt | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) 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-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..f687ea0734 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 @@ -49,7 +49,9 @@ internal class CrossSellAfterClaimClosedRepositoryImpl( status = claim.status?.name, 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 1fae97d7dd..bcd66712bd 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 @@ -33,9 +33,9 @@ sealed class CrossSellInfoType() { data class ClosedClaim( val info: ClaimInfo, + override val contractId: String? ) : CrossSellInfoType() { override val source: String = "closedClaim" - override val contractId: String? = null //todo! override val extraInfo: Map = with(info) { buildMap { this.put("id", id) @@ -65,13 +65,13 @@ sealed class CrossSellInfoType() { data object Addon : CrossSellInfoType() { override val source: String = "addon" override val extraInfo: Map? = null - override val contractId: String? = null //todo! + override val contractId: String? = null } data object EditCoInsured : CrossSellInfoType() { override val source: String = "editCoInsured" override val extraInfo: Map? = null - override val contractId: String? = null //todo! + override val contractId: String? = null } data class MovingFlow( From 91c931a31fc01fee4a30dd09f22554d712ce977a Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 26 Jun 2026 10:23:18 +0200 Subject: [PATCH 15/23] publish to staging --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 12747add14f50c46b40142c9fb9631c90a6fe9dd Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 26 Jun 2026 14:52:04 +0200 Subject: [PATCH 16/23] FetchPolicy.NetworkOnly --- app/feature/feature-cross-sell-sheet/build.gradle.kts | 2 ++ .../feature/cross/sell/sheet/CrossSellSheetViewModel.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/feature/feature-cross-sell-sheet/build.gradle.kts b/app/feature/feature-cross-sell-sheet/build.gradle.kts index 9e27cf44f7..62f3bd9bac 100644 --- a/app/feature/feature-cross-sell-sheet/build.gradle.kts +++ b/app/feature/feature-cross-sell-sheet/build.gradle.kts @@ -11,6 +11,7 @@ hedvig { dependencies { implementation(libs.apollo.runtime) + implementation(libs.apollo.normalizedCache) implementation(libs.arrow.core) implementation(libs.arrow.fx) implementation(projects.apolloCore) @@ -23,4 +24,5 @@ dependencies { implementation(projects.dataCrossSellAfterFlow) implementation(projects.designSystemHedvig) implementation(projects.moleculePublic) + } 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 8d72f23d25..aa6629652f 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 @@ -47,6 +47,8 @@ import octopus.type.CrossSellInput import octopus.type.CrossSellSource import octopus.type.FlowSource import octopus.type.UserFlow +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy @Inject @HedvigViewModel(ActivityRetainedScope::class) @@ -153,6 +155,7 @@ internal class GetCrossSellSheetDataUseCaseImpl( override suspend fun invoke(source: CrossSellInput): Flow> { return apolloClient .query(BottomSheetCrossSellsQuery(source)) + .fetchPolicy(FetchPolicy.NetworkOnly) .safeFlow(::ErrorMessage) .map { response -> either { From cdd45a46c4e65b4cf6ce1a6ed0831cedd2f967b6 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 15:16:48 +0200 Subject: [PATCH 17/23] Run ktlint --- .../changetier/data/ChangeTierRepository.kt | 7 ++++--- .../CrossSellAfterClaimClosedRepository.kt | 3 +-- .../flow/CrossSellAfterFlowRepository.kt | 6 +++--- .../sell/sheet/CrossSellSheetViewModel.kt | 10 +++++----- .../home/home/data/GetHomeDataUseCase.kt | 4 ++-- .../home/home/data/GetHomeDataUseCaseDemo.kt | 2 +- .../feature/home/home/ui/HomeDestination.kt | 2 +- .../feature/home/home/ui/HomePresenterTest.kt | 20 +++++++++---------- .../ui/summary/SummaryDestination.kt | 2 +- .../movingflow/ui/summary/SummaryViewModel.kt | 4 ++-- .../survey/TerminationSurveyDestination.kt | 6 ++++-- .../hedvig/android/crosssells/CrossSells.kt | 14 ++++++------- 12 files changed, 40 insertions(+), 40 deletions(-) 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 2731fd2948..94fffa35eb 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, insuranceId: String,): Either + suspend fun submitChangeTierQuote(quoteId: String, insuranceId: String): Either suspend fun getCurrentQuoteId(): String } @@ -74,7 +74,7 @@ internal class ChangeTierRepositoryImpl( changeTierQuoteStorage.insertAll(quotes) } - override suspend fun submitChangeTierQuote(quoteId: String, insuranceId: String,): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either { apolloClient .mutation(ChangeTierDeductibleCommitIntentMutation(quoteId)) @@ -85,7 +85,8 @@ internal class ChangeTierRepositoryImpl( } .bind() crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( - CrossSellInfoType.ChangeTier(insuranceId),) + CrossSellInfoType.ChangeTier(contractId), + ) } } 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 f687ea0734..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 @@ -49,9 +49,8 @@ internal class CrossSellAfterClaimClosedRepositoryImpl( status = claim.status?.name, type = claim.claimType, typeOfContract = claim.productVariant?.typeOfContract, - ), - contractId = claim.contractId + 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 bcd66712bd..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 @@ -33,7 +33,7 @@ sealed class CrossSellInfoType() { data class ClosedClaim( val info: ClaimInfo, - override val contractId: String? + override val contractId: String?, ) : CrossSellInfoType() { override val source: String = "closedClaim" override val extraInfo: Map = with(info) { @@ -56,7 +56,7 @@ sealed class CrossSellInfoType() { } data class ChangeTier( - override val contractId: String? + override val contractId: String?, ) : CrossSellInfoType() { override val source: String = "changeTier" override val extraInfo: Map? = null @@ -75,7 +75,7 @@ sealed class CrossSellInfoType() { } data class MovingFlow( - override val contractId: String? + override val contractId: String?, ) : CrossSellInfoType() { override val source: String = "movingFlow" override val extraInfo: Map? = null 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 aa6629652f..fd14a5e595 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 @@ -47,8 +49,6 @@ import octopus.type.CrossSellInput import octopus.type.CrossSellSource import octopus.type.FlowSource import octopus.type.UserFlow -import com.apollographql.apollo.cache.normalized.FetchPolicy -import com.apollographql.apollo.cache.normalized.fetchPolicy @Inject @HedvigViewModel(ActivityRetainedScope::class) @@ -121,7 +121,7 @@ internal fun CrossSellInfoType.toCrossSellSource(): CrossSellInput { userFlow = UserFlow.SMART_X_SELL, flowSource = Optional.present(flowSource), experiments = emptyList(), - contractId = Optional.present(this.contractId) + contractId = Optional.present(this.contractId), ) } return when (this) { @@ -191,13 +191,13 @@ internal class GetCrossSellSheetDataUseCaseImpl( description = it.description, deepLink = it.deepLink, pillowImageSmall = it.pillowImageSmall.src, - pillowImageLarge = it.pillowImageLarge.src + pillowImageLarge = it.pillowImageLarge.src, ) } CrossSellSheetData( recommendedCrossSell = recommendedData, otherCrossSells = otherCrossSellsData, - recommendedAddon = recommendedAddon + recommendedAddon = recommendedAddon, ) } } 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 8c8a6281c3..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 @@ -145,13 +145,13 @@ internal class GetHomeDataUseCaseImpl( description = it.description, deepLink = it.deepLink, pillowImageSmall = it.pillowImageSmall.src, - pillowImageLarge = it.pillowImageLarge.src + pillowImageLarge = it.pillowImageLarge.src, ) } val crossSells = CrossSellSheetData( recommendedCrossSell = recommendedCrossSell, otherCrossSells = otherCrossSellsData, - recommendedAddon = recommendedAddon + 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 a3089d99df..942aee19d0 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,7 +53,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ImageAsset("", "", ""), ), ), - null + 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 427ca167a8..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,7 +771,7 @@ private fun PreviewHomeScreen( ImageAsset("", "", ""), ), ), - recommendedAddon = null + recommendedAddon = null, ), crossSellRecommendationNotification = CrossSellRecommendationNotification( true, 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 de5fcd5db3..28ffbf26fe 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 @@ -166,7 +166,7 @@ internal class HomePresenterTest { isHelpCenterEnabled = false, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( - CrossSellSheetData(testCrossSell, listOf(),null), + CrossSellSheetData(testCrossSell, listOf(), null), crossSellRecommendationNotification = CrossSellRecommendationNotification (true, 1L), ), @@ -203,7 +203,7 @@ internal class HomePresenterTest { ), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf(),null), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -280,7 +280,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = hasNotification, showHelpCenter = false, firstVetSections = listOf(), - crossSells = CrossSellSheetData(null, listOf(),null), + crossSells = CrossSellSheetData(null, listOf(), null), travelBannerInfo = null, ).right(), ) @@ -313,7 +313,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf(),null), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -365,7 +365,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, listOf(),null), + crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf( firstVet, ), @@ -420,7 +420,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell),null), + crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -438,7 +438,7 @@ internal class HomePresenterTest { chatAction = null, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( - CrossSellSheetData(testCrossSell, listOf(crossSell),null), + CrossSellSheetData(testCrossSell, listOf(crossSell), null), crossSellRecommendationNotification = CrossSellRecommendationNotification (true, 1L), ), @@ -470,7 +470,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = true, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, emptyList(),null), + crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -516,7 +516,7 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), showChatIcon = false, hasUnseenChatMessages = false, - crossSells = CrossSellSheetData(null, emptyList(),null), + crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, @@ -560,7 +560,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, showHelpCenter = false, firstVetSections = listOf(), - crossSells = CrossSellSheetData(null, emptyList(),null), + crossSells = CrossSellSheetData(null, emptyList(), null), travelBannerInfo = null, ) } diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt index 8ac5438947..9a7140c9c9 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt @@ -498,7 +498,7 @@ private class SummaryUiStateProvider : PreviewParameterProvider ), ), ), - ) + ), ), isSubmitting = false, submitError = null, 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 dd7a654679..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 @@ -178,7 +178,7 @@ internal class SummaryPresenter( } else { crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( CrossSellInfoType.MovingFlow( - moveIntentCommit.newContractId + moveIntentCommit.newContractId, ), ) submitChangesWithData = null @@ -353,7 +353,7 @@ internal sealed interface SummaryEvent { internal data class SummaryInfo( val moveHomeQuote: MoveHomeQuote, - val moveMtaQuotes: List + val moveMtaQuotes: List, ) private data class SubmitChangesData( 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/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 c29b21e85d..71a4ea0b35 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 @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalLayoutDirection @@ -62,7 +61,6 @@ 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.debugBorder 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 @@ -154,7 +152,7 @@ fun CrossSellFloatingBottomSheet( onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader = imageLoader, - recommendedAddon = crossSellSheetData.recommendedAddon + recommendedAddon = crossSellSheetData.recommendedAddon, ) }, ) @@ -357,7 +355,7 @@ private fun AddonRecommendationSection( Row( Modifier .align(Alignment.TopEnd) - .padding(top = 8.dp) + .padding(top = 8.dp), ) { Box( contentAlignment = Alignment.Center, @@ -406,7 +404,7 @@ private fun AddonRecommendationSection( }, enabled = true, modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) } } @@ -917,7 +915,7 @@ private fun PreviewCrossSellsSheetContent( onCrossSellClick = {}, dismissSheet = {}, imageLoader = rememberPreviewImageLoader(), - recommendedAddon = null + recommendedAddon = null, ) } } @@ -957,7 +955,7 @@ private fun PreviewCrossSellsFloatingSheetContent( ImageAsset("", "", ""), ), ).takeIf { case != TripleCase.FIRST }.orEmpty(), - dismissSheet = {}, + dismissSheet = {}, onCrossSellClick = {}, imageLoader = rememberPreviewImageLoader(), ) @@ -1052,7 +1050,7 @@ private fun PreviewRecommendedAddon( description = "Best addon in the world", deepLink = "deep", pillowImageSmall = "src", - pillowImageLarge = "src" + pillowImageLarge = "src", ), onCrossSellClick = {}, dismissSheet = {}, From 6f7d9f997ed1076d85ee09d31f903518792e3c19 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:06:54 +0200 Subject: [PATCH 18/23] Show the default banner text over addon-only cross-sell sheets --- .../hedvig/android/crosssells/CrossSells.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) 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 71a4ea0b35..ce03d31d2a 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 @@ -132,7 +132,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), @@ -165,11 +166,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 { @@ -207,13 +209,15 @@ private fun CrossSellsSheetContent( modifier = Modifier.padding(bottom = 24.dp), ) { if (recommendedAddon != null) { - Spacer(Modifier.height(16.dp)) - AddonRecommendationSection( - recommendedAddon, - onButtonClick = onCrossSellClick, - dismissSheet = dismissSheet, - imageLoader = imageLoader, - ) + Column { + Spacer(Modifier.height(48.dp)) + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) @@ -276,12 +280,15 @@ private fun CrossSellsFloatingSheetContent( verticalArrangement = Arrangement.spacedBy(40.dp), ) { if (recommendedAddon != null) { - AddonRecommendationSection( - recommendedAddon, - onButtonClick = onCrossSellClick, - dismissSheet = dismissSheet, - imageLoader = imageLoader, - ) + Column { + Spacer(Modifier.height(48.dp)) + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) From 259d70c5e58dfb7d5a486fce2a0e73dac2349149 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:07:08 +0200 Subject: [PATCH 19/23] Use the real imageLoader in the floating sheet cross-sells section --- .../src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ce03d31d2a..7c9bc20cc9 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 @@ -310,7 +310,7 @@ private fun CrossSellsFloatingSheetContent( onCrossSellClick = onCrossSellClick, withSubHeader = false, onSheetDismissed = dismissSheet, - imageLoader = rememberPreviewImageLoader(), + imageLoader = imageLoader, ) } } From 6c2e8862ee904327bb8e671739861b432d7a70e6 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:07:22 +0200 Subject: [PATCH 20/23] Include recommendedAddon in the Home cross-sells action gate, test addon mapping --- .../home/home/data/GetHomeDataUseCaseDemo.kt | 2 +- .../feature/home/home/ui/HomePresenter.kt | 1 + .../home/home/data/GetHomeUseCaseTest.kt | 56 +++++++++++++++++++ .../feature/home/home/ui/HomePresenterTest.kt | 52 +++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) 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 942aee19d0..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,7 +53,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ImageAsset("", "", ""), ), ), - null, + recommendedAddon = null, ), travelBannerInfo = null, showChatIcon = false, 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 28ffbf26fe..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() @@ -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() From a8f98c222ba891cd6be17d3ea29878fa0136290b Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:07:37 +0200 Subject: [PATCH 21/23] Rename submitChangeTierQuote param to contractId to match its usage --- .../android/data/changetier/data/ChangeTierRepository.kt | 2 +- .../src/test/kotlin/data/ChangeTierRepositoryImplTest.kt | 8 ++++---- .../feature-choose-tier/src/test/kotlin/CommonTestdata.kt | 2 +- .../step/survey/TerminationSurveyPresenterTest.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) 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 94fffa35eb..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, insuranceId: String): Either + suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either suspend fun getCurrentQuoteId(): String } 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 e6c66aeaa4..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,7 +45,7 @@ class ChangeTierRepositoryImplTest { val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) private val testId = "testId" - private val testInsuranceId = "testInsuranceId" + private val testContractId = "testContractId" private val apolloClientWithBadResponseToSubmit: ApolloClient get() = testApolloClientRule.apolloClient.apply { @@ -75,7 +75,7 @@ class ChangeTierRepositoryImplTest { crossSellAfterFlowRepository = CrossSellAfterFlowRepositoryImpl(), changeTierQuoteStorage = storage, ) - val result = repository.submitChangeTierQuote(testId, testInsuranceId) + val result = repository.submitChangeTierQuote(testId, testContractId) assertThat(result) .isLeft() } @@ -99,10 +99,10 @@ class ChangeTierRepositoryImplTest { } }, ) - val result = repository.submitChangeTierQuote(testId, testInsuranceId) + val result = repository.submitChangeTierQuote(testId, testContractId) assertThat(result).isRight().isEqualTo(Unit) assertThat(crossSellAfterFlowRepository.shouldShowCrossSellSheetWithInfo().first()) - .isEqualTo(CrossSellInfoType.ChangeTier(testInsuranceId)) + .isEqualTo(CrossSellInfoType.ChangeTier(testContractId)) } @Test 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 797a1bc128..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, insuranceId: String): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either {} } 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 ffbd207994..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, insuranceId: String): Either { + override suspend fun submitChangeTierQuote(quoteId: String, contractId: String): Either { return either {} } From 4c5d1bc60d3339f790d622f66bfb044a6ffc2ae5 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:07:37 +0200 Subject: [PATCH 22/23] Leave contractId absent in CrossSellInput for flows without one --- .../feature-cross-sell-sheet/build.gradle.kts | 2 ++ .../sell/sheet/CrossSellSheetViewModel.kt | 2 +- .../CrossSellInfoTypeToCrossSellInputTest.kt | 36 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellInfoTypeToCrossSellInputTest.kt diff --git a/app/feature/feature-cross-sell-sheet/build.gradle.kts b/app/feature/feature-cross-sell-sheet/build.gradle.kts index 62f3bd9bac..d0d5209fe7 100644 --- a/app/feature/feature-cross-sell-sheet/build.gradle.kts +++ b/app/feature/feature-cross-sell-sheet/build.gradle.kts @@ -25,4 +25,6 @@ dependencies { implementation(projects.designSystemHedvig) implementation(projects.moleculePublic) + testImplementation(libs.assertK) + testImplementation(libs.junit) } 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 fd14a5e595..c3a171d030 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 @@ -121,7 +121,7 @@ internal fun CrossSellInfoType.toCrossSellSource(): CrossSellInput { userFlow = UserFlow.SMART_X_SELL, flowSource = Optional.present(flowSource), experiments = emptyList(), - contractId = Optional.present(this.contractId), + contractId = Optional.presentIfNotNull(this.contractId), ) } return when (this) { 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()) + } +} From b9b963dba21f12c696c9517cb77096d2a0811015 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:37:44 +0200 Subject: [PATCH 23/23] Don't show the cross-sell sheet when the response has nothing to show --- .../feature-cross-sell-sheet/build.gradle.kts | 7 ++ .../sell/sheet/CrossSellSheetViewModel.kt | 10 ++- .../sell/sheet/CrossSellSheetPresenterTest.kt | 84 +++++++++++++++++++ .../hedvig/android/crosssells/CrossSells.kt | 5 +- 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/feature/feature-cross-sell-sheet/src/test/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetPresenterTest.kt diff --git a/app/feature/feature-cross-sell-sheet/build.gradle.kts b/app/feature/feature-cross-sell-sheet/build.gradle.kts index d0d5209fe7..6c52abd945 100644 --- a/app/feature/feature-cross-sell-sheet/build.gradle.kts +++ b/app/feature/feature-cross-sell-sheet/build.gradle.kts @@ -9,6 +9,10 @@ hedvig { viewModels() } +android { + testOptions.unitTests.isReturnDefaultValues = true +} + dependencies { implementation(libs.apollo.runtime) implementation(libs.apollo.normalizedCache) @@ -26,5 +30,8 @@ dependencies { 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/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 c3a171d030..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 @@ -74,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 { @@ -103,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) + } + }, ) }, ) 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/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 7c9bc20cc9..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 @@ -91,7 +91,10 @@ 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,