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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+