From 3b9627e81f42bd200c31b98e195f1a9af94d825b Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 24 Jun 2026 11:08:14 +0200 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 {