diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 4bfa42c651..316c4105eb 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -3,7 +3,7 @@ on: push: branches: - develop - - feature/cio-backed-deep-links + - feat/pet-chip-id workflow_dispatch: concurrency: diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 940f9a76b2..59f19d4423 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -185,6 +185,7 @@ dependencies { implementation(projects.designSystemInternals) implementation(projects.featureAddonPurchase) implementation(projects.featureChat) + implementation(projects.featureChipId) implementation(projects.featureChooseTier) implementation(projects.featureClaimChat) implementation(projects.featureClaimDetails) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt b/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt index 24d0169d18..b4d78717c5 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt @@ -65,6 +65,7 @@ import com.hedvig.android.design.system.hedvig.pdfrenderer.PdfDecoder import com.hedvig.android.feature.addon.purchase.di.addonPurchaseModule import com.hedvig.android.feature.change.tier.di.chooseTierModule import com.hedvig.android.feature.chat.di.chatModule +import com.hedvig.android.feature.chip.id.di.chipIdModule import com.hedvig.android.feature.claim.details.di.claimDetailsModule import com.hedvig.android.feature.claimhistory.di.claimHistoryModule import com.hedvig.android.feature.connect.payment.trustly.di.connectPaymentTrustlyModule @@ -296,6 +297,7 @@ val applicationModule = module { authModule, buildConstantsModule, chatModule, + chipIdModule, chooseTierModule, claimChatModule, claimDetailsModule, diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt index fc154333da..43fdab75f4 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt @@ -28,6 +28,8 @@ import com.hedvig.android.feature.change.tier.navigation.changeTierGraph import com.hedvig.android.feature.chat.navigation.ChatDestination import com.hedvig.android.feature.chat.navigation.ChatDestinations import com.hedvig.android.feature.chat.navigation.cbmChatGraph +import com.hedvig.android.feature.chip.id.navigation.ChipIdGraphDestination +import com.hedvig.android.feature.chip.id.navigation.chipIdGraph import com.hedvig.android.feature.claim.details.navigation.ClaimDetailDestination import com.hedvig.android.feature.claim.details.navigation.claimDetailsGraph import com.hedvig.android.feature.claimhistory.nav.ClaimHistoryDestination @@ -212,6 +214,9 @@ internal fun HedvigNavHost( navController.navigate(ProfileDestination.ContactInfo) }, imageLoader = imageLoader, + navigateToChipIdScreen = { + navController.navigate(ChipIdGraphDestination()) + }, ) insuranceGraph( nestedGraphs = { @@ -321,6 +326,9 @@ internal fun HedvigNavHost( ), ) }, + navigateToChipIdScreen = { contractId -> + navController.navigate(ChipIdGraphDestination(contractId)) + }, ) foreverGraph( hedvigDeepLinkContainer = hedvigDeepLinkContainer, @@ -373,6 +381,9 @@ internal fun HedvigNavHost( navController.navigate(InsuranceEvidenceGraphDestination) }, openUrl = openUrl, + navigateToChipId = { + navController.navigate(ChipIdGraphDestination()) + }, ) cbmChatGraph( hedvigDeepLinkContainer = hedvigDeepLinkContainer, @@ -404,6 +415,18 @@ internal fun HedvigNavHost( hedvigDeepLinkContainer = hedvigDeepLinkContainer, onNavigateToNewConversation = ::navigateToNewConversation, ) + chipIdGraph( + navController = navController, + globalSnackBarState = globalSnackBarState, + navigateUp = navController::navigateUp, + hedvigDeepLinkContainer = hedvigDeepLinkContainer, + popBackStackOrFinish = popBackStackOrFinish, + goHome = { + navController.navigate(HomeDestination.Graph) { + popUpTo(ChipIdGraphDestination::class) { inclusive = true } + } + } + ) movingFlowGraph( navController = navController, goToChat = ::navigateToNewConversation, diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 2059bd023b..f58b1218a2 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -168,11 +168,12 @@ Meddelanden Skicka Checkout - Chip-ID - Lägg till Chip-ID - Chip-ID för ditt husdjur saknas - Uppdatera husdjurets chip-ID - Måste vara 15 siffror + ID-nummer + Lägg till ID + Vi saknar ditt djurs ID-nummer + Du har redan lagt till ID-nummer för samtliga djur + Uppdatera ID-nummer + ID-numret måste vara exakt 15 siffror Aktivera pushnotiser så vi kan hålla dig uppdaterad om ditt ärende. Aktivera Ärende @@ -215,7 +216,7 @@ Beskriv i text Din skadeanmälan Röstinspelning - Anpassat objekt + Annat föremål Om du inte vet exakt datum, fyll i på ett ungefär när det inträffade. Ändra svar Ändrar du det här svaret rensas allt du fyllt i efter det. Du behöver gå igenom de stegen igen. @@ -849,7 +850,6 @@ Välj slutdatum Vi skickar en bekräftelse på uppsägningen inom några dagar. Hör gärna av dig om du inte fått den efter 5 dagar. Om du har ställt av din bil säger vi upp din försäkring automatiskt. Vi får informationen direkt från Transportstyrelsen. - Eftersom din bil har avställts kommer din försäkring att sägas upp automatiskt. När du har ställt på bilen hos Transportstyrelsen uppdateras din försäkring automatiskt till ditt tidigare skydd. Om du har skrotat din bil säger vi upp din försäkring automatiskt. Vi får informationen direkt från Transportstyrelsen. Eftersom din bil har skrotats kommer din försäkring att sägas upp automatiskt. @@ -867,7 +867,6 @@ Du kan närsomhelst avsluta din försäkring. Avslutar du din försäkring kommer du endast att debiteras för den tid du varit aktiv medlem hos Hedvig. När du avslutat din försäkring kan du se din sista betalning under fliken \"Betalningar\" här i appen. Avsluta din försäkring Avsluta försäkring - Eftersom din bil är påställd igen kommer din försäkring automatiskt att ändras tillbaka till ditt ordinarie skydd. Din bil är tillbaka på vägen Observera att du bara kan säga upp en försäkring åt gången Välj den försäkring du vill säga upp @@ -882,6 +881,7 @@ Om det blir ett uppehåll i din försäkring, kan det innebära att du inte får den hjälp eller ersättning du behöver i framtiden. Se därför till att alltid ha en aktiv försäkring. Viktig information Jag förstår + Läs mer Nästa betalning Skicka ett meddelande här i appen så hjälper vi dig. Skicka meddelande diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 8a6e8671fc..f2db8a113b 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -168,11 +168,12 @@ Messages Send Checkout - Chip-ID - Add Chip-ID - Chip ID for your pet is missing - Update pet Chip-ID - Must be 15 digits + ID-number + Add ID + We are missing your pet’s ID-number + You’ve already added the ID-number for all your pets + Update ID-number + ID-number needs to be exactly 15 digits Don’t miss out on important information for your claim Enable notifications Case @@ -849,7 +850,6 @@ Select termination date We’ll send a cancellation confirmation within a few days. If you don’t get it after 5 days, feel free to contact us. If you’ve decommissioned your car, we’ll cancel your insurance automatically. We get this information directly from Transportstyrelsen. - Since your car has been decommissioned, your insurance will be automatically cancelled. Once your car is registered as active with Transportstyrelsen, your insurance will be updated automatically to your previous coverage. If you’ve scrapped your car, we’ll cancel your insurance automatically. We get this information directly from Transportstyrelsen. Since your car has been scrapped, your insurance will be automatically cancelled. @@ -867,7 +867,6 @@ You can cancel your insurance at any time. You will only be charged for the time you have been an active member. When the insurance is cancelled you can see your last payment under the tab \"Payments\" here in the app. Cancelling your insurance Cancel insurance - Since your car is registered again, your insurance will automatically switch back to your regular coverage. Your car is back on the road Please note that you can only cancel one insurance at a time Select the insurance you want to cancel @@ -882,6 +881,7 @@ If there is a gap in your insurance, you may not receive future help or compensation. Make sure that you always have an active insurance. Important information I understand + Learn more Next payment You need our help to cancel your insurance. Send us a message to get further help. Send message diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index d94454c4f6..35d531ddea 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -168,11 +168,12 @@ Meddelanden Skicka Checkout - Chip-ID - Lägg till Chip-ID - Chip-ID för ditt husdjur saknas - Uppdatera husdjurets chip-ID - Måste vara 15 siffror + ID-nummer + Lägg till ID + Vi saknar ditt djurs ID-nummer + Du har redan lagt till ID-nummer för samtliga djur + Uppdatera ID-nummer + ID-numret måste vara exakt 15 siffror Aktivera pushnotiser så vi kan hålla dig uppdaterad om ditt ärende. Aktivera Ärende @@ -215,7 +216,7 @@ Beskriv i text Din skadeanmälan Röstinspelning - Anpassat objekt + Annat föremål Om du inte vet exakt datum, fyll i på ett ungefär när det inträffade. Ändra svar Ändrar du det här svaret rensas allt du fyllt i efter det. Du behöver gå igenom de stegen igen. @@ -849,7 +850,6 @@ Välj slutdatum Vi skickar en bekräftelse på uppsägningen inom några dagar. Hör gärna av dig om du inte fått den efter 5 dagar. Om du har ställt av din bil säger vi upp din försäkring automatiskt. Vi får informationen direkt från Transportstyrelsen. - Eftersom din bil har avställts kommer din försäkring att sägas upp automatiskt. När du har ställt på bilen hos Transportstyrelsen uppdateras din försäkring automatiskt till ditt tidigare skydd. Om du har skrotat din bil säger vi upp din försäkring automatiskt. Vi får informationen direkt från Transportstyrelsen. Eftersom din bil har skrotats kommer din försäkring att sägas upp automatiskt. @@ -867,7 +867,6 @@ Du kan närsomhelst avsluta din försäkring. Avslutar du din försäkring kommer du endast att debiteras för den tid du varit aktiv medlem hos Hedvig. När du avslutat din försäkring kan du se din sista betalning under fliken "Betalningar" här i appen. Avsluta din försäkring Avsluta försäkring - Eftersom din bil är påställd igen kommer din försäkring automatiskt att ändras tillbaka till ditt ordinarie skydd. Din bil är tillbaka på vägen Observera att du bara kan säga upp en försäkring åt gången Välj den försäkring du vill säga upp @@ -882,6 +881,7 @@ Om det blir ett uppehåll i din försäkring, kan det innebära att du inte får den hjälp eller ersättning du behöver i framtiden. Se därför till att alltid ha en aktiv försäkring. Viktig information Jag förstår + Läs mer Nästa betalning Skicka ett meddelande här i appen så hjälper vi dig. Skicka meddelande diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 3ecfc1f504..7edb7fd368 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -168,11 +168,12 @@ Messages Send Checkout - Chip-ID - Add Chip-ID - Chip ID for your pet is missing - Update pet Chip-ID - Must be 15 digits + ID-number + Add ID + We are missing your pet’s ID-number + You’ve already added the ID-number for all your pets + Update ID-number + ID-number needs to be exactly 15 digits Don’t miss out on important information for your claim Enable notifications Case @@ -849,7 +850,6 @@ Select termination date We’ll send a cancellation confirmation within a few days. If you don’t get it after 5 days, feel free to contact us. If you’ve decommissioned your car, we’ll cancel your insurance automatically. We get this information directly from Transportstyrelsen. - Since your car has been decommissioned, your insurance will be automatically cancelled. Once your car is registered as active with Transportstyrelsen, your insurance will be updated automatically to your previous coverage. If you’ve scrapped your car, we’ll cancel your insurance automatically. We get this information directly from Transportstyrelsen. Since your car has been scrapped, your insurance will be automatically cancelled. @@ -867,7 +867,6 @@ You can cancel your insurance at any time. You will only be charged for the time you have been an active member. When the insurance is cancelled you can see your last payment under the tab "Payments" here in the app. Cancelling your insurance Cancel insurance - Since your car is registered again, your insurance will automatically switch back to your regular coverage. Your car is back on the road Please note that you can only cancel one insurance at a time Select the insurance you want to cancel @@ -882,6 +881,7 @@ If there is a gap in your insurance, you may not receive future help or compensation. Make sure that you always have an active insurance. Important information I understand + Learn more Next payment You need our help to cancel your insurance. Send us a message to get further help. Send message diff --git a/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ChipIdState.kt b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ChipIdState.kt new file mode 100644 index 0000000000..15a77b32e3 --- /dev/null +++ b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ChipIdState.kt @@ -0,0 +1,7 @@ +package com.hedvig.android.data.contract + +sealed interface ChipIdState { + data object Missing : ChipIdState + + data object NotRequired : ChipIdState +} diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt index 7d864ff86d..3366acd6cb 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt @@ -84,10 +84,11 @@ fun HedvigNotificationCard( withIcon: Boolean = NotificationDefaults.withIconDefault, style: InfoCardStyle = defaultStyle, buttonLoading: Boolean = false, + minLines: Int = 1 ) { HedvigNotificationCard( content = { - HedvigText(text = message) + HedvigText(text = message, minLines = minLines) }, priority = priority, modifier = modifier, diff --git a/app/feature/feature-chip-id/build.gradle.kts b/app/feature/feature-chip-id/build.gradle.kts new file mode 100644 index 0000000000..6cf9c98e32 --- /dev/null +++ b/app/feature/feature-chip-id/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("hedvig.android.library") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") + serialization() + compose() +} + +dependencies { + api(libs.androidx.navigation.common) + + implementation(libs.androidx.navigation.compose) + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.coroutines.core) + implementation(libs.jetbrains.lifecycle.runtime.compose) + implementation(libs.koin.composeViewModel) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.composeUi) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.coreUiData) + implementation(projects.dataContract) + implementation(projects.designSystemHedvig) + implementation(projects.moleculePublic) + implementation(projects.navigationCore) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + implementation(projects.navigationComposeTyped) +} diff --git a/app/feature/feature-chip-id/src/main/AndroidManifest.xml b/app/feature/feature-chip-id/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..568741e54f --- /dev/null +++ b/app/feature/feature-chip-id/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/feature/feature-chip-id/src/main/graphql/GetPetContractsForChipIdQuery.graphql b/app/feature/feature-chip-id/src/main/graphql/GetPetContractsForChipIdQuery.graphql new file mode 100644 index 0000000000..b5a11ac9e6 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/graphql/GetPetContractsForChipIdQuery.graphql @@ -0,0 +1,15 @@ +query GetPetContractsForChipId { + currentMember { + activeContracts { + id + exposureDisplayNameShort + isMissingPetId + currentAgreement { + productVariant { + typeOfContract + displayName + } + } + } + } +} diff --git a/app/feature/feature-chip-id/src/main/graphql/UpdateChipIdMutation.graphql b/app/feature/feature-chip-id/src/main/graphql/UpdateChipIdMutation.graphql new file mode 100644 index 0000000000..c6fc4fa901 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/graphql/UpdateChipIdMutation.graphql @@ -0,0 +1,12 @@ +mutation UpdateChipIdNumber($petId: String!, $contractId: ID!) { + midtermChangePetId(petId: $petId, contractId: $contractId) { + ...on MidtermChangePetIdOutput { + ... on MidtermChangePetIdActivationDate { + activationDate + } + ... on UserError { + message + } + } + } +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/GetContractsWithMissingChipIdUseCase.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/GetContractsWithMissingChipIdUseCase.kt new file mode 100644 index 0000000000..d92e35de04 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/GetContractsWithMissingChipIdUseCase.kt @@ -0,0 +1,43 @@ +package com.hedvig.android.feature.chip.id.data + +import arrow.core.Either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.ApolloOperationError +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.data.contract.toContractGroup +import com.hedvig.android.logger.logcat +import octopus.GetPetContractsForChipIdQuery + +internal interface GetContractsWithMissingChipIdUseCase { + suspend fun invoke(): Either> +} + +internal class GetContractsWithMissingChipIdUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetContractsWithMissingChipIdUseCase { + override suspend fun invoke(): Either> { + return apolloClient + .query(GetPetContractsForChipIdQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .map { data -> + data.currentMember.activeContracts + .mapNotNull { contract -> + if (contract.isMissingPetId) { + PetContractForChipId( + id = contract.id, + displayName = contract.currentAgreement.productVariant.displayName, + contractExposure = contract.exposureDisplayNameShort, + contractGroup = contract.currentAgreement.productVariant.typeOfContract.toContractGroup(), + ) + } else { + null + } + } + }.onLeft { error -> + logcat(operationError = error) { "GetPetContractsForChipIdUseCase failed with $error" } + } + } +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt new file mode 100644 index 0000000000..06d83561a6 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt @@ -0,0 +1,10 @@ +package com.hedvig.android.feature.chip.id.data + +import com.hedvig.android.data.contract.ContractGroup + +data class PetContractForChipId( + val id: String, + val displayName: String, + val contractExposure: String, + val contractGroup: ContractGroup +) diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/UpdateChipIdUseCase.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/UpdateChipIdUseCase.kt new file mode 100644 index 0000000000..5fd2dbbe1e --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/UpdateChipIdUseCase.kt @@ -0,0 +1,51 @@ +package com.hedvig.android.feature.chip.id.data + +import arrow.core.Either +import arrow.core.raise.context.bind +import arrow.core.raise.context.either +import arrow.core.raise.context.raise +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import octopus.UpdateChipIdNumberMutation +import octopus.UpdateChipIdNumberMutation.Data.MidtermChangePetId.Companion.asMidtermChangePetIdActivationDate +import octopus.UpdateChipIdNumberMutation.Data.MidtermChangePetId.Companion.asUserError + +internal interface UpdateChipIdUseCase { + suspend fun invoke(petId: String, insuranceId: String): Either +} + +internal class UpdateChipIdUseCaseImpl( + private val apolloClient: ApolloClient, +) : UpdateChipIdUseCase { + override suspend fun invoke(petId: String, insuranceId: String): Either { + return either { + logcat { "UpdateChipIdNumberMutation start" } + val result = apolloClient.mutation( + UpdateChipIdNumberMutation( + petId = petId, + contractId = insuranceId, + ), + ) + .safeExecute { + logcat { "UpdateChipIdNumberMutation error: $it" } + ErrorMessage() + } + .bind() + + val userError = result.midtermChangePetId.asUserError() + if (userError != null) { + raise(ErrorMessage(userError.message)) + } + val date = result.midtermChangePetId.asMidtermChangePetIdActivationDate()?.activationDate + if (date != null) { + logcat { "UpdateChipIdNumberMutation success with activationDate: $date" } + Unit + } else { + raise(ErrorMessage()) + } + } + } +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/di/ChipIdModule.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/di/ChipIdModule.kt new file mode 100644 index 0000000000..0e05ad40f4 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/di/ChipIdModule.kt @@ -0,0 +1,40 @@ +package com.hedvig.android.feature.chip.id.di + +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.feature.chip.id.data.GetContractsWithMissingChipIdUseCase +import com.hedvig.android.feature.chip.id.data.GetContractsWithMissingChipIdUseCaseImpl +import com.hedvig.android.feature.chip.id.data.UpdateChipIdUseCase +import com.hedvig.android.feature.chip.id.data.UpdateChipIdUseCaseImpl +import com.hedvig.android.feature.chip.id.ui.AddChipIdViewModel +import com.hedvig.android.feature.chip.id.ui.selectinsurance.SelectInsuranceForChipIdViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val chipIdModule = module { + single { + UpdateChipIdUseCaseImpl( + apolloClient = get(), + ) + } + + single { + GetContractsWithMissingChipIdUseCaseImpl( + apolloClient = get(), + ) + } + + viewModel { params -> + SelectInsuranceForChipIdViewModel( + preselectedContractId = params.getOrNull(), + getContractsWithMissingChipIdUseCase = get(), + ) + } + + viewModel { params -> + AddChipIdViewModel( + updateChipIdUseCase = get(), + contractId = params.get(), + getContractsWithMissingChipIdUseCase = get(), + ) + } +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt new file mode 100644 index 0000000000..d1b22df1dd --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt @@ -0,0 +1,97 @@ +package com.hedvig.android.feature.chip.id.navigation + +import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import com.hedvig.android.design.system.hedvig.GlobalSnackBarState +import com.hedvig.android.feature.chip.id.ui.AddChipIdDestination +import com.hedvig.android.feature.chip.id.ui.AddChipIdViewModel +import com.hedvig.android.feature.chip.id.ui.selectinsurance.SelectInsuranceForChipIdDestination +import com.hedvig.android.feature.chip.id.ui.selectinsurance.SelectInsuranceForChipIdViewModel +import com.hedvig.android.navigation.compose.navDeepLinks +import com.hedvig.android.navigation.compose.navdestination +import com.hedvig.android.navigation.compose.navgraph +import com.hedvig.android.navigation.compose.typed.getRouteFromBackStack +import com.hedvig.android.navigation.compose.typedPopUpTo +import com.hedvig.android.navigation.core.HedvigDeepLinkContainer +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf + +fun NavGraphBuilder.chipIdGraph( + navController: NavController, + globalSnackBarState: GlobalSnackBarState, + navigateUp: () -> Unit, + hedvigDeepLinkContainer: HedvigDeepLinkContainer, + popBackStackOrFinish: () -> Unit, + goHome: () -> Unit +) { + navdestination( + deepLinks = navDeepLinks( + hedvigDeepLinkContainer.petIdWithoutContractId, + hedvigDeepLinkContainer.petIdWithContractId, + ), + ) { + val contractId = this.contractId + LaunchedEffect(Unit) { + navController.navigate( + ChipIdGraphDestination( + contractId = contractId, + ), + ) { + typedPopUpTo({ inclusive = true }) + } + } + } + + navgraph( + startDestination = ChipIdDestination.SelectInsuranceForChipId::class, + destinationNavTypeAware = ChipIdGraphDestination, + ) { + navdestination { backStackEntry -> + val chipIdGraphDestination = navController + .getRouteFromBackStack(backStackEntry) + val preselectedContractId = chipIdGraphDestination.contractId + + val viewModel: SelectInsuranceForChipIdViewModel = koinViewModel { + parametersOf(preselectedContractId) + } + SelectInsuranceForChipIdDestination( + viewModel = viewModel, + navigateUp = navigateUp, + popBackStack = popBackStackOrFinish, + navigateToAddChipId = { contractId: String, popSelectInsurance: Boolean -> + navController.navigate(ChipIdDestination.AddChipId(contractId)) { + if (popSelectInsurance) { + typedPopUpTo { + inclusive = true + } + } + } + }, + ) + } + + navdestination { backStackEntry -> + val contractId = this.contractId + val viewModel: AddChipIdViewModel = koinViewModel { + parametersOf(contractId) + } + AddChipIdDestination( + viewModel = viewModel, + globalSnackBarState = globalSnackBarState, + navigateUp = { + if (!navController.popBackStack(ChipIdDestination.AddChipId::class, inclusive = true)) { + goHome() + } + }, + popFlowOnSuccess = { + if (!navController.popBackStack(ChipIdGraphDestination::class, inclusive = true)) { + goHome() + } + } + ) + } + } +} + + diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt new file mode 100644 index 0000000000..891de2fad8 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt @@ -0,0 +1,35 @@ +package com.hedvig.android.feature.chip.id.navigation + +import com.hedvig.android.navigation.common.Destination +import com.hedvig.android.navigation.common.DestinationNavTypeAware +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ChipIdGraphDestination(val contractId: String? = null) : Destination { + companion object : DestinationNavTypeAware { + override val typeList: List = emptyList() + } +} + +internal sealed interface ChipIdDestination { + @androidx.annotation.Keep + @Serializable + data class AddChipId( + val contractId: String + ) : ChipIdDestination, Destination + + @androidx.annotation.Keep + @Serializable + data object SelectInsuranceForChipId : ChipIdDestination, Destination + @androidx.annotation.Keep + @Serializable + data class AddChipIdTriage( + /** Must match the name of the param inside [com.hedvig.android.navigation.core.HedvigDeepLinkContainer.petIdWithContractId] */ + @SerialName("contractId") + val contractId: String? = null + ) : ChipIdDestination, Destination +} + diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt new file mode 100644 index 0000000000..a23ddeb4c5 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt @@ -0,0 +1,407 @@ +package com.hedvig.android.feature.chip.id.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.data.contract.pillowResource +import com.hedvig.android.design.system.hedvig.GlobalSnackBarState +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgressDebounced +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.clearFocusOnTap +import com.hedvig.android.feature.chip.id.data.PetContractForChipId +import com.hedvig.android.feature.chip.id.ui.AddChipIdEvent.RetryLoadData +import com.hedvig.android.feature.chip.id.ui.AddChipIdEvent.SubmitData +import com.hedvig.android.feature.chip.id.ui.AddChipIdUiState.Content +import hedvig.resources.CHIP_ID_LABEL +import hedvig.resources.CHIP_ID_TOP_TITLE +import hedvig.resources.CHIP_ID_WRONG_INPUT +import hedvig.resources.CONTACT_INFO_CHANGES_SAVED +import hedvig.resources.Res +import hedvig.resources.general_save_button +import hedvig.resources.something_went_wrong +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AddChipIdDestination( + viewModel: AddChipIdViewModel, + globalSnackBarState: GlobalSnackBarState, + navigateUp: () -> Unit, + popFlowOnSuccess: () -> Unit, +) { + val uiState: AddChipIdUiState by viewModel.uiState.collectAsStateWithLifecycle() + AddChipIdScreen( + uiState = uiState, + globalSnackBarState = globalSnackBarState, + submitChipId = { + viewModel.emit(SubmitData) + }, + reload = { + viewModel.emit(RetryLoadData) + }, + navigateUp = navigateUp, + showedSnackBar = { + viewModel.emit(AddChipIdEvent.ShowedMessage) + popFlowOnSuccess() + }, + updateText = { + viewModel.emit(AddChipIdEvent.UpdateText(it)) + }, + ) +} + +@Composable +private fun AddChipIdScreen( + uiState: AddChipIdUiState, + globalSnackBarState: GlobalSnackBarState, + submitChipId: () -> Unit, + reload: () -> Unit, + navigateUp: () -> Unit, + showedSnackBar: () -> Unit, + updateText: (String) -> Unit, +) { + val focusManager = LocalFocusManager.current + HedvigScaffold( + topAppBarText = stringResource(Res.string.CHIP_ID_TOP_TITLE), + navigateUp = navigateUp, + modifier = Modifier + .fillMaxSize() + .clearFocusOnTap(), + ) { + when (uiState) { + AddChipIdUiState.Loading -> { + HedvigFullScreenCenterAlignedProgressDebounced( + Modifier + .weight(1f) + .wrapContentHeight(), + ) + } + + AddChipIdUiState.Error -> { + HedvigErrorSection( + onButtonClick = reload, + modifier = Modifier + .weight(1f) + .wrapContentHeight(), + ) + } + + is Content -> { + AddChipIdContent( + uiState = uiState, + globalSnackBarState = globalSnackBarState, + submitChipId = submitChipId, + focusManager = focusManager, + showedSnackBar = showedSnackBar, + updateText = updateText, + ) + } + } + } +} + +@Composable +private fun ColumnScope.AddChipIdContent( + uiState: Content, + globalSnackBarState: GlobalSnackBarState, + submitChipId: () -> Unit, + focusManager: FocusManager, + showedSnackBar: () -> Unit, + updateText: (String) -> Unit, +) { + val successMessage = stringResource(Res.string.CONTACT_INFO_CHANGES_SAVED) + LaunchedEffect(uiState.showSuccessSnackBar) { + if (!uiState.showSuccessSnackBar) return@LaunchedEffect + globalSnackBarState.show(successMessage, NotificationPriority.Campaign) + showedSnackBar() + } + + Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(16.dp)) + + InsuranceInfoCard( + uiState.contract, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + ChipIdTextField( + text = uiState.chipIdText, + labelText = stringResource(Res.string.CHIP_ID_LABEL), + updateText = updateText, + + ) + + AnimatedContent( + targetState = uiState.errorType, + transitionSpec = { fadeIn() + expandVertically() togetherWith fadeOut() + shrinkVertically() }, + modifier = Modifier.padding(top = 4.dp), + ) { errorType -> + if (errorType != null) { + val errorMessage = when (errorType) { + ChipIdErrorType.WrongInput -> stringResource(Res.string.CHIP_ID_WRONG_INPUT) + ChipIdErrorType.GeneralError -> stringResource(Res.string.something_went_wrong) + is ChipIdErrorType.ErrorWithMessage -> errorType.message + } + HedvigNotificationCard( + message = errorMessage, + priority = NotificationPriority.Error, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } + } + + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = stringResource(Res.string.general_save_button), + enabled = !uiState.submittingData, + onClick = { + focusManager.clearFocus() + submitChipId() + }, + isLoading = uiState.submittingData, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize(), + ) + Spacer(Modifier.height(16.dp)) +} + +@Composable +private fun ChipIdTextField( + text: String, + labelText: String, + updateText: (String) -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + var input by remember { mutableStateOf(text) } + val mask = "000-000-000-000-000" + val maskColor = HedvigTheme.colorScheme.textTertiary + val visualTransformation = ChipIdVisualTransformation(mask, maskColor) + HedvigTextField( + text = input, + labelText = labelText, + errorState = HedvigTextFieldDefaults.ErrorState.NoError, + onValueChange = { + if (it.length <= 15) { + updateText(it) + input = it + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + visualTransformation = visualTransformation, + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Medium, + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) +} + +private class ChipIdVisualTransformation( + private val mask: String, + private val maskColor: Color, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val trimmed = if (text.text.length >= 15) text.text.substring(0..14) else text.text + + val annotatedString = buildAnnotatedString { + for (i in trimmed.indices) { + append(trimmed[i]) + if (i in listOf(2, 5, 8, 11)) { + append("-") + } + } + withStyle(SpanStyle(color = maskColor)) { + append(mask.takeLast(mask.length - length)) + } + } + + val personalNumberOffsetTranslator = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return when { + offset <= 2 -> offset + offset <= 5 -> offset + 1 + offset <= 8 -> offset + 2 + offset <= 11 -> offset + 3 + offset <= 15 -> offset + 4 + else -> 19 + } + } + + override fun transformedToOriginal(offset: Int): Int { + return when { + offset <= 3 -> offset + offset <= 7 -> offset - 1 + offset <= 11 -> offset - 2 + offset <= 15 -> offset - 3 + else -> offset - 4 + }.coerceAtMost(text.length) + } + } + return TransformedText(annotatedString, personalNumberOffsetTranslator) + } +} + + +@Composable +private fun InsuranceInfoCard( + insuranceInfo: PetContractForChipId, + modifier: Modifier = Modifier, +) { + HedvigCard( + modifier + .border( + width = 1.dp, + color = HedvigTheme.colorScheme.borderPrimary, + shape = HedvigTheme.shapes.cornerXLarge, + ), + color = HedvigTheme.colorScheme.backgroundPrimary, + ) { + Column(Modifier.padding(16.dp)) { + Row { + Image( + painter = painterResource(insuranceInfo.contractGroup.pillowResource()), + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + HedvigText(insuranceInfo.displayName) + HedvigText(insuranceInfo.contractExposure, color = HedvigTheme.colorScheme.textSecondary) + } + } + } + } +} + +@HedvigPreview +@Composable +private fun PreviewTerminationConfirmationScreen( + @PreviewParameter(AddChipIdScreenStateProvider::class) state: AddChipIdUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + AddChipIdScreen( + state, + globalSnackBarState = GlobalSnackBarState(), + submitChipId = { }, + reload = { }, + navigateUp = { }, + showedSnackBar = {}, + {}, + ) + } + } +} + + +private class AddChipIdScreenStateProvider : CollectionPreviewParameterProvider( + listOf( + AddChipIdUiState.Error, + AddChipIdUiState.Loading, + Content( + chipIdText = "", + contract = PetContractForChipId( + id = "sdf", + displayName = "Display name", + contractExposure = "Kitty", + contractGroup = ContractGroup.CAT, + ), + showSuccessSnackBar = false, + submittingData = false, + ), + Content( + chipIdText = "123456789012345", + contract = PetContractForChipId( + id = "sdf", + displayName = "Display name", + contractExposure = "Kitty", + contractGroup = ContractGroup.CAT, + ), + showSuccessSnackBar = false, + submittingData = false, + ), + Content( + chipIdText = "", + contract = PetContractForChipId( + id = "sdf", + displayName = "Display name", + contractExposure = "Kitty", + contractGroup = ContractGroup.CAT, + ), + showSuccessSnackBar = false, + submittingData = false, + errorType = ChipIdErrorType.WrongInput, + ), + Content( + chipIdText = "", + contract = PetContractForChipId( + id = "sdf", + displayName = "Display name", + contractExposure = "Kitty", + contractGroup = ContractGroup.CAT, + ), + showSuccessSnackBar = false, + submittingData = false, + errorType = ChipIdErrorType.GeneralError, + ), + ), +) diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt new file mode 100644 index 0000000000..c1b7351d85 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt @@ -0,0 +1,171 @@ +package com.hedvig.android.feature.chip.id.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot +import com.hedvig.android.feature.chip.id.data.GetContractsWithMissingChipIdUseCase +import com.hedvig.android.feature.chip.id.data.PetContractForChipId +import com.hedvig.android.feature.chip.id.data.UpdateChipIdUseCase +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class AddChipIdViewModel( + updateChipIdUseCase: UpdateChipIdUseCase, + getContractsWithMissingChipIdUseCase: GetContractsWithMissingChipIdUseCase, + contractId: String, +) : MoleculeViewModel( + initialState = AddChipIdUiState.Loading, + presenter = AddChipIdPresenter( + updateChipIdUseCase = updateChipIdUseCase, + contractId = contractId, + getContractsWithMissingChipIdUseCase = getContractsWithMissingChipIdUseCase, + ), +) + +internal class AddChipIdPresenter( + private val updateChipIdUseCase: UpdateChipIdUseCase, + private val getContractsWithMissingChipIdUseCase: GetContractsWithMissingChipIdUseCase, + private val contractId: String, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: AddChipIdUiState): AddChipIdUiState { + var chipIdText by remember { + val lastChipIdState = lastState.content?.chipIdText + mutableStateOf(lastChipIdState ?: "") + } + var currentState by remember { mutableStateOf(lastState) } + var submittingData by remember { mutableStateOf(false) } + var showSuccessSnackBar by remember { mutableStateOf(false) } + var errorType by remember { mutableStateOf(null) } + + var submitIteration by remember { mutableIntStateOf(0) } + var loadIteration by remember { mutableIntStateOf(0) } + + LaunchedEffect(chipIdText) { + errorType = null + } + + LaunchedEffect(loadIteration) { + getContractsWithMissingChipIdUseCase.invoke().fold( + ifLeft = { + currentState = AddChipIdUiState.Error + }, + ifRight = { + val contract = it.firstOrNull { it.id == contractId } + if (contract == null) { + currentState = AddChipIdUiState.Error + return@LaunchedEffect + } + currentState = AddChipIdUiState.Content( + chipIdText = chipIdText, + contract = contract, + ) + }, + ) + } + + LaunchedEffect(submitIteration) { + if (submitIteration == 0) return@LaunchedEffect + + submittingData = true + errorType = null + + updateChipIdUseCase.invoke(insuranceId = contractId, petId = chipIdText).fold( + ifLeft = { error -> + Snapshot.withMutableSnapshot { + val errorMessage = error.message + submittingData = false + errorType = if (errorMessage==null) ChipIdErrorType.GeneralError + else ChipIdErrorType.ErrorWithMessage(errorMessage) + } + }, + ifRight = { + Snapshot.withMutableSnapshot { + showSuccessSnackBar = true + submittingData = false + } + }, + ) + } + + CollectEvents { event -> + when (event) { + AddChipIdEvent.RetryLoadData -> { + loadIteration++ + } + + AddChipIdEvent.SubmitData -> { + if (!chipIdText.all { it.isDigit() } || chipIdText.length != 15) { + Snapshot.withMutableSnapshot { + errorType = ChipIdErrorType.WrongInput + } + } else { + submitIteration++ + } + } + + AddChipIdEvent.ShowedMessage -> { + Snapshot.withMutableSnapshot { + showSuccessSnackBar = false + errorType = null + } + } + + is AddChipIdEvent.UpdateText -> { + chipIdText = event.newText + } + } + } + + return when (val state = currentState) { + is AddChipIdUiState.Content -> state.copy( + chipIdText = chipIdText, + showSuccessSnackBar = showSuccessSnackBar, + submittingData = submittingData, + errorType = errorType, + ) + AddChipIdUiState.Error, AddChipIdUiState.Loading -> state + } + } +} + +internal sealed interface AddChipIdUiState { + val content: Content? + get() = this as? Content + + data object Loading : AddChipIdUiState + + data object Error : AddChipIdUiState + + data class Content( + val chipIdText: String, + val contract: PetContractForChipId, + val showSuccessSnackBar: Boolean = false, + val submittingData: Boolean = false, + val errorType: ChipIdErrorType? = null, + ) : AddChipIdUiState +} + +internal sealed interface ChipIdErrorType { + data object WrongInput : ChipIdErrorType + + data object GeneralError : ChipIdErrorType + + data class ErrorWithMessage(val message: String) : ChipIdErrorType +} + +internal sealed interface AddChipIdEvent { + data object RetryLoadData : AddChipIdEvent + + data object SubmitData : AddChipIdEvent + + data object ShowedMessage : AddChipIdEvent + + data class UpdateText(val newText: String): AddChipIdEvent +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt new file mode 100644 index 0000000000..9dea55c037 --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt @@ -0,0 +1,224 @@ +package com.hedvig.android.feature.chip.id.ui.selectinsurance + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import com.hedvig.android.compose.ui.dropUnlessResumed +import com.hedvig.android.data.contract.ContractGroup.HOMEOWNER +import com.hedvig.android.data.contract.pillowResource +import com.hedvig.android.design.system.hedvig.EmptyState +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.ERROR +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HorizontalDivider +import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.IconButton +import com.hedvig.android.design.system.hedvig.LocalTextStyle +import com.hedvig.android.design.system.hedvig.ProvideTextStyle +import com.hedvig.android.design.system.hedvig.RadioGroup +import com.hedvig.android.design.system.hedvig.RadioOption +import com.hedvig.android.design.system.hedvig.RadioOptionId +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.a11y.FlowHeading +import com.hedvig.android.design.system.hedvig.icon.Close +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.feature.chip.id.data.PetContractForChipId +import hedvig.resources.ADDON_FLOW_SELECT_INSURANCE_SUBTITLE +import hedvig.resources.ADDON_FLOW_SELECT_INSURANCE_TITLE +import hedvig.resources.CHIP_ID_NO_INSURANCES +import hedvig.resources.Res +import hedvig.resources.SELECT_INSURANCE_TO_REMOVE_ADDON_TITLE +import hedvig.resources.TERMINATION_ADDON_COVERAGE_TITLE +import hedvig.resources.TIER_FLOW_SELECT_INSURANCE_SUBTITLE +import hedvig.resources.general_close_button +import hedvig.resources.general_continue_button +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun SelectInsuranceForChipIdDestination( + viewModel: SelectInsuranceForChipIdViewModel, + navigateUp: () -> Unit, + popBackStack: () -> Unit, + navigateToAddChipId: (contractId: String, popSelectInsurance: Boolean) -> Unit, +) { + val uiState: SelectInsuranceForChipIdState by viewModel.uiState.collectAsStateWithLifecycle() + + SelectInsuranceForChipIdScreen( + uiState = uiState, + navigateUp = navigateUp, + popBackStack = popBackStack, + navigateToAddChipId = { contractId, popSelectInsurance -> + navigateToAddChipId(contractId, popSelectInsurance) + viewModel.emit(SelectInsuranceForChipIdEvent.ClearNavigation) + }, + selectContract = { contract -> + viewModel.emit(SelectInsuranceForChipIdEvent.SelectContract(contract)) + }, + reload = { + viewModel.emit(SelectInsuranceForChipIdEvent.Reload) + }, + ) +} + +@Composable +private fun SelectInsuranceForChipIdScreen( + uiState: SelectInsuranceForChipIdState, + navigateUp: () -> Unit, + popBackStack: () -> Unit, + reload: () -> Unit, + selectContract: (PetContractForChipId) -> Unit, + navigateToAddChipId: (contractId: String, popSelectInsurance: Boolean) -> Unit, +) { + when (uiState) { + SelectInsuranceForChipIdState.Failure -> { + HedvigScaffold( + navigateUp = navigateUp, + ) { + HedvigErrorSection(onButtonClick = reload, modifier = Modifier.padding(16.dp)) + } + } + + SelectInsuranceForChipIdState.Loading -> { + HedvigFullScreenCenterAlignedProgress() + } + + is SelectInsuranceForChipIdState.Success -> { + LaunchedEffect(uiState.contractIdToContinue) { + if (uiState.contractIdToContinue != null) { + navigateToAddChipId( + uiState.contractIdToContinue, + uiState.contracts.size == 1, + ) + } + } + if (uiState.contractIdToContinue == null) { + SelectInsuranceForChipIdContentScreen( + uiState = uiState, + navigateUp = navigateUp, + popBackStack = popBackStack, + selectInsurance = selectContract, + navigateToAddChipId = navigateToAddChipId, + ) + } + } + } +} + +@Composable +private fun SelectInsuranceForChipIdContentScreen( + uiState: SelectInsuranceForChipIdState.Success, + navigateUp: () -> Unit, + popBackStack: () -> Unit, + selectInsurance: (selected: PetContractForChipId) -> Unit, + navigateToAddChipId: (contractId: String, popSelectInsurance: Boolean) -> Unit, +) { + HedvigScaffold( + navigateUp = navigateUp, + topAppBarText = "", + topAppBarActions = { + IconButton( + modifier = Modifier.size(24.dp), + onClick = dropUnlessResumed { popBackStack() }, + content = { + Icon( + imageVector = HedvigIcons.Close, + contentDescription = stringResource(Res.string.general_close_button), + ) + }, + ) + }, + ) { + Spacer(modifier = Modifier.height(8.dp)) + FlowHeading( + stringResource(Res.string.TIER_FLOW_SELECT_INSURANCE_SUBTITLE), + null, + Modifier.padding(horizontal = 16.dp), + ) + if (uiState.contracts.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + ) { + EmptyState( + text = stringResource(Res.string.CHIP_ID_NO_INSURANCES), + description = null, + iconStyle = ERROR, + buttonStyle = EmptyStateDefaults.EmptyStateButtonStyle.NoButton, + ) + } + } else { + Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(16.dp)) + RadioGroup( + options = uiState.contracts.map { insuranceForAddon -> + RadioOption( + id = RadioOptionId(insuranceForAddon.id), + text = insuranceForAddon.displayName, + label = insuranceForAddon.contractExposure, + ) + }, + selectedOption = uiState.selectedContract?.id?.let { RadioOptionId(it) }, + onRadioOptionSelected = { optionId -> + uiState.contracts.firstOrNull { it.id == optionId.id }?.let { + selectInsurance(it) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(12.dp)) + HedvigButton( + stringResource(Res.string.general_continue_button), + enabled = uiState.selectedContract != null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + onClick = { + uiState.selectedContract?.let { + navigateToAddChipId(it.id, uiState.contracts.size == 1) + } + }, + isLoading = false, + ) + Spacer(Modifier.height(16.dp)) + } + + } +} diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt new file mode 100644 index 0000000000..0877a05ffc --- /dev/null +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt @@ -0,0 +1,118 @@ +package com.hedvig.android.feature.chip.id.ui.selectinsurance + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.chip.id.data.GetContractsWithMissingChipIdUseCase +import com.hedvig.android.feature.chip.id.data.PetContractForChipId +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class SelectInsuranceForChipIdViewModel( + preselectedContractId: String?, + getContractsWithMissingChipIdUseCase: GetContractsWithMissingChipIdUseCase, +) : MoleculeViewModel( + initialState = SelectInsuranceForChipIdState.Loading, + presenter = SelectInsuranceForChipIdPresenter(preselectedContractId, getContractsWithMissingChipIdUseCase), + ) + +internal class SelectInsuranceForChipIdPresenter( + private val preselectedContractId: String?, + private val getContractsWithMissingChipIdUseCase: GetContractsWithMissingChipIdUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: SelectInsuranceForChipIdState, + ): SelectInsuranceForChipIdState { + var currentState by remember { mutableStateOf(lastState) } + var loadIteration by remember { mutableIntStateOf(0) } + + var selectedContract: PetContractForChipId? by remember { mutableStateOf( + if (lastState is SelectInsuranceForChipIdState.Success) lastState.selectedContract else + null) } + var contractIdToContinue: String? by remember { mutableStateOf(null) } + + LaunchedEffect(loadIteration) { + currentState = SelectInsuranceForChipIdState.Loading + val result = getContractsWithMissingChipIdUseCase.invoke() + currentState = result.fold( + ifLeft = { SelectInsuranceForChipIdState.Failure }, + ifRight = { contracts -> + val preselected = contracts.firstOrNull { it.id == preselectedContractId } + + if (contracts.size == 1) { + contractIdToContinue = contracts[0].id + } + + SelectInsuranceForChipIdState.Success( + contracts = contracts, + selectedContract = preselected, + contractIdToContinue = contractIdToContinue, + ) + }, + ) + } + + CollectEvents { event -> + when (event) { + SelectInsuranceForChipIdEvent.Reload -> { + loadIteration++ + } + + is SelectInsuranceForChipIdEvent.SelectContract -> { + selectedContract = event.contract + } + + SelectInsuranceForChipIdEvent.SubmitSelected -> { + selectedContract?.let { selected -> + contractIdToContinue = selected.id + } + } + + SelectInsuranceForChipIdEvent.ClearNavigation -> { + contractIdToContinue = null + } + } + } + + return when (val state = currentState) { + is SelectInsuranceForChipIdState.Success -> { + state.copy( + selectedContract = selectedContract ?: state.selectedContract, + contractIdToContinue = contractIdToContinue, + ) + } + + else -> { + state + } + } + } +} + +internal sealed interface SelectInsuranceForChipIdState { + data object Loading : SelectInsuranceForChipIdState + + data class Success( + val contracts: List, + val selectedContract: PetContractForChipId?, + val contractIdToContinue: String? = null, + ) : SelectInsuranceForChipIdState + + data object Failure : SelectInsuranceForChipIdState +} + +internal sealed interface SelectInsuranceForChipIdEvent { + data object Reload : SelectInsuranceForChipIdEvent + + data class SelectContract(val contract: PetContractForChipId) : SelectInsuranceForChipIdEvent + + data object SubmitSelected : SelectInsuranceForChipIdEvent + + data object ClearNavigation : SelectInsuranceForChipIdEvent +} diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index 967925ad07..448965eb09 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -8,11 +8,21 @@ query Home($claimsHistoryFlag: Boolean!) { } terminatedContracts { id + currentAgreement { + productVariant { + typeOfContract + } + } } pendingContracts { id externalInsuranceCancellationHandledByHedvig exposureDisplayName + + productVariant { + typeOfContract + } + } importantMessages { id @@ -57,7 +67,13 @@ query Home($claimsHistoryFlag: Boolean!) { } } activeContracts { + id masterInceptionDate + currentAgreement { + productVariant { + typeOfContract + } + } } } } 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 512923e69c..182b2ff26a 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 @@ -17,8 +17,10 @@ import com.hedvig.android.crosssells.RecommendedCrossSell import com.hedvig.android.data.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.AddonBannerSource import com.hedvig.android.data.addons.data.GetTravelAddonBannerInfoUseCaseProvider +import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset +import com.hedvig.android.data.contract.toContractGroup import com.hedvig.android.data.conversations.HasAnyActiveConversationUseCase import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature @@ -175,7 +177,7 @@ internal class GetHomeDataUseCaseImpl( showHelpCenter = isHelpCenterEnabled, firstVetSections = firstVetActions, crossSells = crossSells, - travelBannerInfo = travelBannerInfo?.firstOrNull(), // todo: check for CAR_ADDON LATER! + travelBannerInfo = travelBannerInfo?.firstOrNull(), ) }.onLeft { error: ApolloOperationError -> logcat(operationError = error) { "GetHomeDataUseCase failed with $error" } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt index d32c08b351..072b01417f 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt @@ -29,6 +29,7 @@ fun NavGraphBuilder.homeGraph( navigateToHelpCenter: () -> Unit, navigateToClaimChat: () -> Unit, navigateToClaimChatInDevMode: () -> Unit, + navigateToChipIdScreen: () -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, @@ -65,6 +66,7 @@ fun NavGraphBuilder.homeGraph( navigateToContactInfo() }, imageLoader = imageLoader, + navigateToChipId = navigateToChipIdScreen, ) } navdestination( 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 6e25e3f958..71a9ae2965 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 @@ -165,6 +165,7 @@ internal fun HomeDestination( navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToFirstVet: (List) -> Unit, navigateToContactInfo: () -> Unit, + navigateToChipId: () -> Unit, imageLoader: ImageLoader, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -188,6 +189,7 @@ internal fun HomeDestination( navigateToFirstVet = navigateToFirstVet, markCrossSellsNotificationAsSeen = { viewModel.emit(HomeEvent.MarkCardCrossSellsAsSeen) }, navigateToContactInfo = navigateToContactInfo, + navigateToChipIdScreen = navigateToChipId, setEpochDayWhenLastToolTipShown = { epochDay -> viewModel.emit(HomeEvent.CrossSellToolTipShown(epochDay)) }, @@ -214,6 +216,7 @@ private fun HomeScreen( navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToFirstVet: (List) -> Unit, navigateToContactInfo: () -> Unit, + navigateToChipIdScreen: () -> Unit, markCrossSellsNotificationAsSeen: () -> Unit, setEpochDayWhenLastToolTipShown: (Long) -> Unit, imageLoader: ImageLoader, @@ -281,6 +284,7 @@ private fun HomeScreen( onNavigateToNewConversation = onNavigateToNewConversation, markMessageAsSeen = markMessageAsSeen, navigateToContactInfo = navigateToContactInfo, + navigateToChipIdScreen = navigateToChipIdScreen, ) } } @@ -427,6 +431,7 @@ private fun HomeScreenSuccess( navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, onNavigateToNewConversation: () -> Unit, navigateToContactInfo: () -> Unit, + navigateToChipIdScreen: () -> Unit, modifier: Modifier = Modifier, ) { val isInPreview = LocalInspectionMode.current @@ -505,6 +510,7 @@ private fun HomeScreenSuccess( openUrl = openUrl, contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets, navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipIdScreen, ) } }, @@ -804,6 +810,7 @@ private fun PreviewHomeScreen( navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, + navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), navigateToClaimChatInDevMode = {}, @@ -835,6 +842,7 @@ private fun PreviewHomeScreenWithError() { navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, + navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), navigateToClaimChatInDevMode = {}, @@ -887,6 +895,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, + navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), navigateToClaimChatInDevMode = {}, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 1db13ed68f..153d9fc1f7 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -250,7 +250,9 @@ private data class SuccessData( }, claimStatusCardsData = homeData.claimStatusCardsData, veryImportantMessages = homeData.veryImportantMessages, - memberReminders = homeData.memberReminders.copy(enableNotifications = null), + memberReminders = homeData.memberReminders.copy( + enableNotifications = null, + ), showHelpCenter = homeData.showHelpCenter, chatAction = chatAction, firstVetAction = firstVetAction, diff --git a/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql b/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql index 65ae234483..0c78b2ce17 100644 --- a/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql +++ b/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql @@ -82,6 +82,7 @@ fragment ContractFragment on Contract { displayName description } + isMissingPetId } fragment AgreementFragment on Agreement { diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt index 478ed902a8..132e1d3a08 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCase.kt @@ -12,7 +12,10 @@ import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.formatName import com.hedvig.android.core.common.formatSsn import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState +import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractId +import com.hedvig.android.data.contract.toContractGroup import com.hedvig.android.data.display.items.DisplayItem import com.hedvig.android.data.productvariant.toAddonVariant import com.hedvig.android.data.productvariant.toProductVariant @@ -139,6 +142,7 @@ private fun InsuranceContractsQuery.Data.CurrentMember.PendingContract.toPending }, cost = this.cost.toMonthlyCost(), basePremium = UiMoney.fromMoneyFragment(this.basePremium), + chipId = ChipIdState.NotRequired, ) } @@ -226,6 +230,10 @@ private fun ContractFragment.toContract( description = it.description, ) }.orEmpty(), + chipId = when (isMissingPetId) { + true -> ChipIdState.Missing + false -> ChipIdState.NotRequired + }, ) } diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseDemo.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseDemo.kt index bcd09c12f8..6a5f94e878 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseDemo.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/GetInsuranceContractsUseCaseDemo.kt @@ -5,6 +5,7 @@ import arrow.core.right import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.productvariant.ProductVariant @@ -91,6 +92,7 @@ internal class GetInsuranceContractsUseCaseDemo : GetInsuranceContractsUseCase { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.NotRequired, ), ).right(), ) diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/InsuranceContract.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/InsuranceContract.kt index 4d914ffbe5..614261a04b 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/InsuranceContract.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/data/InsuranceContract.kt @@ -3,6 +3,7 @@ package com.hedvig.android.feature.insurances.data import com.hedvig.android.core.common.formatName import com.hedvig.android.core.common.formatSsn import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractId import com.hedvig.android.data.display.items.DisplayItem import com.hedvig.android.data.productvariant.AddonVariant @@ -37,6 +38,8 @@ sealed interface InsuranceContract { val basePremium: UiMoney + val chipId: ChipIdState + data class EstablishedInsuranceContract( override val id: String, override val displayName: String, @@ -56,6 +59,7 @@ sealed interface InsuranceContract { override val tierName: String?, override val existingAddons: List, override val availableAddons: List, + override val chipId: ChipIdState, ) : InsuranceContract { override val productVariant: ProductVariant = currentInsuranceAgreement.productVariant override val displayItems: List = currentInsuranceAgreement.displayItems @@ -81,6 +85,7 @@ sealed interface InsuranceContract { override val addons: List?, override val cost: MonthlyCost, override val basePremium: UiMoney, + override val chipId: ChipIdState, ) : InsuranceContract { override val coInsured: List = listOf() override val coOwners: List = listOf() diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurance/InsuranceDestination.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurance/InsuranceDestination.kt index d6d5e01def..ee06e30777 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurance/InsuranceDestination.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurance/InsuranceDestination.kt @@ -52,6 +52,7 @@ import com.hedvig.android.crosssells.CrossSellItemPlaceholder import com.hedvig.android.crosssells.CrossSellsSection import com.hedvig.android.data.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.FlowType +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractId import com.hedvig.android.data.contract.ContractType @@ -689,6 +690,7 @@ private val previewPendingContract = InsuranceContract.PendingInsuranceContract( UiMoney(89.0, UiCurrencyCode.SEK), discounts = emptyList(), ), + chipId = ChipIdState.NotRequired, ) private val previewInsurance = EstablishedInsuranceContract( @@ -737,4 +739,5 @@ private val previewInsurance = EstablishedInsuranceContract( supportsTierChange = true, existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing, ) diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt index 921a116e8a..47e59ddc08 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailDestination.kt @@ -43,6 +43,7 @@ import com.hedvig.android.compose.ui.animateContentHeight import com.hedvig.android.compose.ui.plus import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup.RENTAL import com.hedvig.android.data.contract.ContractId import com.hedvig.android.data.contract.ContractType @@ -111,6 +112,7 @@ internal fun ContractDetailDestination( navigateToRemoveAddon: (ContractId?, AddonVariant?) -> Unit, navigateToUpgradeAddon: (ContractId?, AddonVariant?) -> Unit, navigateToAddAddon: (AvailableAddon) -> Unit, + navigateToChipIdScreen: (String) -> Unit, ) { val uiState: ContractDetailsUiState by viewModel.uiState.collectAsStateWithLifecycle() ContractDetailScreen( @@ -131,6 +133,7 @@ internal fun ContractDetailDestination( navigateToAddAddon = navigateToAddAddon, navigateToRemoveAddon = navigateToRemoveAddon, navigateToUpgradeAddon = navigateToUpgradeAddon, + navigateToChipIdScreen = navigateToChipIdScreen, ) } @@ -153,6 +156,7 @@ private fun ContractDetailScreen( openUrl: (String) -> Unit, navigateToRemoveAddon: (ContractId?, AddonVariant?) -> Unit, navigateToUpgradeAddon: (ContractId?, AddonVariant?) -> Unit, + navigateToChipIdScreen: (String) -> Unit, navigateToAddAddon: (AvailableAddon) -> Unit, ) { Column(Modifier.fillMaxSize()) { @@ -358,6 +362,10 @@ private fun ContractDetailScreen( navigateToAddAddon = navigateToAddAddon, navigateToRemoveAddon = navigateToRemoveAddon, navigateToUpgradeAddon = navigateToUpgradeAddon, + chipIdState = contract.chipId, + onFillChipId = { + navigateToChipIdScreen(contract.id) + }, ) } @@ -495,6 +503,7 @@ private fun PreviewContractDetailScreen() { supportsTierChange = true, existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing, ), true, ), @@ -515,6 +524,7 @@ private fun PreviewContractDetailScreen() { navigateToAddAddon = {}, navigateToRemoveAddon = { _, _ -> }, navigateToUpgradeAddon = { _, _ -> }, + navigateToChipIdScreen = {}, ) } } @@ -544,6 +554,7 @@ private fun PreviewContractDetailScreenFailure() { navigateToAddAddon = {}, navigateToRemoveAddon = { _, _ -> }, navigateToUpgradeAddon = { _, _ -> }, + navigateToChipIdScreen = {}, ) } } diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/EditInsuranceBottomSheetContent.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/EditInsuranceBottomSheetContent.kt index 705cec954c..d572685cc5 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/EditInsuranceBottomSheetContent.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/EditInsuranceBottomSheetContent.kt @@ -43,7 +43,6 @@ import hedvig.resources.Res import hedvig.resources.general_cancel_button import hedvig.resources.general_continue_button import hedvig.resources.insurance_details_change_coverage -import kotlin.collections.buildList import org.jetbrains.compose.resources.stringResource @Composable diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt index fa475ab710..f21ffe4aa9 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/UpcomingChangesBottomSheetContent.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.display.items.DisplayItem import com.hedvig.android.data.display.items.DisplayItem.DisplayItemValue import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt index b8ae40fe5f..a7da9fb90f 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt @@ -28,6 +28,7 @@ import com.hedvig.android.compose.ui.preview.DoubleBooleanCollectionPreviewParam import com.hedvig.android.core.common.daysUntil import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractId import com.hedvig.android.data.contract.ContractType @@ -90,6 +91,8 @@ import hedvig.resources.ADDON_FLOW_UPGRADE_ADDON_DESCRIPTION import hedvig.resources.CHANGE_ADDRESS_CO_INSURED_LABEL import hedvig.resources.CHANGE_ADDRESS_ONLY_YOU import hedvig.resources.CHANGE_ADDRESS_YOU_PLUS +import hedvig.resources.CHIP_ID_MISSING_BUTTON +import hedvig.resources.CHIP_ID_MISSING_MESSAGE import hedvig.resources.CONTRACT_ADD_COINSURED_ACTIVE_FROM import hedvig.resources.CONTRACT_ADD_COINSURED_ACTIVE_UNTIL import hedvig.resources.CONTRACT_COINSURED @@ -137,6 +140,7 @@ internal fun YourInfoTab( onChangeTierClick: () -> Unit, isDecommissioned: Boolean, upcomingChangesInsuranceAgreement: InsuranceAgreement?, + chipIdState: ChipIdState, onEditCoInsuredClick: () -> Unit, onEditCoOwnersClick: () -> Unit, onMissingCoInsuredInfoClick: () -> Unit, @@ -145,6 +149,7 @@ internal fun YourInfoTab( onNavigateToNewConversation: () -> Unit, openUrl: (String) -> Unit, onCancelInsuranceClick: () -> Unit, + onFillChipId: () -> Unit, isTerminated: Boolean, contractHolderDisplayName: String, contractHolderSSN: String?, @@ -352,6 +357,20 @@ internal fun YourInfoTab( .padding(horizontal = 16.dp), ) } + val hasMissingChipId = chipIdState is ChipIdState.Missing + if (hasMissingChipId) { + HedvigNotificationCard( + message = stringResource(Res.string.CHIP_ID_MISSING_MESSAGE), + priority = Attention, + style = Button( + stringResource(Res.string.CHIP_ID_MISSING_BUTTON), + onFillChipId, + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } AddonsSection( existingAddons = existingAddons, availableAddons = availableAddons, @@ -362,7 +381,12 @@ internal fun YourInfoTab( ) if (!isTerminated) { Column(Modifier.padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - if (allowEditCoInsured || allowEditCoOwners || allowChangeTier || allowTerminatingInsurance) { + if (allowEditCoInsured || + allowEditCoOwners || + allowChangeTier || + allowTerminatingInsurance || + chipIdState is ChipIdState.Missing + ) { HedvigButton( text = stringResource(Res.string.CONTRACT_EDIT_INFO_LABEL), enabled = true, @@ -1007,6 +1031,8 @@ private fun PreviewYourInfoTab() { navigateToRemoveAddon = { _, _ -> }, navigateToUpgradeAddon = { _, _ -> }, navigateToAddAddon = {}, + chipIdState = ChipIdState.Missing, + onFillChipId = {}, ) } } diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/navigation/InsuranceGraph.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/navigation/InsuranceGraph.kt index 9963e68743..3be76738c4 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/navigation/InsuranceGraph.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/navigation/InsuranceGraph.kt @@ -41,6 +41,7 @@ fun NavGraphBuilder.insuranceGraph( onNavigateToAddonPurchaseFlow: (List, AvailableAddon?) -> Unit, onNavigateToRemoveAddon: (ContractId?, AddonVariant?) -> Unit, navigateToUpgradeAddon: (ContractId?, AddonVariant?) -> Unit, + navigateToChipIdScreen: (String) -> Unit, ) { navgraph( startDestination = InsurancesDestination.Insurances::class, @@ -103,6 +104,7 @@ fun NavGraphBuilder.insuranceGraph( navigateToAddAddon = { availableAddon -> onNavigateToAddonPurchaseFlow(listOf(availableAddon.relatedContractId), availableAddon) }, + navigateToChipIdScreen = navigateToChipIdScreen, ) } navdestination { diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsDestination.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsDestination.kt index 93f7a883ca..a05b18ab40 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsDestination.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsDestination.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.ImageLoader import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.productvariant.ProductVariant @@ -178,6 +179,7 @@ private class PreviewTerminatedContractsUiStateProvider : supportsTierChange = false, existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing, ), ), ), diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurance/presentation/InsurancePresenterTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurance/presentation/InsurancePresenterTest.kt index 7e389e6487..03ca1900cc 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurance/presentation/InsurancePresenterTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurance/presentation/InsurancePresenterTest.kt @@ -22,6 +22,7 @@ import com.hedvig.android.data.addons.data.AddonBannerInfo import com.hedvig.android.data.addons.data.AddonBannerSource import com.hedvig.android.data.addons.data.FlowType import com.hedvig.android.data.addons.data.GetAddonBannerInfoUseCase +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.contract.CrossSell @@ -97,6 +98,7 @@ internal class InsurancePresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing, ), EstablishedInsuranceContract( id = "contractId#2", @@ -144,6 +146,7 @@ internal class InsurancePresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), ) private val terminatedContracts: List = listOf( @@ -193,6 +196,7 @@ internal class InsurancePresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), EstablishedInsuranceContract( id = "contractId#4", @@ -240,6 +244,7 @@ internal class InsurancePresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), ) private val validCrossSells: CrossSellResult = CrossSellResult( diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt index 9f3a61e2c4..575209deda 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/insurancedetail/ContractDetailPresenterTest.kt @@ -10,6 +10,7 @@ import assertk.assertions.isInstanceOf import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.productvariant.ProductVariant @@ -290,6 +291,7 @@ class ContractDetailPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ) private val insuranceWithTerminationDate = EstablishedInsuranceContract( @@ -338,6 +340,7 @@ class ContractDetailPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ) private val responseTurbine = Turbine>() diff --git a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsPresenterTest.kt b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsPresenterTest.kt index 2057be26bf..14bd019207 100644 --- a/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsPresenterTest.kt +++ b/app/feature/feature-insurances/src/test/kotlin/com/hedvig/android/feature/insurances/terminatedcontracts/TerminatedContractsPresenterTest.kt @@ -10,6 +10,7 @@ import assertk.assertions.isInstanceOf import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ChipIdState import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.productvariant.ProductVariant @@ -233,6 +234,7 @@ class TerminatedContractsPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), EstablishedInsuranceContract( "contractId2", @@ -280,6 +282,7 @@ class TerminatedContractsPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), ) @@ -329,6 +332,7 @@ class TerminatedContractsPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ) private val activeInsurances = listOf( @@ -378,6 +382,7 @@ class TerminatedContractsPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), EstablishedInsuranceContract( "contractId4", @@ -425,6 +430,7 @@ class TerminatedContractsPresenterTest { tierName = "STANDARD", existingAddons = emptyList(), availableAddons = emptyList(), + chipId = ChipIdState.Missing ), ) } diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt index defc760a8f..7dae99dc64 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt @@ -108,6 +108,7 @@ internal fun ProfileDestination( openUrl: (String) -> Unit, onNavigateToNewConversation: () -> Unit, viewModel: ProfileViewModel, + navigateToChipId: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -127,6 +128,7 @@ internal fun ProfileDestination( snoozeNotificationPermission = { viewModel.emit(ProfileUiEvent.SnoozeNotificationPermission) }, onLogout = { viewModel.emit(ProfileUiEvent.Logout) }, onNavigateToNewConversation = onNavigateToNewConversation, + navigateToChipId = navigateToChipId, ) } @@ -148,6 +150,7 @@ private fun ProfileScreen( onNavigateToNewConversation: () -> Unit, snoozeNotificationPermission: () -> Unit, onLogout: () -> Unit, + navigateToChipId: () -> Unit, ) { val systemBarInsetTopDp = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() @@ -226,6 +229,7 @@ private fun ProfileScreen( modifier = Modifier.onConsumedWindowInsetsChanged { consumedWindowInsets.insets = it }, onNavigateToNewConversation = onNavigateToNewConversation, navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipId, ) if (memberReminders.isNotEmpty()) { Spacer(Modifier.height(16.dp)) diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt index ee1dde4b12..5d152bd500 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt @@ -46,6 +46,7 @@ fun NavGraphBuilder.profileGraph( onNavigateToTravelCertificate: () -> Unit, onNavigateToInsuranceEvidence: () -> Unit, openUrl: (String) -> Unit, + navigateToChipId: () -> Unit, ) { navgraph( startDestination = ProfileDestination.Profile::class, @@ -83,6 +84,7 @@ fun NavGraphBuilder.profileGraph( onNavigateToNewConversation = dropUnlessResumed { onNavigateToNewConversation() }, + navigateToChipId = navigateToChipId, ) } navdestination( diff --git a/app/member-reminders/member-reminders-public/build.gradle.kts b/app/member-reminders/member-reminders-public/build.gradle.kts index 08750bf6f2..7ff1c4d7cf 100644 --- a/app/member-reminders/member-reminders-public/build.gradle.kts +++ b/app/member-reminders/member-reminders-public/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.coreBuildConstants) implementation(projects.coreCommonPublic) implementation(projects.coreDemoMode) + implementation(projects.dataContract) implementation(projects.dataPayingMember) implementation(projects.featureFlagsPublic) diff --git a/app/member-reminders/member-reminders-public/src/main/graphql/QueryMissingChipIdReminder.graphql b/app/member-reminders/member-reminders-public/src/main/graphql/QueryMissingChipIdReminder.graphql new file mode 100644 index 0000000000..0364a2a9f8 --- /dev/null +++ b/app/member-reminders/member-reminders-public/src/main/graphql/QueryMissingChipIdReminder.graphql @@ -0,0 +1,7 @@ +query MissingChipIdReminder { + currentMember { + activeContracts { + isMissingPetId + } + } +} diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt index 198f49b21d..f0de036c79 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt @@ -3,6 +3,7 @@ package com.hedvig.android.memberreminders import arrow.core.Either import arrow.core.NonEmptyList import arrow.core.merge +import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.memberreminders.MemberReminder.ContactInfoUpdateNeeded import java.util.UUID @@ -22,6 +23,7 @@ internal class GetMemberRemindersUseCaseImpl( private val getUpcomingRenewalRemindersUseCase: GetUpcomingRenewalRemindersUseCase, private val getNeedsCoInsuredInfoRemindersUseCase: GetNeedsCoInsuredInfoRemindersUseCase, private val getContactInfoUpdateIsNeededUseCase: GetContactInfoUpdateIsNeededUseCase, + private val getMissingChipIdReminderUseCase: GetMissingChipIdReminderUseCase, ) : GetMemberRemindersUseCase { override fun invoke(): Flow { return combine( @@ -52,19 +54,22 @@ internal class GetMemberRemindersUseCaseImpl( getUpcomingRenewalRemindersUseCase.invoke().map { it.mapLeft { null }.merge() }, getNeedsCoInsuredInfoRemindersUseCase.invoke(), getContactInfoUpdateIsNeededUseCase.invoke(), - ) { - enableNotifications: MemberReminder.EnableNotifications?, - connectPayment: MemberReminder.PaymentReminder?, - upcomingRenewalReminders: NonEmptyList?, - coInsuredInfoResult: Either>, - contactInfoReminder: Either, - -> + getMissingChipIdReminderUseCase.invoke(), + ) { values -> + val enableNotifications = values[0] as MemberReminder.EnableNotifications? + val connectPayment = values[1] as MemberReminder.PaymentReminder? + val upcomingRenewalReminders = values[2] as? NonEmptyList? + val coInsuredInfoResult = values[3] as? Either> + val contactInfoReminder = values[4] as? Either + val missingChipIdReminder = values[5] as? Either + MemberReminders( connectPayment = connectPayment, upcomingRenewals = upcomingRenewalReminders, enableNotifications = enableNotifications, - coInsuredInfo = coInsuredInfoResult.getOrNull(), - updateContactInfo = contactInfoReminder.getOrNull(), + coInsuredInfo = coInsuredInfoResult?.getOrNull(), + updateContactInfo = contactInfoReminder?.getOrNull(), + missingChipId = missingChipIdReminder?.getOrNull(), ) } } @@ -76,6 +81,7 @@ data class MemberReminders( val enableNotifications: MemberReminder.EnableNotifications? = null, val coInsuredInfo: List? = null, val updateContactInfo: ContactInfoUpdateNeeded? = null, + val missingChipId: MemberReminder.MissingChipId? = null, ) { /** * In some cases a reminder may be present but may not be applicable in our current app state. @@ -90,6 +96,9 @@ data class MemberReminders( coInsuredInfo?.let { addAll(coInsuredInfo) } + missingChipId?.let { + add(it) + } if (!alreadyHasNotificationPermission) { enableNotifications?.let { add(enableNotifications) @@ -139,4 +148,8 @@ sealed interface MemberReminder { data object ContactInfoUpdateNeeded : MemberReminder { override val id: String = UUID.randomUUID().toString() } + + data class MissingChipId( + override val id: String = UUID.randomUUID().toString(), + ) : MemberReminder } diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMissingChipIdReminderUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMissingChipIdReminderUseCase.kt new file mode 100644 index 0000000000..0e037df7c5 --- /dev/null +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMissingChipIdReminderUseCase.kt @@ -0,0 +1,37 @@ +package com.hedvig.android.memberreminders + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeFlow +import com.hedvig.android.core.common.ErrorMessage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import octopus.MissingChipIdReminderQuery + +internal interface GetMissingChipIdReminderUseCase { + fun invoke(): Flow> +} + +internal class GetMissingChipIdReminderUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetMissingChipIdReminderUseCase { + override fun invoke(): Flow> { + return apolloClient.query(MissingChipIdReminderQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage) + .mapLatest { result: Either -> + either { + result + .bind() + .currentMember + .activeContracts + .firstOrNull { it.isMissingPetId } + ?.let { MemberReminder.MissingChipId() } + } + } + } +} diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/di/MemberRemindersModule.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/di/MemberRemindersModule.kt index a5b2208941..1fc843f70e 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/di/MemberRemindersModule.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/di/MemberRemindersModule.kt @@ -14,6 +14,8 @@ import com.hedvig.android.memberreminders.GetContactInfoUpdateIsNeededUseCase import com.hedvig.android.memberreminders.GetContactInfoUpdateIsNeededUseCaseImpl import com.hedvig.android.memberreminders.GetMemberRemindersUseCase import com.hedvig.android.memberreminders.GetMemberRemindersUseCaseImpl +import com.hedvig.android.memberreminders.GetMissingChipIdReminderUseCase +import com.hedvig.android.memberreminders.GetMissingChipIdReminderUseCaseImpl import com.hedvig.android.memberreminders.GetNeedsCoInsuredInfoRemindersUseCase import com.hedvig.android.memberreminders.GetNeedsCoInsuredInfoRemindersUseCaseImpl import com.hedvig.android.memberreminders.GetUpcomingRenewalRemindersUseCase @@ -44,6 +46,11 @@ val memberRemindersModule = module { get(), ) } + single { + GetMissingChipIdReminderUseCaseImpl( + get(), + ) + } single { GetMemberRemindersUseCaseImpl( get(), @@ -51,6 +58,7 @@ val memberRemindersModule = module { get(), get(), get(), + get(), ) } single { diff --git a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt index 37924c30c1..59401d599f 100644 --- a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt +++ b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt @@ -34,12 +34,14 @@ class GetMemberRemindersUseCaseTest { val getUpcomingRenewalRemindersUseCase = TestGetUpcomingRenewalRemindersUseCase() val getNeedsCoInsuredInfoRemindersUseCase = TestGetNeedsCoInsuredInfoRemindersUseCase() val getContactInfoUpdateIsNeededUseCase = TestGetContactInfoUpdateIsNeededUseCase() + val getMissingChipIdReminderUseCase = TestGetMissingChipIdReminderUseCase() val getMemberRemindersUseCase = GetMemberRemindersUseCaseImpl( enableNotificationsReminderSnoozeManager = enableNotificationsReminderManager, getConnectPaymentReminderUseCase = getConnectPaymentReminderUseCase, getUpcomingRenewalRemindersUseCase = getUpcomingRenewalRemindersUseCase, getNeedsCoInsuredInfoRemindersUseCase = getNeedsCoInsuredInfoRemindersUseCase, getContactInfoUpdateIsNeededUseCase = getContactInfoUpdateIsNeededUseCase, + getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase ) getMemberRemindersUseCase.invoke().test { @@ -66,12 +68,14 @@ class GetMemberRemindersUseCaseTest { val getUpcomingRenewalRemindersUseCase = TestGetUpcomingRenewalRemindersUseCase() val getNeedsCoInsuredInfoRemindersUseCase = TestGetNeedsCoInsuredInfoRemindersUseCase() val getContactInfoUpdateIsNeededUseCase = TestGetContactInfoUpdateIsNeededUseCase() + val getMissingChipIdReminderUseCase = TestGetMissingChipIdReminderUseCase() val getMemberRemindersUseCase = GetMemberRemindersUseCaseImpl( enableNotificationsReminderSnoozeManager = enableNotificationsReminderManager, getConnectPaymentReminderUseCase = getConnectPaymentReminderUseCase, getUpcomingRenewalRemindersUseCase = getUpcomingRenewalRemindersUseCase, getNeedsCoInsuredInfoRemindersUseCase = getNeedsCoInsuredInfoRemindersUseCase, getContactInfoUpdateIsNeededUseCase = getContactInfoUpdateIsNeededUseCase, + getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase ) val testId = "test" @@ -133,4 +137,15 @@ class GetMemberRemindersUseCaseTest { ) } } + + class TestGetMissingChipIdReminderUseCase : GetMissingChipIdReminderUseCase { + override fun invoke(): Flow> { + return flowOf( + either { + null + }, + ) + } + } + } diff --git a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt index 40447eca80..2018cc8ebe 100644 --- a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt +++ b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -19,19 +20,27 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import com.hedvig.android.compose.pager.indicator.HorizontalPagerIndicator import com.hedvig.android.core.common.daysUntil import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.design.system.hedvig.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.LocalTextStyle import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.memberreminders.MemberReminder import com.hedvig.android.memberreminders.MemberReminder.UpcomingRenewal import com.hedvig.android.notification.permission.NotificationPermissionState +import hedvig.resources.CHIP_ID_MISSING_BUTTON +import hedvig.resources.CHIP_ID_MISSING_MESSAGE import hedvig.resources.CONTRACT_COINSURED_MISSING_ADD_INFO import hedvig.resources.CONTRACT_COINSURED_MISSING_INFO_TEXT import hedvig.resources.CONTRACT_COOWNERS_MISSING_INFO_TEXT @@ -51,6 +60,68 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import org.jetbrains.compose.resources.stringResource +@Composable +fun getMemberReminderMessage(reminder: MemberReminder): String { + return when (reminder) { + is MemberReminder.CoInsuredInfo -> stringResource( + when (reminder.coInsuredType) { + CoInsuredFlowType.CoInsured -> Res.string.CONTRACT_COINSURED_MISSING_INFO_TEXT + CoInsuredFlowType.CoOwners -> Res.string.CONTRACT_COOWNERS_MISSING_INFO_TEXT + } + ) + + is MemberReminder.PaymentReminder.ConnectPayment -> + stringResource(Res.string.info_card_missing_payment_body) + + is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> + stringResource(Res.string.info_card_missing_payment_missing_payments_body, reminder.terminationDate) + + is UpcomingRenewal -> + { + val daysUntilRenewal = remember(TimeZone.currentSystemDefault(), reminder.renewalDate) { + daysUntil(reminder.renewalDate) + } + stringResource(Res.string.DASHBOARD_RENEWAL_PROMPTER_BODY, daysUntilRenewal) + } + + + is MemberReminder.EnableNotifications -> + stringResource(Res.string.PROFILE_ALLOW_NOTIFICATIONS_INFO_LABEL) + + is MemberReminder.ContactInfoUpdateNeeded -> + stringResource(Res.string.MISSING_CONTACT_INFO_CARD_TEXT) + + is MemberReminder.MissingChipId -> + stringResource(Res.string.CHIP_ID_MISSING_MESSAGE) + } +} + +@Composable +fun rememberMaxLineCountForReminders( + memberReminders: List, + maxWidthPx: Int, +): Int { + val textMeasurer = rememberTextMeasurer() + val density = LocalDensity.current + val fontFamilyResolver = LocalFontFamilyResolver.current + + val messages = memberReminders.map { reminder -> getMemberReminderMessage(reminder) } + val textStyle = LocalTextStyle.current + + return remember(messages, textMeasurer, maxWidthPx, textStyle, density, fontFamilyResolver) { + messages.maxOfOrNull { message -> + val textLayout = textMeasurer.measure( + text = AnnotatedString(message), + style = textStyle, + constraints = Constraints(maxWidth = maxWidthPx), + density = density, + fontFamilyResolver = fontFamilyResolver, + ) + textLayout.lineCount + } ?: 1 + } +} + @Composable fun MemberReminderCardsWithoutNotification( memberReminders: List, @@ -60,6 +131,7 @@ fun MemberReminderCardsWithoutNotification( onNavigateToNewConversation: () -> Unit, contentPadding: PaddingValues, navigateToContactInfo: () -> Unit, + navigateToChipId: () -> Unit, modifier: Modifier = Modifier, ) { MemberReminderCards( @@ -72,6 +144,7 @@ fun MemberReminderCardsWithoutNotification( notificationPermissionState = null, contentPadding = contentPadding, navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipId, modifier = modifier, ) } @@ -85,6 +158,7 @@ fun MemberReminderCards( snoozeNotificationPermissionReminder: () -> Unit, onNavigateToNewConversation: () -> Unit, navigateToContactInfo: () -> Unit, + navigateToChipId: () -> Unit, notificationPermissionState: NotificationPermissionState?, contentPadding: PaddingValues, modifier: Modifier = Modifier, @@ -99,32 +173,44 @@ fun MemberReminderCards( onNavigateToNewConversation = onNavigateToNewConversation, snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, notificationPermissionState = notificationPermissionState, - modifier = modifier.padding(contentPadding), navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipId, + modifier = modifier.padding(contentPadding), + minLines = 1 ) } else if (memberReminders.isNotEmpty()) { val pagerState = rememberPagerState(pageCount = { memberReminders.size }) - HorizontalPager( - state = pagerState, - contentPadding = contentPadding, - beyondViewportPageCount = 1, - pageSpacing = 8.dp, - key = { index -> memberReminders[index].id }, - modifier = Modifier - .fillMaxWidth() - .systemGestureExclusion(), - ) { page -> - MemberReminderCard( - memberReminder = memberReminders[page], - navigateToAddMissingInfo = navigateToAddMissingInfo, - navigateToConnectPayment = navigateToConnectPayment, - openUrl = openUrl, - onNavigateToNewConversation = onNavigateToNewConversation, - snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, - notificationPermissionState = notificationPermissionState, - navigateToContactInfo = navigateToContactInfo, - modifier = modifier.fillMaxWidth(), + BoxWithConstraints(Modifier.fillMaxWidth()) { + val minLineCount = rememberMaxLineCountForReminders( + memberReminders = memberReminders, + maxWidthPx = constraints.maxWidth ) + Column { + HorizontalPager( + state = pagerState, + contentPadding = contentPadding, + beyondViewportPageCount = 1, + pageSpacing = 8.dp, + key = { index -> memberReminders[index].id }, + modifier = Modifier + .fillMaxWidth() + .systemGestureExclusion(), + ) { page -> + MemberReminderCard( + memberReminder = memberReminders[page], + navigateToAddMissingInfo = navigateToAddMissingInfo, + navigateToConnectPayment = navigateToConnectPayment, + openUrl = openUrl, + onNavigateToNewConversation = onNavigateToNewConversation, + snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, + notificationPermissionState = notificationPermissionState, + navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipId, + modifier = modifier.fillMaxWidth(), + minLines = minLineCount + ) + } + } } Spacer(Modifier.height(16.dp)) @@ -147,20 +233,23 @@ private fun ColumnScope.MemberReminderCard( navigateToAddMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToConnectPayment: () -> Unit, navigateToContactInfo: () -> Unit, + navigateToChipId: () -> Unit, openUrl: (String) -> Unit, snoozeNotificationPermissionReminder: () -> Unit, onNavigateToNewConversation: () -> Unit, notificationPermissionState: NotificationPermissionState?, + minLines: Int, modifier: Modifier = Modifier, ) { when (memberReminder) { is MemberReminder.CoInsuredInfo -> { ReminderCoInsuredInfo( - coInsuredType = memberReminder.coInsuredType, + memberReminder = memberReminder, navigateToAddMissingInfo = { navigateToAddMissingInfo(memberReminder.contractId, memberReminder.coInsuredType) }, modifier = modifier, + minLines = minLines, ) } @@ -168,22 +257,26 @@ private fun ColumnScope.MemberReminderCard( ReminderCardConnectPayment( navigateToConnectPayment = navigateToConnectPayment, modifier = modifier, + minLines = minLines, + memberReminder = memberReminder, ) } is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> { ReminderCardMissingPayment( - terminationDate = memberReminder.terminationDate, + memberReminder = memberReminder, onNavigateToNewConversation = onNavigateToNewConversation, modifier = modifier, + minLines = minLines, ) } is UpcomingRenewal -> { ReminderCardUpcomingRenewals( - upcomingRenewal = memberReminder, openUrl = openUrl, + memberReminder = memberReminder, modifier = modifier, + minLines = minLines, ) } @@ -199,6 +292,7 @@ private fun ColumnScope.MemberReminderCard( ReminderCardEnableNotifications( snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, requestNotificationPermission = notificationPermissionState::launchPermissionRequest, + minLines = minLines, ) } } @@ -208,6 +302,15 @@ private fun ColumnScope.MemberReminderCard( ReminderCardUpdateContactInfo( navigateToContactInfo = navigateToContactInfo, modifier = modifier, + minLines = minLines, + ) + } + + is MemberReminder.MissingChipId -> { + ReminderMissingChipId( + navigateToChipId = navigateToChipId, + minLines = minLines, + modifier = modifier, ) } } @@ -226,10 +329,12 @@ private val cardReminderExitTransition = fadeOut() + shrinkVertically( fun ReminderCardEnableNotifications( snoozeNotificationPermissionReminder: () -> Unit, requestNotificationPermission: () -> Unit, + minLines: Int = 1, modifier: Modifier = Modifier, ) { + val message = getMemberReminderMessage(MemberReminder.EnableNotifications()) HedvigNotificationCard( - message = stringResource(Res.string.PROFILE_ALLOW_NOTIFICATIONS_INFO_LABEL), + message = message, modifier = modifier, priority = NotificationPriority.Info, style = InfoCardStyle.Buttons( @@ -238,92 +343,128 @@ fun ReminderCardEnableNotifications( rightButtonText = stringResource(Res.string.PUSH_NOTIFICATIONS_ALERT_ACTION_OK), onRightButtonClick = requestNotificationPermission, ), + minLines = minLines, ) } @Composable -fun ReminderCardUpdateContactInfo(navigateToContactInfo: () -> Unit, modifier: Modifier = Modifier) { +fun ReminderCardUpdateContactInfo( + navigateToContactInfo: () -> Unit, + modifier: Modifier = Modifier, + minLines: Int = 1, +) { + val message = getMemberReminderMessage(MemberReminder.ContactInfoUpdateNeeded) HedvigNotificationCard( - message = stringResource(Res.string.MISSING_CONTACT_INFO_CARD_TEXT), + message = message, modifier = modifier, priority = NotificationPriority.Info, style = InfoCardStyle.Button( buttonText = stringResource(Res.string.MISSING_CONTACT_INFO_CARD_BUTTON), onButtonClick = navigateToContactInfo, ), + minLines = minLines, + ) +} + +@Composable +internal fun ReminderMissingChipId( + navigateToChipId: () -> Unit, + minLines: Int, + modifier: Modifier = Modifier, +) { + val message = getMemberReminderMessage(MemberReminder.MissingChipId()) + HedvigNotificationCard( + message = message, + modifier = modifier, + priority = NotificationPriority.Attention, + style = InfoCardStyle.Button( + buttonText = stringResource(Res.string.CHIP_ID_MISSING_BUTTON), + onButtonClick = navigateToChipId, + ), + minLines = minLines ) } @Composable -private fun ReminderCardConnectPayment(navigateToConnectPayment: () -> Unit, modifier: Modifier = Modifier) { +private fun ReminderCardConnectPayment( + memberReminder: MemberReminder, + navigateToConnectPayment: () -> Unit, + modifier: Modifier = Modifier, + minLines: Int = 1, +) { + val message = getMemberReminderMessage(memberReminder) HedvigNotificationCard( - message = stringResource(Res.string.info_card_missing_payment_body), + message = message, modifier = modifier, priority = NotificationPriority.Attention, style = InfoCardStyle.Button( buttonText = stringResource(Res.string.PROFILE_PAYMENT_CONNECT_DIRECT_DEBIT_BUTTON), onButtonClick = navigateToConnectPayment, ), + minLines = minLines, ) } @Composable private fun ReminderCardMissingPayment( - terminationDate: LocalDate, + memberReminder: MemberReminder, onNavigateToNewConversation: () -> Unit, modifier: Modifier = Modifier, + minLines: Int = 1, ) { + val message = getMemberReminderMessage(memberReminder) HedvigNotificationCard( - message = stringResource(Res.string.info_card_missing_payment_missing_payments_body, terminationDate), + message = message, modifier = modifier, priority = NotificationPriority.Attention, style = InfoCardStyle.Button( buttonText = stringResource(Res.string.open_chat), onButtonClick = onNavigateToNewConversation, ), + minLines = minLines, ) } @Composable private fun ReminderCardUpcomingRenewals( - upcomingRenewal: UpcomingRenewal, + memberReminder: MemberReminder.UpcomingRenewal, openUrl: (String) -> Unit, modifier: Modifier = Modifier, + minLines: Int = 1, ) { - val daysUntilRenewal = remember(TimeZone.currentSystemDefault(), upcomingRenewal.renewalDate) { - daysUntil(upcomingRenewal.renewalDate) - } - val style = upcomingRenewal.draftCertificateUrl?.let { + val message = getMemberReminderMessage(memberReminder) + val style = memberReminder.draftCertificateUrl?.let { InfoCardStyle.Button( onButtonClick = { openUrl(it) }, buttonText = stringResource(Res.string.CONTRACT_VIEW_CERTIFICATE_BUTTON), ) } ?: InfoCardStyle.Default HedvigNotificationCard( - message = stringResource(Res.string.DASHBOARD_RENEWAL_PROMPTER_BODY, daysUntilRenewal), + message = message, modifier = modifier, priority = NotificationPriority.Info, style = style, + minLines = minLines, ) } @Composable private fun ReminderCoInsuredInfo( - coInsuredType: CoInsuredFlowType, + memberReminder: MemberReminder, navigateToAddMissingInfo: () -> Unit, modifier: Modifier = Modifier, + minLines: Int = 1, ) { + val message = getMemberReminderMessage(memberReminder) HedvigNotificationCard( - message = when (coInsuredType) { - CoInsuredFlowType.CoInsured -> stringResource(Res.string.CONTRACT_COINSURED_MISSING_INFO_TEXT) - CoInsuredFlowType.CoOwners -> stringResource(Res.string.CONTRACT_COOWNERS_MISSING_INFO_TEXT) - }, + message = message, modifier = modifier, priority = NotificationPriority.Attention, style = InfoCardStyle.Button( buttonText = stringResource(Res.string.CONTRACT_COINSURED_MISSING_ADD_INFO), onButtonClick = navigateToAddMissingInfo, ), + minLines = minLines, ) } @@ -332,7 +473,10 @@ private fun ReminderCoInsuredInfo( private fun PreviewReminderCardEnableNotifications() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { - ReminderCardEnableNotifications({}, {}) + ReminderCardEnableNotifications( + snoozeNotificationPermissionReminder = {}, + requestNotificationPermission = {}, + ) } } } @@ -342,7 +486,24 @@ private fun PreviewReminderCardEnableNotifications() { private fun PreviewReminderCardConnectPayment() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { - ReminderCardConnectPayment({}) + ReminderCardConnectPayment( + navigateToConnectPayment = {}, + memberReminder = MemberReminder.PaymentReminder.ConnectPayment() + ) + } + } +} + +@Preview +@Composable +private fun PreviewReminderCardMissingPayment() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ReminderCardConnectPayment( + navigateToConnectPayment = {}, + memberReminder = MemberReminder.PaymentReminder.TerminationDueToMissedPayments( + terminationDate = LocalDate(2029,1,1)) + ) } } } @@ -350,11 +511,12 @@ private fun PreviewReminderCardConnectPayment() { @Preview @Composable private fun PreviewReminderCardUpcomingRenewals() { + val upcomingRenewal = UpcomingRenewal("contract name", LocalDate.parse("2024-03-05"), "") HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ReminderCardUpcomingRenewals( - UpcomingRenewal("contract name", LocalDate.parse("2024-03-05"), ""), - {}, + openUrl = {}, + memberReminder = upcomingRenewal ) } } @@ -365,7 +527,10 @@ private fun PreviewReminderCardUpcomingRenewals() { private fun PreviewReminderCardCoInsuredInfo() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { - ReminderCoInsuredInfo(CoInsuredFlowType.CoInsured, {}) + ReminderCoInsuredInfo( + memberReminder = MemberReminder.CoInsuredInfo("", CoInsuredFlowType.CoInsured), + navigateToAddMissingInfo = {}, + ) } } } @@ -376,7 +541,20 @@ private fun PreviewReminderCardUpdateContactInfo() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ReminderCardUpdateContactInfo( - {}, + navigateToContactInfo = {}, + ) + } + } +} + +@Preview +@Composable +private fun PreviewReminderMissingChipId() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ReminderMissingChipId( + navigateToChipId = {}, + minLines = 1 ) } } diff --git a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt index ece00c6729..650003a628 100644 --- a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt +++ b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt @@ -75,6 +75,10 @@ interface HedvigDeepLinkContainer { * that comes after `/` which follows the base deeplink domain */ fun buildDeepLink(suffix: String): String + + val petIdWithoutContractId: List + + val petIdWithContractId: List } internal class HedvigDeepLinkContainerImpl( @@ -185,6 +189,13 @@ internal class HedvigDeepLinkContainerImpl( override fun buildDeepLink(suffix: String): String { return "${baseDeepLinkDomains.first()}/$suffix" } + + override val petIdWithoutContractId: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> + "$baseDeepLinkDomain/pet-id" + } + override val petIdWithContractId: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> + "$baseDeepLinkDomain/pet-id?contractId={contractId}" + } } val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List @@ -218,4 +229,10 @@ val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List insuranceEvidence.first(), claimFlow.first(), moveContract.first(), - ) + editCoOwners.first(), + carAddon.first(), + carAddonWithContractId.first(), + travelAddonWithContractId.first(), + petIdWithoutContractId.first(), + petIdWithContractId.first(), + ) diff --git a/hedvig-lint/lint-baseline/lint-baseline-feature-chip-id.xml b/hedvig-lint/lint-baseline/lint-baseline-feature-chip-id.xml new file mode 100644 index 0000000000..249a9d603a --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-feature-chip-id.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + +