diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 4e1f56326d..e9731178c5 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -3,7 +3,7 @@ on: push: branches: - develop - - eng/metro-nav3-pr2-nav2-to-nav3 + - feat/xsell-addon-rec workflow_dispatch: concurrency: diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql index 23e721610e..3e54af32d5 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/graphql/FragmentClaimFragment.graphql @@ -33,4 +33,5 @@ fragment ClaimFragment on Claim { displayTitle displayValue } + contractId } diff --git a/app/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..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 @@ -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 @@ -872,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. """ @@ -1003,7 +1020,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 +1198,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 +1301,9 @@ input ClaimIntentSubmitFormInput { stepId: ID! fields: [ClaimIntentFormSubmitInputField!]! } +input ClaimIntentSubmitInformationInput { + stepId: ID! +} input ClaimIntentSubmitSelectInput { stepId: ID! selectedId: ID! @@ -1662,6 +1698,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 +1717,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 +2477,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 @@ -3449,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! @@ -3655,6 +3725,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 +3803,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 +4383,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 +4788,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/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/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 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 ad7b38b66d..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 @@ -20,6 +20,8 @@ interface CrossSellAfterFlowRepository { sealed class CrossSellInfoType() { abstract val source: String + + abstract val contractId: String? protected abstract val extraInfo: Map? val attributes: Map get() = buildMap { @@ -31,6 +33,7 @@ sealed class CrossSellInfoType() { data class ClosedClaim( val info: ClaimInfo, + override val contractId: String? ) : CrossSellInfoType() { override val source: String = "closedClaim" override val extraInfo: Map = with(info) { @@ -52,7 +55,9 @@ sealed class CrossSellInfoType() { ) } - data object ChangeTier : CrossSellInfoType() { + data class ChangeTier( + override val contractId: String? + ) : CrossSellInfoType() { override val source: String = "changeTier" override val extraInfo: Map? = null } @@ -60,14 +65,18 @@ sealed class CrossSellInfoType() { data object Addon : CrossSellInfoType() { override val source: String = "addon" override val extraInfo: Map? = null + override val contractId: String? = null } data object EditCoInsured : CrossSellInfoType() { override val source: String = "editCoInsured" override val extraInfo: Map? = null + override val contractId: String? = null } - data object MovingFlow : CrossSellInfoType() { + data class MovingFlow( + override val contractId: String? + ) : CrossSellInfoType() { override val source: String = "movingFlow" override val extraInfo: Map? = null } diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt index 862c101340..5a4a1a7560 100644 --- a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt @@ -81,7 +81,7 @@ private class SummaryPresenter( if (submitIteration > 0) { val previousState = currentState currentState = MakingChanges - tierRepository.submitChangeTierQuote(params.quoteIdToSubmit).fold( + tierRepository.submitChangeTierQuote(params.quoteIdToSubmit, params.insuranceId).fold( ifLeft = { currentState = previousState backstack.add(SubmitFailureKey) diff --git a/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt b/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt index 2aff04184e..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-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/graphql/QueryBottomSheetCrossSells.graphql b/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql index 2181c30b7b..57e42e2751 100644 --- a/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql +++ b/app/feature/feature-cross-sell-sheet/src/main/graphql/QueryBottomSheetCrossSells.graphql @@ -1,6 +1,19 @@ query BottomSheetCrossSells($input: CrossSellInput!) { currentMember { crossSellV2(input: $input) { + recommendedAddon { + id + title + description + buttonTitle + deepLink + pillowImageLarge { + src + } + pillowImageSmall { + src + } + } recommendedCrossSell { crossSell { ...CrossSellFragment diff --git a/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt b/app/feature/feature-cross-sell-sheet/src/main/kotlin/com/hedvig/android/feature/cross/sell/sheet/CrossSellSheetViewModel.kt index f1168ec236..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 @@ -21,6 +21,7 @@ import com.hedvig.android.core.demomode.DemoManager import com.hedvig.android.core.demomode.DemoSwitcher import com.hedvig.android.crosssells.BundleProgress import com.hedvig.android.crosssells.CrossSellSheetData +import com.hedvig.android.crosssells.RecommendedAddon import com.hedvig.android.crosssells.RecommendedCrossSell import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset @@ -46,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) @@ -118,14 +121,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) } } @@ -151,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 { @@ -178,9 +183,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/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index c333001488..be16d2b2a1 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -58,6 +58,19 @@ query Home($claimsHistoryFlag: Boolean!) { otherCrossSells { ...HomeCrossSellFragment } + recommendedAddon { + id + title + description + buttonTitle + deepLink + pillowImageLarge { + src + } + pillowImageSmall { + src + } + } } memberActions { firstVetAction { diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index c4624d0ea3..8c8a6281c3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -14,6 +14,7 @@ import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.apollo.safeFlow import com.hedvig.android.crosssells.BundleProgress import com.hedvig.android.crosssells.CrossSellSheetData +import com.hedvig.android.crosssells.RecommendedAddon import com.hedvig.android.crosssells.RecommendedCrossSell import com.hedvig.android.data.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.AddonBannerSource @@ -136,9 +137,21 @@ internal class GetHomeDataUseCaseImpl( val otherCrossSellsData = crossSellsData.otherCrossSells.map { it.toCrossSell() } + val recommendedAddon = crossSellsData.recommendedAddon?.let { + RecommendedAddon( + id = it.id, + title = it.title, + buttonTitle = it.buttonTitle, + description = it.description, + deepLink = it.deepLink, + pillowImageSmall = it.pillowImageSmall.src, + pillowImageLarge = it.pillowImageLarge.src + ) + } val crossSells = CrossSellSheetData( recommendedCrossSell = recommendedCrossSell, otherCrossSells = otherCrossSellsData, + recommendedAddon = recommendedAddon ) val showChatIcon = shouldShowChatButton( isInboxEnabledFromKillSwitch = inboxAlwaysAvailable, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index 4ee160b208..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 4f7fde4823..de5fcd5db3 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/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/SummaryDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt index 9a7140c9c9..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,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 71a6eeff48..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 @@ -177,7 +177,9 @@ internal class SummaryPresenter( } } else { crossSellAfterFlowRepository.completedCrossSellTriggeringSelfServiceSuccessfully( - CrossSellInfoType.MovingFlow, + CrossSellInfoType.MovingFlow( + moveIntentCommit.newContractId + ), ) submitChangesWithData = null backstack.popUpTo(inclusive = true) @@ -351,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/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 {} } 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..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 @@ -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,12 @@ 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.graphics.Color 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 +38,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 +62,11 @@ 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 +import com.hedvig.android.design.system.hedvig.icon.Plus import com.hedvig.android.design.system.hedvig.placeholder.crossSellPainterFallback import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.placeholder.shimmer @@ -96,13 +86,23 @@ 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?, +) + +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( @@ -154,6 +154,7 @@ fun CrossSellFloatingBottomSheet( onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader = imageLoader, + recommendedAddon = crossSellSheetData.recommendedAddon ) }, ) @@ -183,6 +184,7 @@ fun CrossSellBottomSheet( CrossSellsSheetContent( recommendedCrossSell = crossSellSheetData.recommendedCrossSell, otherCrossSells = crossSellSheetData.otherCrossSells, + recommendedAddon = crossSellSheetData.recommendedAddon, onCrossSellClick = onCrossSellClick, dismissSheet = { state.dismiss() }, imageLoader, @@ -196,6 +198,7 @@ fun CrossSellBottomSheet( private fun CrossSellsSheetContent( recommendedCrossSell: RecommendedCrossSell?, otherCrossSells: List, + recommendedAddon: RecommendedAddon?, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, imageLoader: ImageLoader, @@ -205,7 +208,15 @@ private fun CrossSellsSheetContent( verticalArrangement = Arrangement.spacedBy(40.dp), modifier = Modifier.padding(bottom = 24.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + Spacer(Modifier.height(16.dp)) + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -246,6 +257,7 @@ private fun CrossSellsSheetContent( @Composable private fun CrossSellsFloatingSheetContent( recommendedCrossSell: RecommendedCrossSell?, + recommendedAddon: RecommendedAddon?, otherCrossSells: List, onCrossSellClick: (String) -> Unit, dismissSheet: () -> Unit, @@ -265,7 +277,14 @@ private fun CrossSellsFloatingSheetContent( .padding(bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(40.dp), ) { - if (recommendedCrossSell != null) { + if (recommendedAddon != null) { + AddonRecommendationSection( + recommendedAddon, + onButtonClick = onCrossSellClick, + dismissSheet = dismissSheet, + imageLoader = imageLoader, + ) + } else if (recommendedCrossSell != null) { Column { Spacer(Modifier.height(48.dp)) RecommendationSection( @@ -309,6 +328,89 @@ private fun CrossSellsFloatingSheetContent( } } +@Composable +private fun AddonRecommendationSection( + recommendedAddon: RecommendedAddon, + onButtonClick: (String) -> Unit, + dismissSheet: () -> Unit, + imageLoader: ImageLoader, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + val placeholder = crossSellPainterFallback(shape = HedvigTheme.shapes.cornerXXLarge) + Box { + AsyncImage( + model = recommendedAddon.pillowImageLarge, + contentDescription = EmptyContentDescription, + placeholder = placeholder, + error = placeholder, + fallback = placeholder, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(horizontal = 8.dp) + .size(140.dp), + ) + Row( + Modifier + .align(Alignment.TopEnd) + .padding(top = 8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .hedvigDropShadow(CircleShape) + .size(30.dp) + .background(HedvigTheme.colorScheme.fillNegative, CircleShape) + .border(1.dp, HedvigTheme.colorScheme.borderPrimary, CircleShape), + ) { + Icon( + imageVector = HedvigIcons.Plus, + contentDescription = EmptyContentDescription, + tint = HedvigTheme.colorScheme.fillPrimary, + modifier = Modifier.size(24.dp), + ) + } + Spacer(Modifier.width(12.dp)) + } + } + Spacer(Modifier.height(24.dp)) + val headingDescription = stringResource(Res.string.CROSS_SELL_TITLE) + + ": ${recommendedAddon.title}" + HedvigText( + text = recommendedAddon.title, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = headingDescription + heading() + }, + ) + HedvigText( + recommendedAddon.description, + style = LocalTextStyle.current.copy( + lineBreak = LineBreak.Heading, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ), + modifier = Modifier.padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(48.dp)) + HedvigButton( + text = recommendedAddon.buttonTitle, + onClick = { + onButtonClick(recommendedAddon.deepLink) + dismissSheet() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + ) + } +} + @Composable private fun RecommendationSection( recommendedCrossSell: RecommendedCrossSell, @@ -815,6 +917,7 @@ private fun PreviewCrossSellsSheetContent( onCrossSellClick = {}, dismissSheet = {}, imageLoader = rememberPreviewImageLoader(), + recommendedAddon = null ) } } @@ -829,7 +932,8 @@ private fun PreviewCrossSellsFloatingSheetContent( HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { CrossSellsFloatingSheetContent( - RecommendedCrossSell( + recommendedAddon = null, + recommendedCrossSell = RecommendedCrossSell( crossSell = CrossSell( "rh", "Car Insurance", @@ -844,7 +948,7 @@ private fun PreviewCrossSellsFloatingSheetContent( backgroundPillowImages = ("ds" to "ds"), bundleProgress = BundleProgress(1, 15), ).takeIf { case != TripleCase.THIRD }, - listOf( + otherCrossSells = listOf( CrossSell( "id", "title", @@ -853,8 +957,8 @@ private fun PreviewCrossSellsFloatingSheetContent( ImageAsset("", "", ""), ), ).takeIf { case != TripleCase.FIRST }.orEmpty(), - {}, - {}, + dismissSheet = {}, + onCrossSellClick = {}, imageLoader = rememberPreviewImageLoader(), ) } @@ -929,3 +1033,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 = {}, + ) + } + } +}