diff --git a/app/apollo/apollo-octopus-public/src/main/graphql/com/hedvig/android/apollo/octopus/graphql/insurance/FragmentProductVariantFragment.graphql b/app/apollo/apollo-octopus-public/src/main/graphql/com/hedvig/android/apollo/octopus/graphql/insurance/FragmentProductVariantFragment.graphql index 71f18c3792..ad9544228d 100644 --- a/app/apollo/apollo-octopus-public/src/main/graphql/com/hedvig/android/apollo/octopus/graphql/insurance/FragmentProductVariantFragment.graphql +++ b/app/apollo/apollo-octopus-public/src/main/graphql/com/hedvig/android/apollo/octopus/graphql/insurance/FragmentProductVariantFragment.graphql @@ -1,5 +1,7 @@ fragment ProductVariantFragment on ProductVariant { displayName + displayNameTier + displayNameTierLong typeOfContract partner perils { diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 51c009251f..026b8bfa9f 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -228,6 +228,8 @@ dependencies { implementation(projects.theme) implementation(projects.trackingCore) implementation(projects.trackingDatadog) + implementation(projects.featureChooseTier) + implementation(projects.dataChangetier) debugImplementation(libs.androidx.compose.uiTestManifest) debugImplementation(libs.androidx.compose.uiTooling) 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 506a96de72..2f1c0a09a6 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 @@ -41,6 +41,7 @@ import com.hedvig.android.core.common.di.datastoreFileQualifier import com.hedvig.android.core.datastore.di.dataStoreModule import com.hedvig.android.core.demomode.di.demoModule import com.hedvig.android.core.fileupload.fileUploadModule +import com.hedvig.android.data.changetier.di.dataChangeTierModule import com.hedvig.android.data.chat.di.dataChatModule import com.hedvig.android.data.claimflow.di.claimFlowDataModule import com.hedvig.android.data.conversations.di.dataConversationsModule @@ -50,6 +51,7 @@ import com.hedvig.android.data.termination.di.terminationDataModule import com.hedvig.android.datadog.core.addDatadogConfiguration import com.hedvig.android.datadog.core.di.datadogModule import com.hedvig.android.datadog.demo.tracking.di.datadogDemoTrackingModule +import com.hedvig.android.feature.change.tier.di.chooseTierModule import com.hedvig.android.feature.changeaddress.di.changeAddressModule import com.hedvig.android.feature.chat.di.chatModule import com.hedvig.android.feature.claim.details.di.claimDetailsModule @@ -297,6 +299,7 @@ val applicationModule = module { authModule, buildConstantsModule, changeAddressModule, + chooseTierModule, chatModule, claimDetailsModule, claimFlowDataModule, @@ -307,6 +310,7 @@ val applicationModule = module { coreAppReviewModule, coreCommonModule, dataChatModule, + dataChangeTierModule, dataConversationsModule, dataPayingMemberModule, dataStoreModule, 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 071f5a28e8..d5f2aa8465 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 @@ -16,6 +16,9 @@ import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.designsystem.material3.motion.MotionDefaults import com.hedvig.android.data.claimflow.ClaimFlowStep import com.hedvig.android.data.claimflow.toClaimFlowDestination +import com.hedvig.android.feature.change.tier.navigation.ChooseTierGraphDestination +import com.hedvig.android.feature.change.tier.navigation.InsuranceCustomizationParameters +import com.hedvig.android.feature.change.tier.navigation.changeTierGraph import com.hedvig.android.feature.changeaddress.navigation.changeAddressGraph import com.hedvig.android.feature.chat.navigation.ChatDestination import com.hedvig.android.feature.chat.navigation.ChatDestinations @@ -158,6 +161,14 @@ internal fun HedvigNavHost( openUrl = openUrl, navigator = navigator, ) + changeTierGraph( + openUrl = openUrl, + navigator = navigator, + navController = hedvigAppState.navController, + onNavigateToNewConversation = { backStackEntry -> + navigateToNewConversation(backStackEntry) + }, + ) insuranceGraph( nestedGraphs = { terminateInsuranceGraph( @@ -195,6 +206,27 @@ internal fun HedvigNavHost( finishApp() } }, + redirectToChangeTierFlow = { backStackEntry, idWithIntent -> + with(navigator) { + backStackEntry.navigate( + destination = ChooseTierGraphDestination( + InsuranceCustomizationParameters( + insuranceId = idWithIntent.first, + activationDateEpochDays = idWithIntent.second.activationDate.toEpochDays(), + currentTierLevel = idWithIntent.second.currentTierLevel, + currentTierName = idWithIntent.second.currentTierName, + quoteIds = idWithIntent.second.quotes.map { + it.id + }, + ), + ), + ) { + typedPopUpTo { + inclusive = true + } + } + } + }, ) }, navigator = navigator, diff --git a/app/core/core-common-public/src/main/kotlin/com/hedvig/android/core/common/di/Qualifier.kt b/app/core/core-common-public/src/main/kotlin/com/hedvig/android/core/common/di/Qualifier.kt index fb4cf59154..56957b5e35 100644 --- a/app/core/core-common-public/src/main/kotlin/com/hedvig/android/core/common/di/Qualifier.kt +++ b/app/core/core-common-public/src/main/kotlin/com/hedvig/android/core/common/di/Qualifier.kt @@ -14,6 +14,11 @@ val datastoreFileQualifier = qualifier("datastoreFileQualifier") */ val databaseFileQualifier = qualifier("databaseFileQualifier") +/** + * The [java.io.File] to be used by Room to store the Change Tier Quotes database in. + */ +val tierQuotesDatabaseFileQualifier = qualifier("tierQuotesDatabaseFileQualifier") + /** * A qualifier to pass a [kotlin.coroutines.CoroutineContext] which should default to * [kotlinx.coroutines.Dispatchers.IO] for production code diff --git a/app/data/data-changetier/build.gradle.kts b/app/data/data-changetier/build.gradle.kts new file mode 100644 index 0000000000..85d6424291 --- /dev/null +++ b/app/data/data-changetier/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("hedvig.android.ktlint") + id("hedvig.android.library") + alias(libs.plugins.squareSortDependencies) + alias(libs.plugins.apollo) + alias(libs.plugins.serialization) +} + +dependencies { + + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.koin.core) + implementation(libs.kotlinx.datetime) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreUiData) + implementation(projects.dataContractPublic) + implementation(projects.featureFlagsPublic) + implementation(projects.dataProductVariantPublic) + implementation(projects.dataProductVariantAndroid) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + implementation(projects.dataChat) +} + +apollo { + service("octopus") { + packageName = "octopus" + dependsOn(projects.apolloOctopusPublic, true) + } +} diff --git a/app/data/data-changetier/src/main/AndroidManifest.xml b/app/data/data-changetier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..568741e54f --- /dev/null +++ b/app/data/data-changetier/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/data/data-changetier/src/main/graphql/MutationChangeTierDeductible.graphql b/app/data/data-changetier/src/main/graphql/MutationChangeTierDeductible.graphql new file mode 100644 index 0000000000..b948a1e342 --- /dev/null +++ b/app/data/data-changetier/src/main/graphql/MutationChangeTierDeductible.graphql @@ -0,0 +1,32 @@ +mutation ChangeTierDeductibleCreateIntent($contractId: ID!, $source: ChangeTierDeductibleSource!) { + changeTierDeductibleCreateIntent(input:{ contractId: $contractId, source: $source }) { + intent { + activationDate + currentTierLevel + currentTierName + quotes { + deductible { + amount { + ...MoneyFragment + } + displayText + percentage + } + displayItems { + displaySubtitle + displayTitle + displayValue + } + id + premium { + ...MoneyFragment + } + productVariant { + ...ProductVariantFragment + } + tierLevel + tierName + } + } + } +} diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt new file mode 100644 index 0000000000..fa9fca807a --- /dev/null +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/ChangeTierRepository.kt @@ -0,0 +1,59 @@ +package com.hedvig.android.data.changetier.data + +import arrow.core.Either +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.data.changetier.database.TierQuoteMapper +import com.hedvig.android.data.chat.database.TierQuoteDao + +interface ChangeTierRepository { + suspend fun startChangeTierIntentAndGetQuotesId( + insuranceId: String, + source: ChangeTierCreateSource, + ): Either + + suspend fun getQuoteById(id: String): TierDeductibleQuote // TODO: I guess it better to be Either too? + + suspend fun getQuotesById(ids: List): List + + suspend fun addQuotesToDb(quotes: List) +} + +internal class ChangeTierRepositoryImpl( + private val createChangeTierDeductibleIntentUseCase: CreateChangeTierDeductibleIntentUseCase, + private val tierQuoteDao: TierQuoteDao, + private val mapper: TierQuoteMapper, +) : ChangeTierRepository { + override suspend fun startChangeTierIntentAndGetQuotesId( + insuranceId: String, + source: ChangeTierCreateSource, + ): Either { + tierQuoteDao.clearAllQuotes() + val result = createChangeTierDeductibleIntentUseCase.invoke(insuranceId, source) + result.onRight { intent -> + val quotes = intent.quotes.map { quote -> + mapper.quoteToDbModel(quote) + } + tierQuoteDao.insertAll(quotes) + } + return result + } + + override suspend fun getQuoteById(id: String): TierDeductibleQuote { + val dbModel = tierQuoteDao.getOneQuoteById(id) + return mapper.dbModelToQuote(dbModel) + } + + override suspend fun getQuotesById(ids: List): List { + val list = tierQuoteDao.getQuotesById(ids) + return list.map { entity -> + mapper.dbModelToQuote(entity) + } + } + + override suspend fun addQuotesToDb(quotes: List) { + val mapped = quotes.map { quote -> + mapper.quoteToDbModel(quote) + } + tierQuoteDao.insertAll(mapped) + } +} diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt new file mode 100644 index 0000000000..c8d267cc9c --- /dev/null +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt @@ -0,0 +1,226 @@ +package com.hedvig.android.data.changetier.data + +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.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiCurrencyCode.SEK +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.productVariant.android.toProductVariant +import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature +import com.hedvig.android.logger.LogPriority.ERROR +import com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.first +import octopus.ChangeTierDeductibleCreateIntentMutation + +internal interface CreateChangeTierDeductibleIntentUseCase { + suspend fun invoke( + insuranceId: String, + source: ChangeTierCreateSource, + ): Either +} + +internal class CreateChangeTierDeductibleIntentUseCaseImpl( + private val apolloClient: ApolloClient, + private val featureManager: FeatureManager, +) : CreateChangeTierDeductibleIntentUseCase { + override suspend fun invoke( + insuranceId: String, + source: ChangeTierCreateSource, + ): Either { + return either { + val isTierEnabled = featureManager.isFeatureEnabled(Feature.TIER).first() + if (!isTierEnabled) { + logcat(ERROR) { "Tried to get changeTierQuotes when feature flag is disabled!" } + raise(ErrorMessage()) + } else { +// ChangeTierDeductibleIntent( +// activationDate = LocalDate(2024, 10, 3), +// currentTierName = "Standard", +// currentTierLevel = 1, +// quotes = quotesForPreview, +// ) + // todo: remove mock!!! + + val changeTierDeductibleResponse = apolloClient + .mutation( + ChangeTierDeductibleCreateIntentMutation( + contractId = insuranceId, + source = source.toSource(), + ), + ) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + val intent = changeTierDeductibleResponse.getOrNull()?.changeTierDeductibleCreateIntent?.intent + if (intent != null) { + try { + val quotes = intent.quotes.map { + TierDeductibleQuote( + id = it.id, + deductible = it.deductible.toDeductible(), + displayItems = it.displayItems.toDisplayItems(), + premium = UiMoney.fromMoneyFragment(it.premium), + productVariant = it.productVariant.toProductVariant(), + tier = Tier( + tierName = it.tierName!!, + tierLevel = it.tierLevel!!, + info = it.productVariant.displayNameTierLong, + tierDisplayName = it.productVariant.displayNameTier, + ), + ) + } + ChangeTierDeductibleIntent( + activationDate = intent.activationDate, + currentTierLevel = intent.currentTierLevel, + currentTierName = intent.currentTierName, + quotes = quotes, + ) + } catch (e: Exception) { + logcat(ERROR) { "Tried to get changeTierQuotes but quotes have tierLevel or tierName == null!" } + raise(ErrorMessage()) + } + } else { + if (changeTierDeductibleResponse.isRight()) { + logcat(ERROR) { "Tried to get changeTierQuotes but output intent is null!" } + } + if (changeTierDeductibleResponse.isLeft()) { + logcat(ERROR) { "Tried to get changeTierQuotes but got error: $changeTierDeductibleResponse!" } + } + raise(ErrorMessage()) + } + } + } + } +} + +private fun ChangeTierDeductibleCreateIntentMutation.Data.ChangeTierDeductibleCreateIntent.Intent.Quote.Deductible?.toDeductible(): Deductible? { + return if (this != null) { + Deductible( + deductibleAmount = UiMoney.fromMoneyFragment(this.amount), + deductiblePercentage = this.percentage, + description = this.displayText, + ) + } else { + null + } +} + +private fun List.toDisplayItems(): List { + return this.map { + ChangeTierDeductibleDisplayItem( + displayTitle = it.displayTitle, + displaySubtitle = it.displaySubtitle, + displayValue = it.displayValue, + ) + } +} + +private val quotesForPreview = listOf( + TierDeductibleQuote( + id = "id0", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(199.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id1", + deductible = Deductible( + UiMoney(1000.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(255.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id2", + deductible = Deductible( + UiMoney(3500.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden", + ), + displayItems = listOf(), + premium = UiMoney(355.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id3", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(230.0, SEK), + tier = Tier("STANDARD", tierLevel = 1, info = "Vårt mellanpaket med hög ersättning.", tierDisplayName = "Standard"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id4", + deductible = Deductible( + UiMoney(3500.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden", + ), + displayItems = listOf(), + premium = UiMoney(655.0, SEK), + tier = Tier("STANDARD", tierLevel = 1, info = "Vårt mellanpaket med hög ersättning.", tierDisplayName = "Standard"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), +) diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/IntentModel.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/IntentModel.kt new file mode 100644 index 0000000000..94a906600d --- /dev/null +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/IntentModel.kt @@ -0,0 +1,67 @@ +package com.hedvig.android.data.changetier.data + +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource.SELF_SERVICE +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource.TERMINATION_BETTER_COVERAGE +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource.TERMINATION_BETTER_PRICE +import com.hedvig.android.data.productvariant.ProductVariant +import kotlinx.datetime.LocalDate +import octopus.type.ChangeTierDeductibleSource + +data class ChangeTierDeductibleIntent( + val activationDate: LocalDate, + val currentTierLevel: Int?, + val currentTierName: String?, + val quotes: List, +) + +data class TierDeductibleQuote( + val id: String, + val tier: Tier, + val deductible: Deductible?, + val premium: UiMoney, + val displayItems: List, + val productVariant: ProductVariant, +) + +data class ChangeTierDeductibleDisplayItem( + val displayTitle: String, + val displaySubtitle: String?, + val displayValue: String, +) + +data class Tier( + val tierName: String, + val tierLevel: Int, + val tierDisplayName: String?, + val info: String?, +) + +data class Deductible( + val deductibleAmount: UiMoney?, + val deductiblePercentage: Int?, + val description: String, +) { + val percentageNotZero = deductiblePercentage != null && deductiblePercentage != 0 + val optionText = if (percentageNotZero && deductibleAmount != null) { + "$deductibleAmount + $deductiblePercentage%" + } else if (percentageNotZero) { + "$deductiblePercentage%" + } else { + deductibleAmount?.toString() ?: "" + } +} + +enum class ChangeTierCreateSource { + SELF_SERVICE, + TERMINATION_BETTER_COVERAGE, + TERMINATION_BETTER_PRICE, +} + +internal fun ChangeTierCreateSource.toSource(): ChangeTierDeductibleSource { + return when (this) { + SELF_SERVICE -> ChangeTierDeductibleSource.SELF_SERVICE + TERMINATION_BETTER_COVERAGE -> ChangeTierDeductibleSource.TERMINATION_BETTER_COVERAGE + TERMINATION_BETTER_PRICE -> ChangeTierDeductibleSource.TERMINATION_BETTER_PRICE + } +} diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/database/TierQuoteMapper.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/database/TierQuoteMapper.kt new file mode 100644 index 0000000000..f8aea722e2 --- /dev/null +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/database/TierQuoteMapper.kt @@ -0,0 +1,196 @@ +package com.hedvig.android.data.changetier.database + +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleDisplayItem +import com.hedvig.android.data.changetier.data.Deductible +import com.hedvig.android.data.changetier.data.Tier +import com.hedvig.android.data.changetier.data.TierDeductibleQuote +import com.hedvig.android.data.chat.database.ChangeTierDeductibleDisplayItemDbModel +import com.hedvig.android.data.chat.database.ChangeTierQuoteEntity +import com.hedvig.android.data.chat.database.DeductibleDbModel +import com.hedvig.android.data.chat.database.InsurableLimitDBM +import com.hedvig.android.data.chat.database.InsuranceVariantDocumentDBM +import com.hedvig.android.data.chat.database.ProductVariantDbModel +import com.hedvig.android.data.chat.database.ProductVariantPerilDBM +import com.hedvig.android.data.chat.database.TierDbModel +import com.hedvig.android.data.chat.database.UiMoneyDbModel +import com.hedvig.android.data.productvariant.InsurableLimit +import com.hedvig.android.data.productvariant.InsuranceVariantDocument +import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.data.productvariant.ProductVariantPeril + +internal class TierQuoteMapper { + fun quoteToDbModel(quote: TierDeductibleQuote): ChangeTierQuoteEntity { + return ChangeTierQuoteEntity( + id = quote.id, + tier = mapTierToTierDbModel(quote.tier), + deductible = quote.deductible?.let { mapDeductibleToDeductibleDbModel(it) }, + premium = mapUiMoneyToUiMoneyDbModel(quote.premium), + displayItems = quote.displayItems.map { mapDisplayItemToDbModel(it) }, + productVariant = mapProductVariantToDbModel(quote.productVariant), + ) + } + + fun dbModelToQuote(entity: ChangeTierQuoteEntity): TierDeductibleQuote { + return TierDeductibleQuote( + id = entity.id, + tier = mapTierDbModelToTier(entity.tier), + deductible = entity.deductible?.let { mapDeductibleDbModelToDeductible(it) }, + premium = mapUiMoneyDbModelToUiMoney(entity.premium), + displayItems = entity.displayItems.map { mapDbModelToDisplayItem(it) }, + productVariant = mapProductVariantDbModelToProductVariant(entity.productVariant), + ) + } + + private fun mapTierToTierDbModel(tier: Tier): TierDbModel { + return TierDbModel( + tierName = tier.tierName, + tierLevel = tier.tierLevel, + info = tier.info, + tierDisplayName = tier.tierDisplayName, + ) + } + + private fun mapTierDbModelToTier(tierDbModel: TierDbModel): Tier { + return Tier( + tierName = tierDbModel.tierName, + tierLevel = tierDbModel.tierLevel, + info = tierDbModel.info, + tierDisplayName = tierDbModel.tierDisplayName, + ) + } + + private fun mapDeductibleToDeductibleDbModel(deductible: Deductible): DeductibleDbModel { + return DeductibleDbModel( + deductibleAmount = deductible.deductibleAmount?.let { mapUiMoneyToUiMoneyDbModel(it) }, + deductiblePercentage = deductible.deductiblePercentage, + description = deductible.description, + ) + } + + private fun mapDeductibleDbModelToDeductible(deductibleDbModel: DeductibleDbModel): Deductible { + return Deductible( + deductibleAmount = deductibleDbModel.deductibleAmount?.let { mapUiMoneyDbModelToUiMoney(it) }, + deductiblePercentage = deductibleDbModel.deductiblePercentage, + description = deductibleDbModel.description, + ) + } + + private fun mapUiMoneyToUiMoneyDbModel(uiMoney: UiMoney): UiMoneyDbModel { + return UiMoneyDbModel( + amount = uiMoney.amount, + currencyCode = uiMoney.currencyCode, + ) + } + + private fun mapUiMoneyDbModelToUiMoney(uiMoneyDbModel: UiMoneyDbModel): UiMoney { + return UiMoney( + amount = uiMoneyDbModel.amount, + currencyCode = uiMoneyDbModel.currencyCode, + ) + } + + private fun mapDisplayItemToDbModel(item: ChangeTierDeductibleDisplayItem): ChangeTierDeductibleDisplayItemDbModel { + return ChangeTierDeductibleDisplayItemDbModel( + displayTitle = item.displayTitle, + displaySubtitle = item.displaySubtitle, + displayValue = item.displayValue, + ) + } + + private fun mapDbModelToDisplayItem( + dbModel: ChangeTierDeductibleDisplayItemDbModel, + ): ChangeTierDeductibleDisplayItem { + return ChangeTierDeductibleDisplayItem( + displayTitle = dbModel.displayTitle, + displaySubtitle = dbModel.displaySubtitle, + displayValue = dbModel.displayValue, + ) + } + + private fun mapProductVariantToDbModel(variant: ProductVariant): ProductVariantDbModel { + return ProductVariantDbModel( + displayName = variant.displayName, + contractGroup = variant.contractGroup, + contractType = variant.contractType, + partner = variant.partner, + perils = variant.perils.map { productVariantPerilToDbModel(it) }, + insurableLimits = variant.insurableLimits.map { insurableLimitToDbModel(it) }, + documents = variant.documents.map { insuranceVariantDocumentToDbModel(it) }, + tierName = variant.displayTierName, + tierNameLong = variant.displayTierNameLong, + ) + } + + private fun mapProductVariantDbModelToProductVariant(variantDbModel: ProductVariantDbModel): ProductVariant { + return ProductVariant( + displayName = variantDbModel.displayName, + contractGroup = variantDbModel.contractGroup, + contractType = variantDbModel.contractType, + partner = variantDbModel.partner, + perils = variantDbModel.perils.map { productVariantPerilFromDbModel(it) }, + insurableLimits = variantDbModel.insurableLimits.map { insurableLimitFromDbModel(it) }, + documents = variantDbModel.documents.map { insuranceVariantDocumentFromDbModel(it) }, + displayTierName = variantDbModel.tierName, + displayTierNameLong = variantDbModel.tierNameLong, + ) + } + + fun productVariantPerilToDbModel(peril: ProductVariantPeril): ProductVariantPerilDBM { + return ProductVariantPerilDBM( + id = peril.id, + title = peril.title, + description = peril.description, + info = peril.info, + covered = peril.covered, + exceptions = peril.exceptions, + colorCode = peril.colorCode, + ) + } + + fun productVariantPerilFromDbModel(perilDb: ProductVariantPerilDBM): ProductVariantPeril { + return ProductVariantPeril( + id = perilDb.id, + title = perilDb.title, + description = perilDb.description, + info = perilDb.info, + covered = perilDb.covered, + exceptions = perilDb.exceptions, + colorCode = perilDb.colorCode, + ) + } + + fun insurableLimitToDbModel(limit: InsurableLimit): InsurableLimitDBM { + return InsurableLimitDBM( + label = limit.label, + limit = limit.limit, + description = limit.description, + type = limit.type, + ) + } + + fun insurableLimitFromDbModel(limitDb: InsurableLimitDBM): InsurableLimit { + return InsurableLimit( + label = limitDb.label, + limit = limitDb.limit, + description = limitDb.description, + type = limitDb.type, + ) + } + + fun insuranceVariantDocumentToDbModel(doc: InsuranceVariantDocument): InsuranceVariantDocumentDBM { + return InsuranceVariantDocumentDBM( + displayName = doc.displayName, + url = doc.url, + type = doc.type, + ) + } + + fun insuranceVariantDocumentFromDbModel(docDb: InsuranceVariantDocumentDBM): InsuranceVariantDocument { + return InsuranceVariantDocument( + displayName = docDb.displayName, + url = docDb.url, + type = docDb.type, + ) + } +} diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/di/DataChangeTierModule.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/di/DataChangeTierModule.kt new file mode 100644 index 0000000000..01bbbe5a9a --- /dev/null +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/di/DataChangeTierModule.kt @@ -0,0 +1,30 @@ +package com.hedvig.android.data.changetier.di + +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.data.changetier.data.ChangeTierRepository +import com.hedvig.android.data.changetier.data.ChangeTierRepositoryImpl +import com.hedvig.android.data.changetier.data.CreateChangeTierDeductibleIntentUseCase +import com.hedvig.android.data.changetier.data.CreateChangeTierDeductibleIntentUseCaseImpl +import com.hedvig.android.data.changetier.database.TierQuoteMapper +import com.hedvig.android.data.chat.database.TierQuoteDao +import com.hedvig.android.featureflags.FeatureManager +import org.koin.dsl.module + +val dataChangeTierModule = module { + single { + CreateChangeTierDeductibleIntentUseCaseImpl( + get(), + get(), + ) + } + single { + TierQuoteMapper() + } + single { + ChangeTierRepositoryImpl( + createChangeTierDeductibleIntentUseCase = get(), + tierQuoteDao = get(), + mapper = get(), + ) + } +} diff --git a/app/data/data-chat/build.gradle.kts b/app/data/data-chat/build.gradle.kts index d688f5f268..aeb2677df9 100644 --- a/app/data/data-chat/build.gradle.kts +++ b/app/data/data-chat/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.room) alias(libs.plugins.squareSortDependencies) + alias(libs.plugins.serialization) } dependencies { @@ -16,6 +17,12 @@ dependencies { implementation(libs.sqlite.bundled) implementation(libs.uuid) implementation(projects.coreCommonPublic) + implementation(projects.dataProductVariantPublic) + implementation(projects.dataContractPublic) + implementation(projects.coreUiData) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + } room { diff --git a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/AppDatabase.kt b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/AppDatabase.kt index cbb74a71ee..b91b69d94f 100644 --- a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/AppDatabase.kt +++ b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/AppDatabase.kt @@ -7,20 +7,24 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.migration.AutoMigrationSpec import com.hedvig.android.data.chat.database.converter.InstantConverter +import com.hedvig.android.data.chat.database.converter.TierQuoteTypeConverter import com.hedvig.android.data.chat.database.converter.UuidConverter @Database( - entities = [ChatMessageEntity::class, RemoteKeyEntity::class], - version = 2, + entities = [ChatMessageEntity::class, RemoteKeyEntity::class, ChangeTierQuoteEntity::class], + version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2, spec = Migration1To2::class), + AutoMigration(from = 2, to = 3), ], ) -@TypeConverters(InstantConverter::class, UuidConverter::class) +@TypeConverters(InstantConverter::class, UuidConverter::class, TierQuoteTypeConverter::class) abstract class AppDatabase : RoomDatabase() { abstract fun chatDao(): ChatDao abstract fun remoteKeyDao(): RemoteKeyDao + + abstract fun tierQuoteDao(): TierQuoteDao } @DeleteTable(tableName = "conversations") diff --git a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/ChangeTierQuoteEntity.kt b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/ChangeTierQuoteEntity.kt new file mode 100644 index 0000000000..23b78903ae --- /dev/null +++ b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/ChangeTierQuoteEntity.kt @@ -0,0 +1,107 @@ +package com.hedvig.android.data.chat.database + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.data.chat.database.converter.TierQuoteTypeConverter +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.productvariant.InsurableLimit.InsurableLimitType +import com.hedvig.android.data.productvariant.InsuranceVariantDocument.InsuranceDocumentType +import kotlinx.serialization.Serializable + +@Entity(tableName = "change_tier_quotes") +data class ChangeTierQuoteEntity( + @PrimaryKey + val id: String, + @TypeConverters(TierQuoteTypeConverter::class) + val tier: TierDbModel, + @TypeConverters(TierQuoteTypeConverter::class) + val deductible: DeductibleDbModel?, + @TypeConverters(TierQuoteTypeConverter::class) + val premium: UiMoneyDbModel, + @TypeConverters(TierQuoteTypeConverter::class) + val displayItems: List, + @TypeConverters(TierQuoteTypeConverter::class) + val productVariant: ProductVariantDbModel, +) + +@Serializable +@Entity +data class UiMoneyDbModel( + val amount: Double, + val currencyCode: UiCurrencyCode, +) + +@Serializable +@Entity +data class ChangeTierDeductibleDisplayItemDbModel( + val displayTitle: String, + val displaySubtitle: String?, + val displayValue: String, +) + +@Serializable +@Entity +data class TierDbModel( + val tierName: String, + val tierLevel: Int, + val info: String?, + val tierDisplayName: String?, +) + +@Serializable +@Entity +data class DeductibleDbModel( + @TypeConverters(TierQuoteTypeConverter::class) + val deductibleAmount: UiMoneyDbModel?, + val deductiblePercentage: Int?, + val description: String, +) + +@Serializable +@Entity +data class ProductVariantDbModel( + val displayName: String, + val contractGroup: ContractGroup, + val contractType: ContractType, + val partner: String?, + @TypeConverters(TierQuoteTypeConverter::class) + val perils: List, + @TypeConverters(TierQuoteTypeConverter::class) + val insurableLimits: List, + @TypeConverters(TierQuoteTypeConverter::class) + val documents: List, + val tierName: String?, + val tierNameLong: String?, +) + +@Serializable +@Entity +data class ProductVariantPerilDBM( + val id: String, + val title: String, + val description: String, + val info: String, + val covered: List, + val exceptions: List, + val colorCode: String?, +) + +@Serializable +@Entity +data class InsurableLimitDBM( + val label: String, + val limit: String, + val description: String, + val type: InsurableLimitType, +) + +@Serializable +@Entity +data class InsuranceVariantDocumentDBM( + val displayName: String, + val url: String, + val type: InsuranceDocumentType, +) diff --git a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/TierQuoteDao.kt b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/TierQuoteDao.kt new file mode 100644 index 0000000000..d6a72685a0 --- /dev/null +++ b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/TierQuoteDao.kt @@ -0,0 +1,21 @@ +package com.hedvig.android.data.chat.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface TierQuoteDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(quotes: List) + + @Query("DELETE FROM change_tier_quotes") + suspend fun clearAllQuotes() + + @Query("SELECT * FROM change_tier_quotes WHERE id=:id LIMIT 1") + suspend fun getOneQuoteById(id: String): ChangeTierQuoteEntity + + @Query("SELECT * FROM change_tier_quotes WHERE id IN(:ids)") + suspend fun getQuotesById(ids: List): List +} diff --git a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/converter/TierQuoteTypeConverter.kt b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/converter/TierQuoteTypeConverter.kt new file mode 100644 index 0000000000..ce549c8a41 --- /dev/null +++ b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/database/converter/TierQuoteTypeConverter.kt @@ -0,0 +1,95 @@ +package com.hedvig.android.data.chat.database.converter + +import androidx.room.TypeConverter +import com.hedvig.android.data.chat.database.ChangeTierDeductibleDisplayItemDbModel +import com.hedvig.android.data.chat.database.DeductibleDbModel +import com.hedvig.android.data.chat.database.ProductVariantDbModel +import com.hedvig.android.data.chat.database.TierDbModel +import com.hedvig.android.data.chat.database.UiMoneyDbModel +import com.hedvig.android.data.productvariant.InsurableLimit +import com.hedvig.android.data.productvariant.InsuranceVariantDocument +import com.hedvig.android.data.productvariant.ProductVariantPeril +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class TierQuoteTypeConverter { + @TypeConverter + fun fromTierDbModel(model: TierDbModel): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toTierDbModel(string: String): TierDbModel { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromDeductibleDbModel(model: DeductibleDbModel): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toDeductibleDbModel(string: String): DeductibleDbModel { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromUiMoneyDbModel(model: UiMoneyDbModel): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toUiMoneyDbModel(string: String): UiMoneyDbModel { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromChangeTierDeductibleDisplayItemDbModel(models: List): String { + return Json.encodeToString(models) + } + + @TypeConverter + fun toChangeTierDeductibleDisplayItemDbModel(string: String): List { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromProductVariantDbModel(model: ProductVariantDbModel): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toProductVariantDbModel(string: String): ProductVariantDbModel { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromProductVariantPeril(model: ProductVariantPeril): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toProductVariantPeril(string: String): ProductVariantPeril { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromInsurableLimit(model: InsurableLimit): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toInsurableLimit(string: String): InsurableLimit { + return Json.decodeFromString(string) + } + + @TypeConverter + fun fromInsuranceVariantDocument(model: InsuranceVariantDocument): String { + return Json.encodeToString(model) + } + + @TypeConverter + fun toInsuranceVariantDocument(string: String): InsuranceVariantDocument { + return Json.decodeFromString(string) + } +} diff --git a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/di/DataChatModule.kt b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/di/DataChatModule.kt index 2d4582e743..c14de9a8a7 100644 --- a/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/di/DataChatModule.kt +++ b/app/data/data-chat/src/main/kotlin/com/hedvig/android/data/chat/di/DataChatModule.kt @@ -8,6 +8,7 @@ import com.hedvig.android.core.common.di.ioDispatcherQualifier import com.hedvig.android.data.chat.database.AppDatabase import com.hedvig.android.data.chat.database.ChatDao import com.hedvig.android.data.chat.database.RemoteKeyDao +import com.hedvig.android.data.chat.database.TierQuoteDao import java.io.File import kotlin.coroutines.CoroutineContext import org.koin.dsl.module @@ -26,6 +27,9 @@ val dataChatModule = module { single { get().chatDao() } + single { + get().tierQuoteDao() + } single { get().remoteKeyDao() } diff --git a/app/data/data-product-variant/data-product-variant-android/src/main/kotlin/com/hedvig/android/data/productVariant/android/ProductVariantAndroid.kt b/app/data/data-product-variant/data-product-variant-android/src/main/kotlin/com/hedvig/android/data/productVariant/android/ProductVariantAndroid.kt index f506ec909b..e359459b1c 100644 --- a/app/data/data-product-variant/data-product-variant-android/src/main/kotlin/com/hedvig/android/data/productVariant/android/ProductVariantAndroid.kt +++ b/app/data/data-product-variant/data-product-variant-android/src/main/kotlin/com/hedvig/android/data/productVariant/android/ProductVariantAndroid.kt @@ -22,6 +22,8 @@ fun InsuranceVariantDocument.InsuranceDocumentType.getStringRes() = when (this) } fun ProductVariantFragment.toProductVariant() = ProductVariant( + displayTierNameLong = displayNameTierLong, + displayTierName = displayNameTier, displayName = this.displayName, contractGroup = this.typeOfContract.toContractGroup(), contractType = this.typeOfContract.toContractType(), diff --git a/app/data/data-product-variant/data-product-variant-public/src/main/kotlin/com/hedvig/android/data/productvariant/ProductVariant.kt b/app/data/data-product-variant/data-product-variant-public/src/main/kotlin/com/hedvig/android/data/productvariant/ProductVariant.kt index a08ab95425..f717990adb 100644 --- a/app/data/data-product-variant/data-product-variant-public/src/main/kotlin/com/hedvig/android/data/productvariant/ProductVariant.kt +++ b/app/data/data-product-variant/data-product-variant-public/src/main/kotlin/com/hedvig/android/data/productvariant/ProductVariant.kt @@ -11,6 +11,8 @@ data class ProductVariant( val perils: List, val insurableLimits: List, val documents: List, + val displayTierName: String? = null, + val displayTierNameLong: String? = null, ) data class ProductVariantPeril( diff --git a/app/design-system/design-system-hedvig/build.gradle.kts b/app/design-system/design-system-hedvig/build.gradle.kts index f459655c4e..bafbf58186 100644 --- a/app/design-system/design-system-hedvig/build.gradle.kts +++ b/app/design-system/design-system-hedvig/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { implementation(libs.androidx.graphicsShapes) implementation(libs.compose.richtext) implementation(libs.modal.sheet) + implementation(libs.coil.coil) + implementation(libs.coil.compose) + implementation(projects.placeholder) implementation(projects.composeUi) implementation(projects.coreResources) implementation(projects.designSystemInternals) diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt index f02da7ca27..e41cba9648 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt @@ -177,12 +177,13 @@ fun HedvigDialog( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, applyDefaultPadding: Boolean = true, + dialogProperties: DialogProperties = DialogDefaults.defaultProperties, style: DialogStyle = DialogDefaults.defaultDialogStyle, content: @Composable () -> Unit, ) { Dialog( onDismissRequest = onDismissRequest, - properties = DialogDefaults.defaultProperties, + properties = dialogProperties, ) { Surface( shape = DialogDefaults.shape, diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dropdown.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dropdown.kt index 98c87165b0..02ffae4653 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dropdown.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Dropdown.kt @@ -41,12 +41,14 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import com.hedvig.android.design.system.hedvig.DropdownDefaults.DropdownStyle import com.hedvig.android.design.system.hedvig.DropdownItem.DropdownItemWithIcon import com.hedvig.android.design.system.hedvig.DropdownItem.SimpleDropdownItem import com.hedvig.android.design.system.hedvig.icon.Checkmark import com.hedvig.android.design.system.hedvig.icon.ChevronDown import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.Lock import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.tokens.AnimationTokens import com.hedvig.android.design.system.hedvig.tokens.CommonLargeDropdownTokens @@ -78,32 +80,43 @@ fun DropdownWithDialog( isEnabled: Boolean = true, hasError: Boolean = false, errorText: String? = null, + containerColor: Color? = null, + dialogProperties: DialogProperties = DialogDefaults.defaultProperties, + dialogContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null, ) { var isDialogVisible by rememberSaveable { mutableStateOf(false) } if (isDialogVisible) { HedvigDialog( + applyDefaultPadding = dialogContent == null, + dialogProperties = dialogProperties, onDismissRequest = { isDialogVisible = false }, style = DialogDefaults.DialogStyle.NoButtons, ) { - Column( - modifier = Modifier.background( - color = dropdownColors.containerColor(false).value, - shape = size.shape, - ), - ) { - style.items.forEachIndexed { index, item -> - DropdownOption( - item = item, - size = size, - style = style, - onClick = { - onItemChosen(index) - isDialogVisible = false - }, - isSelected = index == chosenItemIndex, - ) + if (dialogContent != null) { + dialogContent { + isDialogVisible = false + } + } else { + Column( + modifier = Modifier.background( + color = dropdownColors.containerColor(false).value, + shape = size.shape, + ), + ) { + style.items.forEachIndexed { index, item -> + DropdownOption( + item = item, + size = size, + style = style, + onClick = { + onItemChosen(index) + isDialogVisible = false + }, + isSelected = index == chosenItemIndex, + ) + } } } } @@ -117,11 +130,14 @@ fun DropdownWithDialog( modifier = modifier, style = style, onClick = { - onSelectorClick() - isDialogVisible = true + if (isEnabled) { + onSelectorClick() + isDialogVisible = true + } }, errorText = errorText, isDialogOpen = isDialogVisible, + containerColor = containerColor, ) } @@ -137,13 +153,14 @@ private fun DropdownSelector( style: DropdownStyle, onClick: () -> Unit, modifier: Modifier = Modifier, + containerColor: Color? = null, ) { Column( modifier = modifier, ) { Surface( shape = size.shape, - color = dropdownColors.containerColor(showError).value, + color = containerColor ?: dropdownColors.containerColor(showError).value, modifier = Modifier .clip(size.shape) .clickable( @@ -216,8 +233,12 @@ private fun DropdownSelector( rotationZ = fullRotation }, ) { + val icon = when (isEnabled) { + false -> HedvigIcons.Lock + true -> HedvigIcons.ChevronDown + } Icon( - HedvigIcons.ChevronDown, + icon, "", tint = dropdownColors.chevronColor(isEnabled), ) @@ -265,6 +286,7 @@ fun IconStyleStartSlot(text: String, textStyle: TextStyle, textColor: Color, ico Modifier.size(DropdownTokens.IconSize), tint = Color.Unspecified, ) + is IconResource.Vector -> Icon( icon.imageVector, "", @@ -348,6 +370,7 @@ private fun DropdownOption( text = item.text, textStyle = size.textStyle, ) + is DropdownStyle.Icon -> IconStyleStartSlot( textColor = textColor, text = item.text, diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt index cbd460b48b..3fdf8df6af 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt @@ -1,9 +1,43 @@ package com.hedvig.android.design.system.hedvig +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import coil.compose.AsyncImage +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider +import com.hedvig.android.design.system.hedvig.ChipType.GENERAL +import com.hedvig.android.design.system.hedvig.ChipType.TIER +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighLightSize.Small +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Frosted +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.DARK +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.MEDIUM +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.HelipadOutline +import com.hedvig.android.placeholder.PlaceholderHighlight +import com.hedvig.android.placeholder.placeholder +import com.hedvig.android.placeholder.shimmer @Composable fun HedvigCard(modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { @@ -21,3 +55,136 @@ fun HedvigCard(modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, con content() } } + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun InsuranceCard( + chips: List, + topText: String, + bottomText: String, + imageLoader: ImageLoader, + isLoading: Boolean, + modifier: Modifier = Modifier, + fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)), + backgroundImageUrl: String? = null, +) { + Box(modifier.clip(HedvigTheme.shapes.cornerXLarge)) { + if (isLoading) { + Image( + painter = ColorPainter(Color.Black.copy(alpha = 0.3f)), + modifier = Modifier + .matchParentSize() + .placeholder(visible = true, highlight = PlaceholderHighlight.shimmer()), + contentDescription = null, + ) + } else { + AsyncImage( + model = backgroundImageUrl, + contentDescription = null, + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), + ) + } + HedvigTheme { + Column(Modifier.padding(16.dp)) { + Row(Modifier.heightIn(86.dp)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalArrangement = Arrangement.Top, + modifier = Modifier.weight(1f), + ) { + if (!isLoading) { + for (chip in chips) { + Chip(chip, Modifier.padding(bottom = 8.dp)) + } + } + } + Spacer(Modifier.width(8.dp)) + Icon( + imageVector = HedvigIcons.HelipadOutline, + contentDescription = null, + tint = HedvigTheme.colorScheme.fillWhite, + modifier = Modifier + .size(24.dp), + ) + } + Spacer(Modifier.height(8.dp)) + HedvigText( + topText, + color = HedvigTheme.colorScheme.textWhite, + modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + ) + Spacer(Modifier.height(4.dp)) + HedvigTheme(darkTheme = true) { + HedvigText( + text = bottomText, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + ) + } + } + } + } +} + +@Composable +private fun Chip(chip: ChipUiData, modifier: Modifier = Modifier) { + HedvigTheme(darkTheme = false) { + HighlightLabel( + modifier = modifier, + size = Small, + labelText = chip.chipText, + color = when (chip.chipType) { + GENERAL -> Frosted(MEDIUM) + TIER -> Frosted(DARK) + }, + ) + } +} + +@Composable +fun InsuranceCardPlaceholder(imageLoader: ImageLoader, modifier: Modifier = Modifier) { + InsuranceCard( + chips = listOf(), + topText = "", + bottomText = "", + imageLoader = imageLoader, + isLoading = true, + modifier = modifier, + ) +} + +data class ChipUiData( + val chipText: String, + val chipType: ChipType, +) + +enum class ChipType { + GENERAL, + TIER, +} + +@HedvigPreview +@Composable +private fun PreviewInsuranceCard( + @PreviewParameter(BooleanCollectionPreviewParameterProvider::class) isLoading: Boolean, +) { + HedvigTheme { + Surface { + InsuranceCard( + chips = listOf( + ChipUiData("Bas", TIER), + ChipUiData("Activates 20.03.2024", GENERAL), + ), + topText = "Home Insurance", + bottomText = "Bellmansgatan 19A ∙ You +1", + imageLoader = rememberPreviewImageLoader(), + isLoading = isLoading, + ) + } + } +} diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigPreview.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigPreview.kt index bfc60839e8..785bf7e9cb 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigPreview.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigPreview.kt @@ -1,7 +1,64 @@ package com.hedvig.android.design.system.hedvig +import android.content.Context import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import coil.ComponentRegistry +import coil.ImageLoader +import coil.disk.DiskCache +import coil.memory.MemoryCache +import coil.request.DefaultRequestOptions +import coil.request.Disposable +import coil.request.ErrorResult +import coil.request.ImageRequest +import coil.request.ImageResult +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred + +/** + * A fake ImageLoader to be used inside compose @Previews to satisfy the demands of the composables that need it. + * Do *not* call from production code. + */ +class PreviewImageLoader(private val context: Context) : ImageLoader { + override val components: ComponentRegistry = ComponentRegistry() + override val defaults: DefaultRequestOptions = DefaultRequestOptions() + override val diskCache: DiskCache? = null + override val memoryCache: MemoryCache? = null + + override fun enqueue(request: ImageRequest): Disposable { + return object : Disposable { + override val isDisposed: Boolean + get() = true + override val job: Deferred + get() = CompletableDeferred() + + override fun dispose() {} + } + } + + override suspend fun execute(request: ImageRequest): ImageResult { + return ErrorResult(null, request, Throwable()) + } + + override fun newBuilder(): ImageLoader.Builder { + return ImageLoader.Builder(context) + } + + override fun shutdown() {} +} + +/** + * A fake ImageLoader to be used inside compose @Previews to satisfy the demands of the composables that need it. + * Do *not* call from production code. + */ +@Composable +fun rememberPreviewImageLoader(): PreviewImageLoader { + val context = LocalContext.current + return remember { PreviewImageLoader(context) } +} @Preview( name = "lightMode portrait", diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt index 40d7ae76fd..4ddbea9c5c 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt @@ -14,6 +14,7 @@ import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighLightS import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Amber import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Blue +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Frosted import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Green import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Grey import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor.Pink @@ -106,6 +107,12 @@ fun HighlightLabel(labelText: String, size: HighLightSize, color: HighlightColor DARK -> highLightColors.yellowDark } } + + is Frosted -> when (color.shade) { + LIGHT -> highLightColors.frostedLight + MEDIUM -> highLightColors.frostedMedium + DARK -> highLightColors.frostedDark + } } val textColor = when (color) { is Grey -> { @@ -116,6 +123,10 @@ fun HighlightLabel(labelText: String, size: HighLightSize, color: HighlightColor } } + is Frosted -> { + highLightColors.textColorForFrosted + } + else -> highLightColors.defaultTextColor } Surface( @@ -222,6 +233,8 @@ object HighlightLabelDefaults { data class Pink(override val shade: HighlightShade) : HighlightColor() data class Grey(override val shade: HighlightShade) : HighlightColor() + + data class Frosted(override val shade: HighlightShade) : HighlightColor() } } @@ -257,6 +270,10 @@ private data class HighLightColors( val textColorForGreyLight: Color, val textColorForGreyMedium: Color, val textColorForGreyDark: Color, + val textColorForFrosted: Color, + val frostedLight: Color, + val frostedMedium: Color, + val frostedDark: Color, ) private val highLightColors: HighLightColors @@ -295,6 +312,10 @@ private val highLightColors: HighLightColors textColorForGreyLight = fromToken(TextPrimary), textColorForGreyMedium = fromToken(TextPrimary), textColorForGreyDark = fromToken(TextNegative), + textColorForFrosted = fromToken(ColorSchemeKeyTokens.TextWhite), + frostedLight = fromToken(ColorSchemeKeyTokens.SurfaceSecondaryTransparent), + frostedMedium = fromToken(ColorSchemeKeyTokens.FillTertiaryTransparent), + frostedDark = fromToken(ColorSchemeKeyTokens.FillSecondaryTransparent), ) } } diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt index aaa0942c09..1edb7f8a66 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt @@ -1,10 +1,12 @@ package com.hedvig.android.design.system.hedvig import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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 @@ -13,12 +15,14 @@ import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import com.hedvig.android.compose.ui.LayoutWithoutPlacement import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Small import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.SecondaryAlt import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle @@ -66,6 +70,7 @@ fun HedvigNotificationCard( modifier: Modifier = Modifier, withIcon: Boolean = NotificationDefaults.withIconDefault, style: InfoCardStyle = defaultStyle, + buttonLoading: Boolean = false, ) { val padding = if (withIcon) paddingWithIcon else paddingNoIcon Surface( @@ -124,7 +129,20 @@ fun HedvigNotificationCard( buttonSize = Small, modifier = Modifier.fillMaxWidth(), ) { - HedvigText(style.buttonText, style = textStyle) + LayoutWithoutPlacement(sizeAdjustingContent = { + HedvigText(style.buttonText, style = textStyle) + }) { + if (!buttonLoading) { + HedvigText(style.buttonText, style = textStyle) + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + ThreeDotsLoading() + } + } + } } } } diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/RadioOption.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/RadioOption.kt index c4bdee7e7f..744ffd8835 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/RadioOption.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/RadioOption.kt @@ -109,6 +109,51 @@ internal fun calculateLockedStateForItemInGroup(data: RadioOptionData, groupLock return if (groupLockedState == Locked) Locked else data.lockedState } +/* +* Overload with Left-Aligned style, Medium size, with Composable content parameter + */ +@Composable +fun RadioOption( + chosenState: ChosenState, + onClick: () -> Unit, + optionContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + lockedState: LockedState = NotLocked, +) { + val fixedSize = RadioOptionDefaults.RadioOptionSize.Medium.size(LeftAligned) + val interactionSource = remember { MutableInteractionSource() } + val clickableModifier = + modifier + .clip(fixedSize.shape) + .semantics { role = Role.RadioButton } + .clickable( + enabled = when (lockedState) { + Locked -> false + NotLocked -> true + }, + interactionSource = interactionSource, + indication = LocalIndication.current, + ) { + onClick() + } + + Surface( + modifier = clickableModifier, + shape = fixedSize.shape, + color = radioOptionColors.containerColor, + ) { + Row( + modifier = Modifier.padding(fixedSize.contentPadding), + ) { + SelectIndicationCircle(chosenState, lockedState) + Spacer(Modifier.width(8.dp)) + Row(Modifier.weight(1f)) { + optionContent() + } + } + } +} + @Composable fun RadioOption( optionText: String, @@ -248,6 +293,7 @@ internal fun SelectIndicationCircle( NotLocked -> Modifier.border(8.dp, radioOptionColors.chosenIndicatorColor, CircleShape) } } + NotChosen -> { when (lockedState) { Locked -> Modifier.border(8.dp, radioOptionColors.disabledIndicatorColor, CircleShape) diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/tokens/DialogTokens.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/tokens/DialogTokens.kt index 413b60eb31..68f1febba0 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/tokens/DialogTokens.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/tokens/DialogTokens.kt @@ -7,7 +7,7 @@ internal object DialogTokens { val ContainerColor = ColorSchemeKeyTokens.BackgroundPrimary val ContainerShape = ShapeKeyTokens.CornerXLarge val ShadowElevation = 2.dp - val NoButtonsPadding = PaddingValues(24.dp) + val NoButtonsPadding = PaddingValues(16.dp) val BigButtonsPadding = PaddingValues(top = 48.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) val SmallButtonsPadding = PaddingValues(top = 48.dp, bottom = 24.dp, start = 24.dp, end = 24.dp) } diff --git a/app/feature/feature-choose-tier/build.gradle.kts b/app/feature/feature-choose-tier/build.gradle.kts new file mode 100644 index 0000000000..a9e7612bf6 --- /dev/null +++ b/app/feature/feature-choose-tier/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + id("hedvig.android.feature") + id("hedvig.android.ktlint") + id("hedvig.android.library") + id("hedvig.android.library.compose") + alias(libs.plugins.serialization) //todo: maybe not needed at all - check! + alias(libs.plugins.squareSortDependencies) + id("kotlin-parcelize") + alias(libs.plugins.apollo) +} + +android { + testOptions.unitTests.isReturnDefaultValues = true +} + +dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.compose) + implementation(libs.androidx.navigation.common) + implementation(libs.androidx.navigation.compose) + implementation(libs.arrow.core) + implementation(libs.coil.coil) + implementation(libs.coil.compose) + implementation(libs.coil.gif) + implementation(libs.coil.svg) + implementation(libs.coroutines.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.coreUiData) + implementation(projects.dataContractAndroid) + implementation(projects.dataContractPublic) + implementation(projects.dataProductVariantAndroid) + implementation(projects.dataProductVariantPublic) + implementation(projects.designSystemHedvig) + implementation(projects.languageCore) + implementation(projects.moleculeAndroid) + implementation(projects.moleculePublic) + implementation(projects.navigationCompose) + implementation(projects.navigationComposeTyped) + implementation(projects.navigationCore) + implementation(projects.dataChangetier) + implementation(projects.featureFlagsPublic) + + testImplementation(libs.assertK) + testImplementation(libs.coroutines.test) + testImplementation(libs.junit) + testImplementation(libs.turbine) + testImplementation(projects.coreCommonTest) + testImplementation(projects.languageTest) + testImplementation(projects.loggingTest) + testImplementation(projects.moleculeTest) +} + +apollo { + service("octopus") { + packageName = "octopus" + dependsOn(projects.apolloOctopusPublic, true) + } +} diff --git a/app/feature/feature-choose-tier/src/main/AndroidManifest.xml b/app/feature/feature-choose-tier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..568741e54f --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/feature/feature-choose-tier/src/main/graphql/CurrentContractQuery.graphql b/app/feature/feature-choose-tier/src/main/graphql/CurrentContractQuery.graphql new file mode 100644 index 0000000000..ca9c19c015 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/graphql/CurrentContractQuery.graphql @@ -0,0 +1,23 @@ +query CurrentContractsForTierChange { + currentMember { + activeContracts { + id + exposureDisplayName + currentAgreement { + premium { + ...MoneyFragment + } + deductible { + displayText + percentage + amount { + ...MoneyFragment + } + } + productVariant { + ...ProductVariantFragment + } + } + } + } +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/data/GetCurrentContractDataUseCase.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/data/GetCurrentContractDataUseCase.kt new file mode 100644 index 0000000000..2cdc2ec83f --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/data/GetCurrentContractDataUseCase.kt @@ -0,0 +1,88 @@ +package com.hedvig.android.feature.change.tier.data + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.changetier.data.Deductible +import com.hedvig.android.data.productVariant.android.toProductVariant +import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature.TIER +import com.hedvig.android.logger.LogPriority.ERROR +import com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.first +import octopus.CurrentContractsForTierChangeQuery + +internal interface GetCurrentContractDataUseCase { + suspend fun invoke(insuranceId: String): Either +} + +internal class GetCurrentContractDataUseCaseImpl( + private val apolloClient: ApolloClient, + private val featureManager: FeatureManager, +) : GetCurrentContractDataUseCase { + override suspend fun invoke(insuranceId: String): Either { + return either { + val isTierEnabled = featureManager.isFeatureEnabled(TIER).first() + if (!isTierEnabled) { + logcat(ERROR) { "Tried to start Change Tier flow when feature flag is disabled" } + raise(ErrorMessage()) + } else { + val result = apolloClient.query(CurrentContractsForTierChangeQuery()).safeExecute().getOrNull() + if (result == null) { + logcat(ERROR) { "Tried to start Change Tier flow but got error from CurrentContractsQuery" } + raise(ErrorMessage()) + } else { + val dataResult = result.currentMember.activeContracts.firstOrNull { it.id == insuranceId } + if (dataResult == null) { + logcat(ERROR) { "Tried to start Change Tier flow but got null active contract" } + raise(ErrorMessage()) + } else { + val deductible = Deductible( + deductibleAmount = dataResult.currentAgreement.deductible?.amount?.let { + UiMoney.fromMoneyFragment(it) + }, + deductiblePercentage = dataResult.currentAgreement.deductible?.percentage, + description = dataResult.currentAgreement.deductible?.displayText ?: "", + ) + CurrentContractData( + currentExposureName = dataResult.exposureDisplayName, + currentDisplayPremium = UiMoney.fromMoneyFragment(dataResult.currentAgreement.premium), + deductible = deductible, + productVariant = dataResult.currentAgreement.productVariant.toProductVariant(), + ) + } + } + } +// // todo: remove mock!!! +// CurrentContractData( +// currentExposureName = "Testsgatan 555", +// currentDisplayPremium = UiMoney(295.0, SEK), +// deductible = Deductible( +// UiMoney(1000.0, SEK), +// deductiblePercentage = 25, +// description = "En fast del och en rörlig del om 25% av skadekostnaden.", +// ), +// productVariant = ProductVariant( +// displayName = "Test", +// contractGroup = ContractGroup.RENTAL, +// contractType = ContractType.SE_APARTMENT_RENT, +// partner = "test", +// perils = listOf(), +// insurableLimits = listOf(), +// documents = listOf(), +// ), +// ) + } + } +} + +data class CurrentContractData( + val currentExposureName: String, + val currentDisplayPremium: UiMoney, + val deductible: Deductible, + val productVariant: ProductVariant, +) diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/di/ChooseTierModule.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/di/ChooseTierModule.kt new file mode 100644 index 0000000000..0d5d052bce --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/di/ChooseTierModule.kt @@ -0,0 +1,44 @@ +package com.hedvig.android.feature.change.tier.di + +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.data.changetier.data.ChangeTierRepository +import com.hedvig.android.feature.change.tier.data.GetCurrentContractDataUseCase +import com.hedvig.android.feature.change.tier.data.GetCurrentContractDataUseCaseImpl +import com.hedvig.android.feature.change.tier.navigation.InsuranceCustomizationParameters +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonViewModel +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageViewModel +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryViewModel +import com.hedvig.android.featureflags.FeatureManager +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val chooseTierModule = module { + viewModel { params -> + SelectCoverageViewModel( + params = params.get(), + tierRepository = get(), + getCurrentContractDataUseCase = get(), + ) + } + + single { + GetCurrentContractDataUseCaseImpl( + apolloClient = get(), + featureManager = get(), + ) + } + + viewModel { params -> + SummaryViewModel( + quoteId = params.get(), + tierRepository = get(), + ) + } + + viewModel { params -> + ComparisonViewModel( + quoteIds = params.get>(), + tierRepository = get(), + ) + } +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierGraph.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierGraph.kt new file mode 100644 index 0000000000..6b669f1674 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierGraph.kt @@ -0,0 +1,85 @@ +package com.hedvig.android.feature.change.tier.navigation + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonDestination +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonViewModel +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageViewModel +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectTierDestination +import com.hedvig.android.feature.change.tier.ui.stepsummary.ChangeTierSummaryDestination +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryViewModel +import com.hedvig.android.feature.change.tier.ui.sucess.SuccessScreen +import com.hedvig.android.navigation.compose.DestinationNavTypeAware +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.core.Navigator +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import kotlinx.datetime.LocalDate +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +fun NavGraphBuilder.changeTierGraph( + navigator: Navigator, + navController: NavController, + onNavigateToNewConversation: (NavBackStackEntry) -> Unit, + openUrl: (String) -> Unit, // todo: maybe needed for comparison for now? +) { + navgraph( + startDestination = ChooseTierDestination.SelectTierAndDeductible::class, + destinationNavTypeAware = object : DestinationNavTypeAware { + override val typeList: List = listOf(typeOf()) + }, + ) { + navdestination { backStackEntry -> + val chooseTierGraphDestination = navController + .getRouteFromBackStack(backStackEntry) + val viewModel: SelectCoverageViewModel = koinViewModel { + parametersOf(chooseTierGraphDestination.parameters) + } + SelectTierDestination( + viewModel = viewModel, + navigateUp = navigator::navigateUp, + navigateToSummary = { quote -> + navigator.navigateUnsafe(ChooseTierDestination.Summary(quote.id)) // todo: Unsafe??? + }, + navigateToComparison = { listOfQuotes -> + navigator.navigateUnsafe(ChooseTierDestination.Comparison(listOfQuotes.map { it.id })) // todo: Unsafe??? + }, + ) + } + + navdestination { _ -> + val viewModel: ComparisonViewModel = koinViewModel { + parametersOf(this.quoteIds) + } + ComparisonDestination( + viewModel = viewModel, + navigateUp = navigator::navigateUp, + ) + } + + navdestination { backStackEntry -> + val viewModel: SummaryViewModel = koinViewModel { + parametersOf(quoteIdToSubmit) + } + ChangeTierSummaryDestination( + viewModel = viewModel, + navigateUp = navigator::navigateUp, + onNavigateToNewConversation = { + onNavigateToNewConversation(backStackEntry) + }, + openUrl = openUrl, + ) + } + + navdestination { _ -> + SuccessScreen( + LocalDate.fromEpochDays(activationDate), + navigateUp = navigator::navigateUp, + ) + } + } +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierNavDestination.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierNavDestination.kt new file mode 100644 index 0000000000..797fde4010 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/navigation/ChooseTierNavDestination.kt @@ -0,0 +1,92 @@ +package com.hedvig.android.feature.change.tier.navigation + +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import androidx.navigation.NavType +import com.hedvig.android.navigation.compose.Destination +import com.hedvig.android.navigation.compose.DestinationNavTypeAware +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +data class ChooseTierGraphDestination( + /** + * The ID to the contract and the change tier intent info with activation date, current tier level and all quotes + */ + @SerialName("customization_params") + val parameters: InsuranceCustomizationParameters, +) : Destination { + companion object : DestinationNavTypeAware { + override val typeList: List = listOf(typeOf()) + } +} + +internal sealed interface ChooseTierDestination { + @Serializable + data object SelectTierAndDeductible : ChooseTierDestination, Destination + + @Serializable + data class ChangingTierSuccess(val activationDate: Int) : ChooseTierDestination, Destination { + companion object : DestinationNavTypeAware { + override val typeList: List = listOf(typeOf()) + } + } + + @Serializable + data class Comparison(val quoteIds: List) : ChooseTierDestination, Destination { + companion object : DestinationNavTypeAware { + override val typeList: List = listOf(typeOf>()) // todo: quote or list of quotes here? + } + } + + @Serializable + data class Summary( + val quoteIdToSubmit: String, + // todo: also activation date, maybe??? + ) : ChooseTierDestination, Destination { + companion object : DestinationNavTypeAware { + override val typeList: List = listOf(typeOf()) + } + } +} + +@Serializable +@Parcelize +data class InsuranceCustomizationParameters( + val insuranceId: String, + val activationDateEpochDays: Int, + val currentTierLevel: Int?, + val currentTierName: String?, + val quoteIds: List, +) : Parcelable + +class InsuranceCustomizationParametersType() : NavType( + isNullableAllowed = false, +) { + override fun get(bundle: Bundle, key: String): InsuranceCustomizationParameters? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + bundle.getParcelable(key, InsuranceCustomizationParameters::class.java) + } else { + @Suppress("DEPRECATION") + bundle.getParcelable(key) + } + } + + override fun parseValue(value: String): InsuranceCustomizationParameters { + return Json.decodeFromString(value) + } + + override fun serializeAsValue(value: InsuranceCustomizationParameters): String { + return Json.encodeToString(value) + } + + override fun put(bundle: Bundle, key: String, value: InsuranceCustomizationParameters) { + bundle.putParcelable(key, value) + } +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonDestination.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonDestination.kt new file mode 100644 index 0000000000..a31931bd39 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonDestination.kt @@ -0,0 +1,146 @@ +package com.hedvig.android.feature.change.tier.ui.comparison + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.design.system.hedvig.HedvigCircularProgressIndicator +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigTabRowMaxSixTabs +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.PerilData +import com.hedvig.android.design.system.hedvig.PerilDefaults.PerilSize +import com.hedvig.android.design.system.hedvig.PerilList +import com.hedvig.android.design.system.hedvig.TabDefaults +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonState.Loading +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonState.Success +import hedvig.resources.R + +@Composable +internal fun ComparisonDestination(viewModel: ComparisonViewModel, navigateUp: () -> Unit) { + val uiState: ComparisonState by viewModel.uiState.collectAsStateWithLifecycle() + when (val state = uiState) { + Loading -> { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + HedvigCircularProgressIndicator() + } + } + + is Success -> ComparisonScreen(state, navigateUp) + } +} + +@Composable +private fun ComparisonScreen(uiState: Success, navigateUp: () -> Unit) { + HedvigScaffold( + navigateUp = navigateUp, + topAppBarText = stringResource(R.string.TIER_FLOW_COMPARE_BUTTON), // todo: change copy?? + ) { + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + Spacer(Modifier.height(20.dp)) + val titles = uiState.quotes.map { it.tier.tierDisplayName ?: "-" } + CoveragePagerSelector( + selectedTabIndex = selectedTabIndex, + selectTabIndex = { selectedTabIndex = it }, + tabTitles = titles, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + AnimatedContent( + targetState = selectedTabIndex, + transitionSpec = { + val spec = tween(durationMillis = 600, easing = FastOutSlowInEasing) + if (initialState == 0) { + slideIntoContainer(SlideDirection.Start, spec) togetherWith slideOutOfContainer(SlideDirection.Start, spec) + } else { + slideIntoContainer(SlideDirection.End, spec) togetherWith slideOutOfContainer(SlideDirection.End, spec) + } + }, + ) { index -> + // todo: will use different API for this + Column(Modifier.padding(horizontal = 16.dp)) { + val quote = uiState.quotes[index] + quote.productVariant.insurableLimits.forEachIndexed { i, insurableLimit -> + HorizontalItemsWithMaximumSpaceTaken( + modifier = Modifier.padding(vertical = 16.dp), + startSlot = { + HedvigText(insurableLimit.label) + }, + endSlot = { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + HedvigText( + insurableLimit.limit, + color = HedvigTheme.colorScheme.textSecondary, + textAlign = TextAlign.End, + ) + } + }, + spaceBetween = 8.dp, + ) + if (i != quote.productVariant.insurableLimits.lastIndex) { + HorizontalDivider() + } + } + Spacer(Modifier.height(16.dp)) + PerilList( + size = PerilSize.Small, + perilItems = quote.productVariant.perils.map { + PerilData( + title = it.title, + description = it.description, + colorCode = it.colorCode, + covered = it.covered, + ) + }, + ) + } + } + } +} + +@Composable +private fun CoveragePagerSelector( + selectedTabIndex: Int, + selectTabIndex: (Int) -> Unit, + tabTitles: List, + modifier: Modifier = Modifier, +) { + HedvigTabRowMaxSixTabs( + tabTitles = tabTitles, + tabStyle = TabDefaults.TabStyle.Filled, + selectedTabIndex = selectedTabIndex, + onTabChosen = { selectTabIndex(it) }, + modifier = modifier, + ) +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonViewModel.kt new file mode 100644 index 0000000000..6df711befe --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/comparison/ComparisonViewModel.kt @@ -0,0 +1,61 @@ +package com.hedvig.android.feature.change.tier.ui.comparison + +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 com.hedvig.android.data.changetier.data.ChangeTierRepository +import com.hedvig.android.data.changetier.data.TierDeductibleQuote +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonEvent.ShowTab +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonState.Loading +import com.hedvig.android.feature.change.tier.ui.comparison.ComparisonState.Success +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class ComparisonViewModel( + quoteIds: List, + tierRepository: ChangeTierRepository, +) : MoleculeViewModel( + initialState = Loading, + presenter = ComparisonPresenter( + quoteIds = quoteIds, + tierRepository = tierRepository, + ), + ) + +private class ComparisonPresenter( + private val quoteIds: List, + private val tierRepository: ChangeTierRepository, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: ComparisonState): ComparisonState { + var currentState by remember { mutableStateOf(lastState) } + + LaunchedEffect(Unit) { + // TODO: add error state!!! and either! + val result = tierRepository.getQuotesById(quoteIds) + currentState = Success(result) + } + + CollectEvents { event -> + when (event) { + is ShowTab -> TODO() + } + } + + return currentState + } +} + +internal sealed interface ComparisonState { + data object Loading : ComparisonState + + data class Success(val quotes: List) : ComparisonState +} + +internal sealed interface ComparisonEvent { + data class ShowTab(val index: Int) : ComparisonEvent +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectCoverageViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectCoverageViewModel.kt new file mode 100644 index 0000000000..770afffbba --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectCoverageViewModel.kt @@ -0,0 +1,328 @@ +package com.hedvig.android.feature.change.tier.ui.stepcustomize + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap +import com.hedvig.android.data.changetier.data.ChangeTierRepository +import com.hedvig.android.data.changetier.data.Deductible +import com.hedvig.android.data.changetier.data.Tier +import com.hedvig.android.data.changetier.data.TierDeductibleQuote +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.feature.change.tier.data.GetCurrentContractDataUseCase +import com.hedvig.android.feature.change.tier.navigation.InsuranceCustomizationParameters +import com.hedvig.android.feature.change.tier.ui.stepcustomize.FailureReason.GENERAL +import com.hedvig.android.feature.change.tier.ui.stepcustomize.FailureReason.QUOTES_ARE_EMPTY +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ChangeDeductibleForChosenTier +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ChangeDeductibleInDialog +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ChangeTier +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ChangeTierInDialog +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ClearNavigationStep +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.LaunchComparison +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.Reload +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.SubmitChosenQuoteToContinue +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Failure +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Loading +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Success +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class SelectCoverageViewModel( + params: InsuranceCustomizationParameters, + tierRepository: ChangeTierRepository, + getCurrentContractDataUseCase: GetCurrentContractDataUseCase, +) : MoleculeViewModel( + initialState = Loading, + presenter = SelectCoveragePresenter( + params = params, + getCurrentContractDataUseCase = getCurrentContractDataUseCase, + tierRepository = tierRepository, + ), + ) + +private class SelectCoveragePresenter( + private val params: InsuranceCustomizationParameters, + private val tierRepository: ChangeTierRepository, + val getCurrentContractDataUseCase: GetCurrentContractDataUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: SelectCoverageState, + ): SelectCoverageState { + var chosenTier by remember { mutableStateOf(if (lastState is Success) lastState.uiState.chosenTier else null) } + var chosenQuote by remember { mutableStateOf(if (lastState is Success) lastState.uiState.chosenQuote else null) } + var chosenTierInDialog by remember { + mutableStateOf(if (lastState is Success) lastState.uiState.chosenTier else null) + } + var chosenQuoteInDialog by remember { + mutableStateOf(if (lastState is Success) lastState.uiState.chosenQuote else null) + } + var quoteToNavigateFurther by remember { mutableStateOf(null) } + var quotesToCompare by remember { mutableStateOf?>(null) } + + var currentPartialState by remember { mutableStateOf(mapLastStateToPartial(state = lastState)) } + + var currentContractLoadIteration by remember { mutableIntStateOf(0) } + + CollectEvents { event -> + when (event) { + is ChangeDeductibleForChosenTier -> { + chosenQuote = chosenQuoteInDialog + } + + is ChangeTier -> { + val state = currentPartialState + if (state !is PartialUiState.Success) return@CollectEvents + // set newly chosen tier + val locallyChosen = chosenTierInDialog + locallyChosen?.let { local -> + chosenTier = local + // try to pre-choose a quote with the same deductible and newly chosen coverage + // if there is no such quote, the deductible will not be per-chosen + val previouslyChosenDeductible = chosenQuote?.deductible + val quoteWithNewTierOldDeductible = + state.map[local]!!.firstOrNull { it.deductible == previouslyChosenDeductible } + chosenQuote = quoteWithNewTierOldDeductible + } + } + + ClearNavigationStep -> { + quoteToNavigateFurther = null + quotesToCompare = null + } + + SubmitChosenQuoteToContinue -> { + val state = currentPartialState + if (state !is PartialUiState.Success) return@CollectEvents + if (chosenQuote != state.currentActiveQuote) { + quoteToNavigateFurther = chosenQuote + } + } + + Reload -> currentContractLoadIteration++ + + LaunchComparison -> { + if (currentPartialState !is PartialUiState.Success) return@CollectEvents + val filtered = (currentPartialState as PartialUiState.Success).map.values.flatten() + .filter { it.deductible == chosenQuote?.deductible } + quotesToCompare = + filtered + } + + is ChangeDeductibleInDialog -> { + chosenQuoteInDialog = event.quote + } + is ChangeTierInDialog -> { + chosenTierInDialog = event.tier + } + } + } + + LaunchedEffect(currentContractLoadIteration) { + getCurrentContractDataUseCase.invoke(params.insuranceId).fold( + ifLeft = { + currentPartialState = PartialUiState.Failure(GENERAL) + }, + ifRight = { currentContractData -> + val quotesResult: List = tierRepository.getQuotesById(params.quoteIds) + if (quotesResult.isEmpty()) { + currentPartialState = PartialUiState.Failure(QUOTES_ARE_EMPTY) + } else { + val current: TierDeductibleQuote? = + if (params.currentTierName != null && params.currentTierLevel != null) { + TierDeductibleQuote( + id = CURRENT_ID, + deductible = currentContractData.deductible, + tier = Tier( + tierName = params.currentTierName, + tierLevel = params.currentTierLevel, + info = currentContractData.productVariant.displayTierNameLong, + tierDisplayName = currentContractData.productVariant.displayTierName, + ), + productVariant = currentContractData.productVariant, + displayItems = listOf(), + premium = currentContractData.currentDisplayPremium, + ) + } else { + null + } + current?.let { + tierRepository.addQuotesToDb(listOf(it)) + } + val quotes = buildList { + addAll(quotesResult) + current?.let { + add(it) + } + } + // pre-choosing current quote + chosenTier = current?.tier + chosenTierInDialog = current?.tier + chosenQuote = current + chosenQuoteInDialog = current + currentPartialState = PartialUiState.Success( + contractData = ContractData( + activeDisplayPremium = current?.premium.toString(), + contractGroup = current?.productVariant?.contractGroup ?: quotes[0].productVariant.contractGroup, + contractDisplayName = current?.productVariant?.displayName ?: quotes[0].productVariant.displayName, + contractDisplaySubtitle = currentContractData.currentExposureName, + ), + // setting current quote aside for comparison later + currentActiveQuote = current, + // adding current tierName and quote to the list, create map + map = mapQuotesToTiersAndQuotes(quotes), + ) + } + }, + ) + } + return when (currentPartialState) { + is PartialUiState.Failure -> Failure((currentPartialState as PartialUiState.Failure).reason) + PartialUiState.Loading -> Loading + is PartialUiState.Success -> { + Success( + map = (currentPartialState as PartialUiState.Success).map, + currentActiveQuote = (currentPartialState as PartialUiState.Success).currentActiveQuote, + uiState = SelectCoverageSuccessUiState( + isCurrentChosen = chosenQuote == (currentPartialState as PartialUiState.Success).currentActiveQuote, + chosenQuote = chosenQuote, + chosenTier = chosenTier, + tiers = buildListOfTiersAndPremiums( + map = (currentPartialState as PartialUiState.Success).map, + currentDeductible = chosenQuote?.deductible, + ), + quotesForChosenTier = (currentPartialState as PartialUiState.Success).map[chosenTier]!!, + isTierChoiceEnabled = (currentPartialState as PartialUiState.Success).map.keys.size > 1, + contractData = (currentPartialState as PartialUiState.Success).contractData, + quoteToNavigateFurther = quoteToNavigateFurther, + quotesToCompare = quotesToCompare, + chosenInDialogQuote = chosenQuoteInDialog, + chosenInDialogTier = chosenTierInDialog, + ), + ) + } + } + } +} + +private fun buildListOfTiersAndPremiums( + map: SnapshotStateMap>, + currentDeductible: Deductible?, +): List> { + return buildList { + map.keys.forEach { tier -> + // trying to show premium for same deductible in different tier-coverage, + // but if this doesn't work, the lowest for this coverage + val premium = map[tier]!!.firstOrNull { + it.deductible == currentDeductible + }?.premium ?: map[tier]!!.minBy { it.tier.tierLevel }.premium + add(tier to premium.toString()) + } + }.sortedBy { pair -> + pair.first.tierLevel + } +} + +private fun mapQuotesToTiersAndQuotes( + quotes: List, +): SnapshotStateMap> { + val grouped = quotes + .groupBy { + it.tier + } + .map { entry -> + entry.key to entry.value.sortedBy { + it.premium.amount + } + } + val result = mutableStateMapOf(*grouped.toTypedArray()) + return result +} + +internal sealed interface SelectCoverageEvent { + data object SubmitChosenQuoteToContinue : SelectCoverageEvent + + data object ChangeDeductibleForChosenTier : SelectCoverageEvent + + data object ChangeTier : SelectCoverageEvent + + data class ChangeDeductibleInDialog(val quote: TierDeductibleQuote) : SelectCoverageEvent + + data class ChangeTierInDialog(val tier: Tier) : SelectCoverageEvent + + data object LaunchComparison : SelectCoverageEvent + + data object ClearNavigationStep : SelectCoverageEvent + + data object Reload : SelectCoverageEvent +} + +private fun mapLastStateToPartial(state: SelectCoverageState): PartialUiState { + return when (state) { + Loading -> PartialUiState.Loading + is Failure -> PartialUiState.Failure(state.reason) + is Success -> PartialUiState.Success( + contractData = state.uiState.contractData, + currentActiveQuote = state.currentActiveQuote, + map = state.map, + ) + } +} + +private sealed interface PartialUiState { + data object Loading : PartialUiState + + data class Failure(val reason: FailureReason) : PartialUiState + + data class Success( + val contractData: ContractData, + val currentActiveQuote: TierDeductibleQuote?, + val map: SnapshotStateMap>, + ) : PartialUiState +} + +internal sealed interface SelectCoverageState { + data object Loading : SelectCoverageState + + data class Success( + val uiState: SelectCoverageSuccessUiState, + val currentActiveQuote: TierDeductibleQuote?, + val map: SnapshotStateMap>, + ) : SelectCoverageState + + data class Failure(val reason: FailureReason) : SelectCoverageState +} + +internal enum class FailureReason { + GENERAL, + QUOTES_ARE_EMPTY, +} + +internal data class SelectCoverageSuccessUiState( + val contractData: ContractData, + val chosenTier: Tier?, + val chosenQuote: TierDeductibleQuote?, + val chosenInDialogTier: Tier?, + val chosenInDialogQuote: TierDeductibleQuote?, + val isCurrentChosen: Boolean, + val isTierChoiceEnabled: Boolean, + val quoteToNavigateFurther: TierDeductibleQuote? = null, + val quotesToCompare: List? = null, + val tiers: List>, // sorted list of tiers with corresponding premiums (depending on selected deductible) + val quotesForChosenTier: List, +) + +internal data class ContractData( + val contractGroup: ContractGroup, + val contractDisplayName: String, + val contractDisplaySubtitle: String, + val activeDisplayPremium: String?, +) + +private const val CURRENT_ID = "current" diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectTierDestination.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectTierDestination.kt new file mode 100644 index 0000000000..08318b5351 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepcustomize/SelectTierDestination.kt @@ -0,0 +1,754 @@ +package com.hedvig.android.feature.change.tier.ui.stepcustomize + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextAlign.Companion +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.core.uidata.UiCurrencyCode.SEK +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.changetier.data.Deductible +import com.hedvig.android.data.changetier.data.Tier +import com.hedvig.android.data.changetier.data.TierDeductibleQuote +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.contract.android.toPillow +import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large +import com.hedvig.android.design.system.hedvig.ChosenState +import com.hedvig.android.design.system.hedvig.ChosenState.Chosen +import com.hedvig.android.design.system.hedvig.ChosenState.NotChosen +import com.hedvig.android.design.system.hedvig.DropdownDefaults.DropdownSize.Small +import com.hedvig.android.design.system.hedvig.DropdownDefaults.DropdownStyle.Label +import com.hedvig.android.design.system.hedvig.DropdownItem.SimpleDropdownItem +import com.hedvig.android.design.system.hedvig.DropdownWithDialog +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCircularProgressIndicator +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigMultiScreenPreview +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.HedvigTextButton +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighLightSize +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.MEDIUM +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.RadioOption +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.icon.Close +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.feature.change.tier.ui.stepcustomize.FailureReason.GENERAL +import com.hedvig.android.feature.change.tier.ui.stepcustomize.FailureReason.QUOTES_ARE_EMPTY +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageEvent.ClearNavigationStep +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Failure +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Loading +import com.hedvig.android.feature.change.tier.ui.stepcustomize.SelectCoverageState.Success +import hedvig.resources.R + +@Composable +internal fun SelectTierDestination( + viewModel: SelectCoverageViewModel, + navigateUp: () -> Unit, + navigateToSummary: (quote: TierDeductibleQuote) -> Unit, + navigateToComparison: (listOfQuotes: List) -> Unit, +) { + val uiState: SelectCoverageState by viewModel.uiState.collectAsStateWithLifecycle() + Box( + Modifier.fillMaxSize(), + ) { + when (val state = uiState) { + is Failure -> FailureScreen( + reason = state.reason, + reload = { + viewModel.emit(SelectCoverageEvent.Reload) + }, + navigateUp = navigateUp, + ) + + Loading -> LoadingScreen() + is Success -> { + LaunchedEffect(state.uiState.quoteToNavigateFurther) { + if (state.uiState.quoteToNavigateFurther != null) { + viewModel.emit(ClearNavigationStep) + navigateToSummary(state.uiState.quoteToNavigateFurther) // todo: check here + } + } + LaunchedEffect(state.uiState.quotesToCompare) { + if (state.uiState.quotesToCompare != null) { + viewModel.emit(ClearNavigationStep) + navigateToComparison(state.uiState.quotesToCompare) // todo: check here + } + } + SelectTierScreen( + uiState = state.uiState, + navigateUp = navigateUp, + onCompareClick = { + viewModel.emit(SelectCoverageEvent.LaunchComparison) + }, + onContinueClick = { + viewModel.emit(SelectCoverageEvent.SubmitChosenQuoteToContinue) + }, + onChooseTierClick = { + viewModel.emit(SelectCoverageEvent.ChangeTier) + }, + onChooseDeductibleClick = { + viewModel.emit(SelectCoverageEvent.ChangeDeductibleForChosenTier) + }, + onChooseDeductibleInDialogClick = { quote -> + viewModel.emit(SelectCoverageEvent.ChangeDeductibleInDialog(quote)) + }, + onChooseTierInDialogClick = { tier -> + viewModel.emit(SelectCoverageEvent.ChangeTierInDialog(tier)) + }, + ) + } + } + } +} + +@Composable +private fun FailureScreen(reload: () -> Unit, navigateUp: () -> Unit, reason: FailureReason) { + Box(Modifier.fillMaxSize()) { + val subTitle = when (reason) { + GENERAL -> stringResource(R.string.GENERAL_ERROR_BODY) + QUOTES_ARE_EMPTY -> "Turns out you're already at the best possible coverage and price!" + // todo: remove hardcoded string!! + } + val action = when (reason) { + GENERAL -> reload + QUOTES_ARE_EMPTY -> navigateUp + } + val title = when (reason) { + GENERAL -> stringResource(R.string.GENERAL_ERROR_BODY) + QUOTES_ARE_EMPTY -> "Oops!" // todo: another copy?? + } + val buttonText = when (reason) { + GENERAL -> stringResource(R.string.GENERAL_ERROR_BODY) + QUOTES_ARE_EMPTY -> stringResource(R.string.general_back_button) + // todo: remove hardcoded string!! + } + HedvigErrorSection( + onButtonClick = action, + modifier = Modifier.fillMaxSize(), + subTitle = subTitle, + title = title, + buttonText = buttonText, + ) + } +} + +@Composable +private fun LoadingScreen() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + HedvigCircularProgressIndicator() + } +} + +@Composable +private fun SelectTierScreen( + uiState: SelectCoverageSuccessUiState, + navigateUp: () -> Unit, + onCompareClick: () -> Unit, + onContinueClick: () -> Unit, + onChooseDeductibleClick: () -> Unit, + onChooseTierClick: () -> Unit, + onChooseDeductibleInDialogClick: (quote: TierDeductibleQuote) -> Unit, + onChooseTierInDialogClick: (tier: Tier) -> Unit, +) { + HedvigScaffold( + navigateUp = navigateUp, + topAppBarText = "", + topAppBarActions = { + IconButton( + modifier = Modifier.size(24.dp), + onClick = { navigateUp() }, + content = { + Icon( + imageVector = HedvigIcons.Close, + contentDescription = null, + ) + }, + ) + }, + ) { + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + text = stringResource(R.string.TIER_FLOW_TITLE), + style = HedvigTheme.typography.headlineMedium, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + HedvigText( + style = HedvigTheme.typography.headlineMedium.copy( + lineBreak = LineBreak.Heading, + color = HedvigTheme.colorScheme.textSecondary, + ), + text = stringResource(R.string.TIER_FLOW_SUBTITLE), + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(16.dp)) + CustomizationCard( + modifier = Modifier.padding(horizontal = 16.dp), + data = uiState.contractData, + onChooseTierClick = onChooseTierClick, + onChooseDeductibleClick = onChooseDeductibleClick, + newDisplayPremium = uiState.chosenQuote?.premium, + isCurrentChosen = uiState.isCurrentChosen, + chosenTier = uiState.chosenTier, + chosenQuote = uiState.chosenQuote, + isTierChoiceEnabled = uiState.isTierChoiceEnabled, + quotesForChosenTier = uiState.quotesForChosenTier, + tiers = uiState.tiers, + chosenTierInDialog = uiState.chosenInDialogTier, + chosenQuoteInDialog = uiState.chosenInDialogQuote, + onChooseDeductibleInDialogClick = onChooseDeductibleInDialogClick, + onChooseTierInDialogClick = onChooseTierInDialogClick, + ) + Spacer(Modifier.height(4.dp)) + HedvigTextButton( + buttonSize = Large, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.TIER_FLOW_COMPARE_BUTTON), + onClick = { + onCompareClick() + }, + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + buttonSize = Large, + text = stringResource(R.string.general_continue_button), + enabled = !uiState.isCurrentChosen, + onClick = { + onContinueClick() + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun CustomizationCard( + data: ContractData, + tiers: List>, + quotesForChosenTier: List, + chosenTier: Tier?, + chosenQuote: TierDeductibleQuote?, + chosenTierInDialog: Tier?, + chosenQuoteInDialog: TierDeductibleQuote?, + onChooseDeductibleInDialogClick: (quote: TierDeductibleQuote) -> Unit, + onChooseTierInDialogClick: (tier: Tier) -> Unit, + newDisplayPremium: UiMoney?, + isTierChoiceEnabled: Boolean, + onChooseDeductibleClick: () -> Unit, + onChooseTierClick: () -> Unit, + isCurrentChosen: Boolean, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = HedvigTheme.shapes.cornerXLarge, + ) { + Column(Modifier.padding(16.dp)) { + PillAndBasicInfo( + contractGroup = data.contractGroup, + displayName = data.contractDisplayName, + displaySubtitle = data.contractDisplaySubtitle, + ) + Spacer(Modifier.height(16.dp)) + val tierSimpleItems = buildList { + for (tier in tiers) { + add(SimpleDropdownItem(tier.first.tierDisplayName ?: "-")) // todo: what if they are null?? + } + } + DropdownWithDialog( + dialogProperties = DialogProperties(usePlatformDefaultWidth = false), + isEnabled = isTierChoiceEnabled, + style = Label( + label = stringResource(R.string.TIER_FLOW_COVERAGE_LABEL), + items = tierSimpleItems, + ), + size = Small, + hintText = stringResource(R.string.TIER_FLOW_COVERAGE_PLACEHOLDER), + onItemChosen = { _ -> }, // not needed, as we not use the default dialog content + chosenItemIndex = tiers.indexOfFirst { it.first.tierLevel == chosenTier?.tierLevel }, + onSelectorClick = {}, + containerColor = HedvigTheme.colorScheme.fillNegative, + ) { onDismissRequest -> + val listOfOptions = buildList { + tiers.forEachIndexed { index, pair -> + add( + ExpandedRadioOptionData( + chosenState = if (chosenTierInDialog == pair.first) Chosen else NotChosen, + title = pair.first.tierDisplayName ?: "-", // todo: what if they are null?? + premium = pair.second, + info = pair.first.info, + onRadioOptionClick = { + onChooseTierInDialogClick(pair.first) + }, + ), + ) + } + } + DropdownContent( + onContinueButtonClick = { + onChooseTierClick() + onDismissRequest() + }, + onCancelButtonClick = { + onDismissRequest() + }, + title = stringResource(R.string.TIER_FLOW_SELECT_COVERAGE_TITLE), + data = listOfOptions, + subTitle = stringResource(R.string.TIER_FLOW_SELECT_COVERAGE_SUBTITLE), + ) + } + if (!isTierChoiceEnabled) { + HedvigText( + stringResource(R.string.TIER_FLOW_LOCKED_INFO_DESCRIPTION), + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp, top = 4.dp), + ) + } + if (quotesForChosenTier.isNotEmpty()) { + Spacer(Modifier.height(4.dp)) + val deductibleSimpleItems = buildList { + for (quote in quotesForChosenTier) { + quote.deductible?.let { + add(SimpleDropdownItem(it.optionText)) + } + } + } + DropdownWithDialog( + dialogProperties = DialogProperties(usePlatformDefaultWidth = false), + style = Label( + label = stringResource(R.string.TIER_FLOW_DEDUCTIBLE_LABEL), + items = deductibleSimpleItems, + ), + isEnabled = quotesForChosenTier.size > 1, + size = Small, + hintText = stringResource(R.string.TIER_FLOW_DEDUCTIBLE_PLACEHOLDER), + onItemChosen = { _ -> }, // not needed, as we not use the default dialog content, + chosenItemIndex = quotesForChosenTier.indexOfFirst { it == chosenQuote }, + onSelectorClick = {}, + containerColor = HedvigTheme.colorScheme.fillNegative, + ) { onDismissRequest -> + val listOfOptions = buildList { + quotesForChosenTier.forEach { quote -> + quote.deductible?.let { + add( + ExpandedRadioOptionData( + chosenState = if (chosenQuoteInDialog == quote) Chosen else NotChosen, + title = it.optionText, + premium = quote.premium.toString(), + info = it.description, + onRadioOptionClick = { + onChooseDeductibleInDialogClick(quote) + }, + ), + ) + } + } + } + DropdownContent( + onContinueButtonClick = { + onChooseDeductibleClick() + onDismissRequest() + }, + onCancelButtonClick = { + onDismissRequest() + }, + title = stringResource(R.string.TIER_FLOW_SELECT_DEDUCTIBLE_TITLE), + data = listOfOptions, + subTitle = stringResource(R.string.TIER_FLOW_SELECT_DEDUCTIBLE_SUBTITLE), + ) + } + } + Spacer(Modifier.height(16.dp)) + HorizontalItemsWithMaximumSpaceTaken( + startSlot = { + HedvigText( + stringResource(R.string.TIER_FLOW_TOTAL), + style = HedvigTheme.typography.bodySmall, + ) + }, + spaceBetween = 8.dp, + endSlot = { + HedvigText( + text = newDisplayPremium.toString(), + textAlign = TextAlign.End, + style = HedvigTheme.typography.bodySmall, + ) + }, + ) + if (!isCurrentChosen && data.activeDisplayPremium != null) { + HedvigText( + modifier = Modifier.fillMaxWidth(), + textAlign = Companion.End, + text = stringResource(R.string.TIER_FLOW_PREVIOUS_PRICE, data.activeDisplayPremium), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + } +} + +@Composable +private fun DropdownContent( + title: String, + subTitle: String, + onContinueButtonClick: () -> Unit, + onCancelButtonClick: () -> Unit, + data: List, + modifier: Modifier = Modifier, +) { + Column( + modifier + .padding(16.dp) + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(16.dp)) + HedvigText( + title, + modifier = Modifier.fillMaxWidth(), + textAlign = Companion.Center, + ) + HedvigText( + subTitle, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.fillMaxWidth(), + textAlign = Companion.Center, + ) + Spacer(Modifier.height(24.dp)) + data.forEachIndexed { index, option -> + RadioOption( + chosenState = option.chosenState, + onClick = option.onRadioOptionClick, + optionContent = { + ExpandedOptionContent( + title = option.title, + premium = option.premium, + comment = option.info, + ) + }, + ) + if (index != data.lastIndex) { + Spacer(Modifier.height(4.dp)) + } + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = stringResource(R.string.general_continue_button), + onClick = onContinueButtonClick, + modifier = Modifier.fillMaxWidth(), + enabled = true, + ) + Spacer(Modifier.height(8.dp)) + HedvigTextButton( + text = stringResource(R.string.general_cancel_button), + onClick = onCancelButtonClick, + modifier = Modifier.fillMaxWidth(), + buttonSize = Large, + ) + } +} + +private data class ExpandedRadioOptionData( + val onRadioOptionClick: () -> Unit, + val chosenState: ChosenState, + val title: String, + val premium: String, + val info: String?, +) + +@Composable +private fun PillAndBasicInfo(contractGroup: ContractGroup, displayName: String, displaySubtitle: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = contractGroup.toPillow()), + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + HedvigText( + text = displayName, + style = HedvigTheme.typography.headlineSmall, + ) + HedvigText( + color = HedvigTheme.colorScheme.textSecondary, + text = displaySubtitle, + style = HedvigTheme.typography.bodySmall, + ) + } + } +} + +@Composable +private fun ExpandedOptionContent(title: String, premium: String, comment: String?) { + Column { + HorizontalItemsWithMaximumSpaceTaken( + startSlot = { + HedvigText(title) + }, + spaceBetween = 8.dp, + endSlot = { + Row(horizontalArrangement = Arrangement.End) { + HighlightLabel(labelText = premium, size = HighLightSize.Small, color = HighlightColor.Grey(MEDIUM)) + } + }, + ) + if (comment != null) { + Spacer(Modifier.height(8.dp)) + HedvigText( + text = comment, + modifier = Modifier.fillMaxWidth(), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } +} + +@HedvigPreview +@Composable +private fun CustomizationCardPreview() { + HedvigTheme { + CustomizationCard( + data = dataForPreview, + chosenTier = Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ), + onChooseTierClick = {}, + onChooseDeductibleClick = {}, + newDisplayPremium = UiMoney(199.0, SEK), + isCurrentChosen = false, + isTierChoiceEnabled = true, + chosenQuote = quotesForPreview[0], + quotesForChosenTier = quotesForPreview, + tiers = listOf( + Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ) to "199", + Tier( + "STANDARD", + tierLevel = 1, + info = "Vårt mellanpaket med hög ersättning.", + tierDisplayName = "Standard", + ) to "155", + ), + chosenTierInDialog = Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ), + chosenQuoteInDialog = quotesForPreview[0], + onChooseDeductibleInDialogClick = {}, + onChooseTierInDialogClick = {}, + ) + } +} + +@HedvigMultiScreenPreview +@Composable +private fun SelectTierScreenPreview() { + HedvigTheme { + SelectTierScreen( + uiState = SelectCoverageSuccessUiState( + contractData = dataForPreview, + tiers = listOf( + Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ) to "199", + Tier( + "STANDARD", + tierLevel = 1, + info = "Vårt mellanpaket med hög ersättning.", + tierDisplayName = "Standard", + ) to "155", + ), + quotesForChosenTier = quotesForPreview, + isCurrentChosen = false, + isTierChoiceEnabled = true, + chosenTier = Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ), + chosenQuote = quotesForPreview[0], + chosenInDialogTier = Tier( + "BAS", + tierLevel = 0, + info = "Vårt paket med grundläggande villkor.", + tierDisplayName = "Bas", + ), + chosenInDialogQuote = quotesForPreview[0], + ), + {}, + {}, + {}, + {}, + {}, + {}, + {}, + ) + } +} + +private val dataForPreview = ContractData( + contractGroup = ContractGroup.HOMEOWNER, + contractDisplayName = "Home Homeowner", + contractDisplaySubtitle = "Addressvägen 777", + activeDisplayPremium = "449 kr/mo", +) + +private val quotesForPreview = listOf( + TierDeductibleQuote( + id = "id0", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(199.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id1", + deductible = Deductible( + UiMoney(1000.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(255.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id2", + deductible = Deductible( + UiMoney(3500.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden", + ), + displayItems = listOf(), + premium = UiMoney(355.0, SEK), + tier = Tier("BAS", tierLevel = 0, info = "Vårt paket med grundläggande villkor.", tierDisplayName = "Bas"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id3", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf(), + premium = UiMoney(230.0, SEK), + tier = Tier("STANDARD", tierLevel = 1, info = "Vårt mellanpaket med hög ersättning.", tierDisplayName = "Standard"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), + TierDeductibleQuote( + id = "id4", + deductible = Deductible( + UiMoney(3500.0, SEK), + deductiblePercentage = 25, + description = "En fast del och en rörlig del om 25% av skadekostnaden", + ), + displayItems = listOf(), + premium = UiMoney(655.0, SEK), + tier = Tier("STANDARD", tierLevel = 1, info = "Vårt mellanpaket med hög ersättning.", tierDisplayName = "Standard"), + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + ), + ), +) diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryDestination.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryDestination.kt new file mode 100644 index 0000000000..f2e855e37a --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryDestination.kt @@ -0,0 +1,18 @@ +package com.hedvig.android.feature.change.tier.ui.stepsummary + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import com.hedvig.android.design.system.hedvig.HedvigText + +@Composable +internal fun ChangeTierSummaryDestination( + viewModel: SummaryViewModel, + navigateUp: () -> Unit, + onNavigateToNewConversation: () -> Unit, + openUrl: (String) -> Unit, +) { + Box(contentAlignment = Alignment.Center) { + HedvigText("Summary") + } +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt new file mode 100644 index 0000000000..4d8d736f85 --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt @@ -0,0 +1,62 @@ +package com.hedvig.android.feature.change.tier.ui.stepsummary + +import androidx.compose.runtime.Composable +import com.hedvig.android.data.changetier.data.ChangeTierRepository +import com.hedvig.android.data.changetier.data.TierDeductibleQuote +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryEvent.ExpandCard +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryEvent.Reload +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryEvent.ScrollToDetails +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryEvent.SubmitQuote +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryState.Loading +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class SummaryViewModel( + quoteId: String, + tierRepository: ChangeTierRepository, +) : MoleculeViewModel( + initialState = Loading, + presenter = SummaryPresenter( + quoteId = quoteId, + tierRepository = tierRepository, + ), + ) + +private class SummaryPresenter( + private val quoteId: String, + private val tierRepository: ChangeTierRepository, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: SummaryState): SummaryState { + CollectEvents { event -> + when (event) { + ExpandCard -> TODO() + Reload -> TODO() + ScrollToDetails -> TODO() + SubmitQuote -> TODO() + } + } + TODO("Not yet implemented") + } +} + +internal sealed interface SummaryState { + data object Loading : SummaryState + + data class Success( + val quote: TierDeductibleQuote, + ) : SummaryState + + data object Failure : SummaryState +} + +internal sealed interface SummaryEvent { + data object SubmitQuote : SummaryEvent + + data object ScrollToDetails : SummaryEvent + + data object ExpandCard : SummaryEvent + + data object Reload : SummaryEvent +} diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/sucess/SuccessScreen.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/sucess/SuccessScreen.kt new file mode 100644 index 0000000000..d5fc750f8b --- /dev/null +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/sucess/SuccessScreen.kt @@ -0,0 +1,10 @@ +package com.hedvig.android.feature.change.tier.ui.sucess + +import androidx.compose.runtime.Composable +import com.hedvig.android.design.system.hedvig.HedvigText +import kotlinx.datetime.LocalDate + +@Composable +internal fun SuccessScreen(activationDate: LocalDate, navigateUp: () -> Unit) { + HedvigText("Activation date: $activationDate") +} 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 2ce9fd7ffe..35b36d991c 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 @@ -35,7 +35,8 @@ internal class GetInsuranceContractsUseCaseImpl( .safeFlow(::ErrorMessage), featureManager.isFeatureEnabled(Feature.EDIT_COINSURED), featureManager.isFeatureEnabled(Feature.MOVING_FLOW), - ) { insuranceQueryResponse, isEditCoInsuredEnabled, isMovingFlowEnabled -> + featureManager.isFeatureEnabled(Feature.TIER), + ) { insuranceQueryResponse, isEditCoInsuredEnabled, isMovingFlowEnabled, isTierEnabled -> either { val insuranceQueryData = insuranceQueryResponse.bind() val contractHolderDisplayName = insuranceQueryData.getContractHolderDisplayName() @@ -48,6 +49,7 @@ internal class GetInsuranceContractsUseCaseImpl( contractHolderSSN = contractHolderSSN, isEditCoInsuredEnabled = isEditCoInsuredEnabled, isMovingFlowEnabled = isMovingFlowEnabled, + isTierEnabled = isTierEnabled, ) } val activeContracts = insuranceQueryData.currentMember.activeContracts.map { @@ -57,6 +59,7 @@ internal class GetInsuranceContractsUseCaseImpl( contractHolderSSN = contractHolderSSN, isEditCoInsuredEnabled = isEditCoInsuredEnabled, isMovingFlowEnabled = isMovingFlowEnabled, + isTierEnabled = isTierEnabled, ) } terminatedContracts + activeContracts @@ -76,9 +79,11 @@ private fun ContractFragment.toContract( contractHolderSSN: String?, isEditCoInsuredEnabled: Boolean, isMovingFlowEnabled: Boolean, + isTierEnabled: Boolean, ): InsuranceContract { return InsuranceContract( id = id, + tierName = if (isTierEnabled) currentAgreement.productVariant.displayNameTier else null, displayName = currentAgreement.productVariant.displayName, contractHolderDisplayName = contractHolderDisplayName, contractHolderSSN = contractHolderSSN, 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 7865e4e2dd..f9b4761ad4 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 @@ -21,6 +21,7 @@ data class InsuranceContract( val supportsAddressChange: Boolean, val supportsEditCoInsured: Boolean, val isTerminated: Boolean, + val tierName: String? = null, ) data class InsuranceAgreement( 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 5dd968c67d..0ed6682c9a 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 @@ -50,10 +50,6 @@ import com.hedvig.android.core.designsystem.component.card.HedvigCard import com.hedvig.android.core.designsystem.component.error.HedvigErrorSection import com.hedvig.android.core.designsystem.component.information.HedvigInformationSection import com.hedvig.android.core.designsystem.material3.squircleMedium -import com.hedvig.android.core.designsystem.preview.HedvigPreview -import com.hedvig.android.core.designsystem.theme.HedvigTheme -import com.hedvig.android.core.ui.card.InsuranceCard -import com.hedvig.android.core.ui.card.InsuranceCardPlaceholder import com.hedvig.android.core.ui.preview.rememberPreviewImageLoader import com.hedvig.android.crosssells.CrossSellItemPlaceholder import com.hedvig.android.crosssells.CrossSellsSection @@ -62,8 +58,11 @@ import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.contract.android.CrossSell import com.hedvig.android.data.productvariant.ProductVariant import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.InsuranceCard +import com.hedvig.android.design.system.hedvig.InsuranceCardPlaceholder import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.feature.insurances.data.InsuranceAgreement @@ -300,7 +299,6 @@ private fun InsuranceCard( .clickable { onInsuranceCardClick(contract.id) }, - shape = MaterialTheme.shapes.squircleMedium, fallbackPainter = contract.createPainter(), isLoading = false, ) @@ -387,8 +385,8 @@ private fun PreviewInsuranceScreen( @Composable private fun PreviewInsuranceDestinationAnimation() { val values = InsuranceUiStateProvider().values.toList() - com.hedvig.android.core.designsystem.theme.HedvigTheme { - Surface(color = MaterialTheme.colorScheme.background) { + HedvigTheme { + Surface { PreviewContentWithProvidedParametersAnimatedOnClick( parametersList = values, content = { insuranceUiState -> @@ -526,4 +524,5 @@ private val previewInsurance = InsuranceContract( isTerminated = false, contractHolderDisplayName = "Hhhhh Hhhhh", contractHolderSSN = "19910913-1893", + tierName = "Bas", ) 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 9cc4ca89ec..9ded78bc2f 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 @@ -46,17 +46,19 @@ import com.hedvig.android.core.designsystem.component.error.HedvigErrorSection import com.hedvig.android.core.designsystem.preview.HedvigPreview import com.hedvig.android.core.designsystem.theme.HedvigTheme import com.hedvig.android.core.ui.appbar.m3.TopAppBarWithBack -import com.hedvig.android.core.ui.card.InsuranceCard -import com.hedvig.android.core.ui.card.InsuranceCardPlaceholder import com.hedvig.android.core.ui.plus import com.hedvig.android.core.ui.preview.rememberPreviewImageLoader -import com.hedvig.android.data.contract.ContractGroup -import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.contract.ContractGroup.RENTAL +import com.hedvig.android.data.contract.ContractType.SE_APARTMENT_RENT import com.hedvig.android.data.productvariant.InsuranceVariantDocument import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.design.system.hedvig.InsuranceCard +import com.hedvig.android.design.system.hedvig.InsuranceCardPlaceholder import com.hedvig.android.feature.insurances.data.CancelInsuranceData import com.hedvig.android.feature.insurances.data.InsuranceAgreement +import com.hedvig.android.feature.insurances.data.InsuranceAgreement.CreationCause.NEW_CONTRACT import com.hedvig.android.feature.insurances.data.InsuranceContract +import com.hedvig.android.feature.insurances.insurancedetail.ContractDetailsUiState.Success import com.hedvig.android.feature.insurances.insurancedetail.coverage.CoverageTab import com.hedvig.android.feature.insurances.insurancedetail.documents.DocumentsTab import com.hedvig.android.feature.insurances.insurancedetail.yourinfo.YourInfoTab @@ -324,10 +326,11 @@ private fun PreviewContractDetailScreen() { HedvigTheme { Surface(color = MaterialTheme.colorScheme.background) { ContractDetailScreen( - uiState = ContractDetailsUiState.Success( + uiState = Success( InsuranceContract( "1", "Test123", + tierName = "Premium", exposureDisplayName = "Test exposure", inceptionDate = LocalDate.fromEpochDays(200), terminationDate = LocalDate.fromEpochDays(400), @@ -337,8 +340,8 @@ private fun PreviewContractDetailScreen() { displayItems = listOf(), productVariant = ProductVariant( displayName = "Variant", - contractGroup = ContractGroup.RENTAL, - contractType = ContractType.SE_APARTMENT_RENT, + contractGroup = RENTAL, + contractType = SE_APARTMENT_RENT, partner = null, perils = listOf(), insurableLimits = listOf(), @@ -346,7 +349,7 @@ private fun PreviewContractDetailScreen() { ), certificateUrl = null, coInsured = listOf(), - creationCause = InsuranceAgreement.CreationCause.NEW_CONTRACT, + creationCause = NEW_CONTRACT, ), upcomingInsuranceAgreement = null, renewalDate = LocalDate.fromEpochDays(500), diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/coverage/CoverageTab.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/coverage/CoverageTab.kt index 979cb608e8..0a0f79f9c9 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/coverage/CoverageTab.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/coverage/CoverageTab.kt @@ -137,7 +137,7 @@ private fun ColumnScope.PerilSection(perilItems: List) { expandedItemIndex = index } }, - color = perilItem.colorCode?.color, + color = perilItem.colorCode?.color(), title = perilItem.title, expandedTitle = perilItem.description, expandedDescriptionList = perilItem.covered, @@ -149,8 +149,13 @@ private fun ColumnScope.PerilSection(perilItems: List) { } } -private val String.color - get() = Color(android.graphics.Color.parseColor(this)) +private fun String.color(): Color { + return try { + Color(android.graphics.Color.parseColor(this)) + } catch (e: Exception) { + Color.Black // todo: was crushing in dev, possibly terms migration not finished + } +} @Composable private fun ExpandableCoverageCard( 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 3b2b43b36c..2ec6100163 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 @@ -21,12 +21,12 @@ import com.hedvig.android.core.designsystem.component.progress.HedvigFullScreenC import com.hedvig.android.core.designsystem.material3.squircleMedium import com.hedvig.android.core.designsystem.preview.HedvigPreview import com.hedvig.android.core.designsystem.theme.HedvigTheme -import com.hedvig.android.core.ui.card.InsuranceCard import com.hedvig.android.core.ui.preview.rememberPreviewImageLoader import com.hedvig.android.core.ui.scaffold.HedvigScaffold import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.design.system.hedvig.InsuranceCard import com.hedvig.android.feature.insurances.data.InsuranceAgreement import com.hedvig.android.feature.insurances.data.InsuranceContract import com.hedvig.android.feature.insurances.ui.createChips @@ -83,7 +83,6 @@ private fun TerminatedContractsScreen( topText = contract.currentInsuranceAgreement.productVariant.displayName, bottomText = contract.exposureDisplayName, imageLoader = imageLoader, - shape = MaterialTheme.shapes.squircleMedium, modifier = Modifier .padding(horizontal = 16.dp) .clip(MaterialTheme.shapes.squircleMedium) @@ -126,6 +125,7 @@ private class PreviewTerminatedContractsUiStateProvider : InsuranceContract( "1", "Test123", + tierName = "Premium", exposureDisplayName = "Test exposure", inceptionDate = LocalDate.fromEpochDays(200), terminationDate = LocalDate.fromEpochDays(400), diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/ui/InsuranceContractExt.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/ui/InsuranceContractExt.kt index 2e4f033cf3..aaf409e4f3 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/ui/InsuranceContractExt.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/ui/InsuranceContractExt.kt @@ -8,6 +8,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.hedvig.android.data.contract.android.toDrawableRes import com.hedvig.android.data.contract.isTrialContract +import com.hedvig.android.design.system.hedvig.ChipType.GENERAL +import com.hedvig.android.design.system.hedvig.ChipType.TIER +import com.hedvig.android.design.system.hedvig.ChipUiData import com.hedvig.android.feature.insurances.data.InsuranceContract import hedvig.resources.R import kotlinx.datetime.Clock @@ -15,11 +18,14 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn @Composable -internal fun InsuranceContract.createChips(): List { +internal fun InsuranceContract.createChips(): List { val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) return listOfNotNull( + tierName?.let { + ChipUiData(chipText = it, chipType = TIER) + }, terminationDate?.let { terminationDate -> - if (terminationDate == today) { + val text = if (terminationDate == today) { if (currentInsuranceAgreement.productVariant.contractType.isTrialContract()) { stringResource(R.string.CONTRACTS_TRIAL_TERMINATION_DATE_MESSAGE_TOMORROW) } else { @@ -34,15 +40,19 @@ internal fun InsuranceContract.createChips(): List { stringResource(R.string.CONTRACT_STATUS_TO_BE_TERMINATED, terminationDate) } } + ChipUiData(text, GENERAL) }, upcomingInsuranceAgreement?.activeFrom?.let { activeFromDate -> - stringResource(R.string.DASHBOARD_INSURANCE_STATUS_ACTIVE_UPDATE_DATE, activeFromDate) + val text = stringResource(R.string.DASHBOARD_INSURANCE_STATUS_ACTIVE_UPDATE_DATE, activeFromDate) + ChipUiData(text, GENERAL) }, inceptionDate.let { inceptionDate -> if (inceptionDate > today) { - stringResource(R.string.CONTRACT_STATUS_ACTIVE_IN_FUTURE, inceptionDate) + val text = stringResource(R.string.CONTRACT_STATUS_ACTIVE_IN_FUTURE, inceptionDate) + ChipUiData(text, GENERAL) } else if (terminationDate == null) { - stringResource(id = R.string.DASHBOARD_INSURANCE_STATUS_ACTIVE) + val text = stringResource(id = R.string.DASHBOARD_INSURANCE_STATUS_ACTIVE) + ChipUiData(text, GENERAL) } else { null } 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 a46db94ee9..729fd9f661 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 @@ -87,6 +87,7 @@ internal fun ProfileDestination( viewModel: ProfileViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ProfileScreen( uiState = uiState, reload = { viewModel.emit(ProfileUiEvent.Reload) }, @@ -205,7 +206,8 @@ private fun ProfileScreen( showLogoutDialog = true }, modifier = Modifier - .padding(horizontal = 16.dp).fillMaxWidth() + .padding(horizontal = 16.dp) + .fillMaxWidth() .testTag("logout"), ) Spacer(Modifier.height(16.dp)) diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt index 2c1c274d8e..68653a0f5b 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileViewModel.kt @@ -11,6 +11,9 @@ import arrow.core.Either import com.hedvig.android.auth.LogoutUseCase import com.hedvig.android.feature.profile.data.CheckTravelCertificateDestinationAvailabilityUseCase import com.hedvig.android.feature.profile.data.TravelCertificateAvailabilityError +import com.hedvig.android.feature.profile.tab.ProfileUiEvent.Logout +import com.hedvig.android.feature.profile.tab.ProfileUiEvent.Reload +import com.hedvig.android.feature.profile.tab.ProfileUiEvent.SnoozeNotificationPermission import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.memberreminders.EnableNotificationsReminderManager @@ -59,9 +62,9 @@ internal class ProfilePresenter( CollectEvents { event -> when (event) { - ProfileUiEvent.Logout -> logoutUseCase.invoke() - ProfileUiEvent.SnoozeNotificationPermission -> snoozeNotificationReminderRequest++ - ProfileUiEvent.Reload -> dataLoadIteration++ + Logout -> logoutUseCase.invoke() + SnoozeNotificationPermission -> snoozeNotificationReminderRequest++ + Reload -> dataLoadIteration++ } } diff --git a/app/feature/feature-terminate-insurance/build.gradle.kts b/app/feature/feature-terminate-insurance/build.gradle.kts index d20c6b241b..60920f8861 100644 --- a/app/feature/feature-terminate-insurance/build.gradle.kts +++ b/app/feature/feature-terminate-insurance/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.navigationCompose) implementation(projects.navigationComposeTyped) implementation(projects.navigationCore) + implementation(projects.dataChangetier) testImplementation(libs.apollo.testingSupport) testImplementation(libs.assertK) diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt index b27317d58e..6c3bc6649b 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt @@ -8,6 +8,9 @@ import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.feature.terminateinsurance.InsuranceId +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature.TIER +import kotlinx.coroutines.flow.first import kotlinx.datetime.LocalDate import octopus.FlowTerminationDateNextMutation import octopus.FlowTerminationDeletionNextMutation @@ -26,26 +29,32 @@ internal interface TerminateInsuranceRepository { suspend fun submitReasonForCancelling(reason: TerminationReason): Either suspend fun confirmDeletion(): Either + + suspend fun getContractId(): String } internal class TerminateInsuranceRepositoryImpl( private val apolloClient: ApolloClient, + private val featureManager: FeatureManager, private val terminationFlowContextStorage: TerminationFlowContextStorage, ) : TerminateInsuranceRepository { override suspend fun startTerminationFlow(insuranceId: InsuranceId): Either { return either { + val isTierEnabled = featureManager.isFeatureEnabled(TIER).first() val result = apolloClient .mutation(FlowTerminationStartMutation(FlowTerminationStartInput(insuranceId.id))) .safeExecute(::ErrorMessage) .bind() .flowTerminationStart terminationFlowContextStorage.saveContext(result.context) - result.currentStep.toTerminateInsuranceStep() + terminationFlowContextStorage.saveContractId(insuranceId.id) + result.currentStep.toTerminateInsuranceStep(isTierEnabled) } } override suspend fun setTerminationDate(terminationDate: LocalDate): Either { return either { + val isTierEnabled = featureManager.isFeatureEnabled(TIER).first() val result = apolloClient .mutation( FlowTerminationDateNextMutation( @@ -57,7 +66,7 @@ internal class TerminateInsuranceRepositoryImpl( .bind() .flowTerminationDateNext terminationFlowContextStorage.saveContext(result.context) - result.currentStep.toTerminateInsuranceStep() + result.currentStep.toTerminateInsuranceStep(isTierEnabled) } } @@ -65,6 +74,7 @@ internal class TerminateInsuranceRepositoryImpl( reason: TerminationReason, ): Either { return either { + val isTierEnabled = featureManager.isFeatureEnabled(TIER).first() val result = apolloClient .mutation( FlowTerminationSurveyNextMutation( @@ -81,19 +91,24 @@ internal class TerminateInsuranceRepositoryImpl( .bind() .flowTerminationSurveyNext terminationFlowContextStorage.saveContext(result.context) - result.currentStep.toTerminateInsuranceStep() + result.currentStep.toTerminateInsuranceStep(isTierEnabled) } } override suspend fun confirmDeletion(): Either { return either { + val isTierEnabled = featureManager.isFeatureEnabled(TIER).first() val result = apolloClient .mutation(FlowTerminationDeletionNextMutation(terminationFlowContextStorage.getContext())) .safeExecute(::ErrorMessage) .bind() .flowTerminationDeletionNext terminationFlowContextStorage.saveContext(result.context) - result.currentStep.toTerminateInsuranceStep() + result.currentStep.toTerminateInsuranceStep(isTierEnabled) } } + + override suspend fun getContractId(): String { + return terminationFlowContextStorage.getContractId() + } } diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceStep.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceStep.kt index 65231e2608..ac31673eef 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceStep.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceStep.kt @@ -1,5 +1,6 @@ package com.hedvig.android.feature.terminateinsurance.data +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Action.UnknownAction import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceDestination import com.hedvig.android.feature.terminateinsurance.navigation.TerminationGraphParameters import com.hedvig.android.logger.LogPriority @@ -41,7 +42,9 @@ internal sealed interface TerminateInsuranceStep { data class UnknownStep(val message: String? = "") : TerminateInsuranceStep } -internal fun TerminationFlowStepFragment.CurrentStep.toTerminateInsuranceStep(): TerminateInsuranceStep { +internal fun TerminationFlowStepFragment.CurrentStep.toTerminateInsuranceStep( + isTierFeatureEnabled: Boolean, +): TerminateInsuranceStep { return when (this) { is TerminationFlowStepFragment.FlowTerminationDateStepCurrentStep -> { TerminateInsuranceStep.TerminateInsuranceDate(minDate, maxDate) @@ -58,7 +61,7 @@ internal fun TerminationFlowStepFragment.CurrentStep.toTerminateInsuranceStep(): is TerminationFlowStepFragment.FlowTerminationSurveyStepCurrentStep -> { TerminateInsuranceStep.Survey( - options.toOptionList(), + options.toOptionList(isTierFeatureEnabled), ) } @@ -66,48 +69,107 @@ internal fun TerminationFlowStepFragment.CurrentStep.toTerminateInsuranceStep(): } } -private fun List.toOptionList(): - List { +private fun List.toOptionList( + isTierFeatureEnabled: Boolean, +): List { return map { + // remade a bit of logic here. If we receive unknown actions in suggestion for one of the subOptions + // (or the option itself), + // subOptions then become empty and instead the freeTextField becomes available TerminationSurveyOption( id = it.id, title = it.title, listIndex = this.indexOf(it), - feedBackRequired = it.feedBack != null, - subOptions = it.subOptions?.toSubOptionList() ?: listOf(), - suggestion = it.suggestion?.toSuggestion(), + feedBackRequired = it.feedBack != null || + (it.suggestion?.toSuggestion(isTierFeatureEnabled) == UnknownAction) || + it.subOptions?.noUnknownActions(isTierFeatureEnabled) == false, + subOptions = it.subOptions?.toSubOptionList(isTierFeatureEnabled) ?: listOf(), + suggestion = it.suggestion?.toSuggestion(isTierFeatureEnabled), ) } } -private fun List.toSubOptionList(): - List { - return map { +private fun List.noUnknownActions( + isTierFeatureEnabled: Boolean, +): Boolean { + return none { subOption -> + subOption.suggestion?.toSuggestion(isTierFeatureEnabled) == UnknownAction + } +} + +private fun List.toSubOptionList( + isTierFeatureEnabled: Boolean, +): List { + // no subOptions if one of them contains some action that we don't know how to handle + val filtered = takeIf { subs -> + subs.noUnknownActions(isTierFeatureEnabled) + } ?: listOf() + return filtered.map { subOption -> TerminationSurveyOption( - id = it.id, - title = it.title, - feedBackRequired = it.feedBack != null, + id = subOption.id, + title = subOption.title, + feedBackRequired = subOption.feedBack != null, subOptions = listOf(), - listIndex = this.indexOf(it), - suggestion = it.suggestion?.toSuggestion(), + listIndex = filtered.indexOf(subOption), + suggestion = subOption.suggestion?.toSuggestion(isTierFeatureEnabled), ) } } -private fun FlowTerminationSurveyOptionSuggestionFragment.toSuggestion(): SurveyOptionSuggestion? { +private fun FlowTerminationSurveyOptionSuggestionFragment.toSuggestion( + isTierFeatureEnabled: Boolean, +): SurveyOptionSuggestion? { return when (this) { is FlowTerminationSurveyOptionSuggestionActionFlowTerminationSurveyOptionSuggestionFragment -> { - if (action == FlowTerminationSurveyRedirectAction.UPDATE_ADDRESS) { - SurveyOptionSuggestion.Action.UpdateAddress( - description = description, - buttonTitle = buttonTitle, - ) - } else { - logcat( - LogPriority.WARN, - message = { "FlowTerminationSurveyStepCurrentStep unknown suggestion type: ${this.action.rawValue}" }, - ) - null + when (action) { + FlowTerminationSurveyRedirectAction.UPDATE_ADDRESS -> { + SurveyOptionSuggestion.Action.UpdateAddress( + description = description, + buttonTitle = buttonTitle, + ) + } + + FlowTerminationSurveyRedirectAction.CHANGE_TIER_FOUND_BETTER_PRICE -> { + if (isTierFeatureEnabled) { + SurveyOptionSuggestion.Action.DowngradePriceByChangingTier( + description = description, + buttonTitle = buttonTitle, + ) + } else { + logcat( + LogPriority.ERROR, + message = { + "FlowTerminationSurveyStepCurrentStep suggestion: CHANGE_TIER_FOUND_BETTER_PRICE but tier feature flag is disabled!" + }, + ) + UnknownAction + } + } + + FlowTerminationSurveyRedirectAction.CHANGE_TIER_MISSING_COVERAGE_AND_TERMS -> { + if (isTierFeatureEnabled) { + SurveyOptionSuggestion.Action.UpgradeCoverageByChangingTier( + description = description, + buttonTitle = buttonTitle, + ) + } else { + logcat( + LogPriority.ERROR, + message = { + "FlowTerminationSurveyStepCurrentStep suggestion: CHANGE_TIER_MISSING_COVERAGE_AND_TERMS but tier feature flag is disabled!" + }, + ) + UnknownAction + } + } + + else -> { + logcat( + LogPriority.WARN, + message = { "FlowTerminationSurveyStepCurrentStep unknown suggestion type: ${this.action.rawValue}" }, + ) + UnknownAction + } } } diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationFlowContextStorage.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationFlowContextStorage.kt index 5d6f170af2..cc379aa4ad 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationFlowContextStorage.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationFlowContextStorage.kt @@ -18,6 +18,20 @@ class TerminationFlowContextStorage( .first()!! } + suspend fun getContractId(): String { + return datastore.data + .map { preferences: Preferences -> + preferences[TERMINATION_FLOW_CONTRACT_ID_KEY] + } + .first()!! + } + + suspend fun saveContractId(contractId: String) { + datastore.edit { preferences -> + preferences[TERMINATION_FLOW_CONTRACT_ID_KEY] = contractId + } + } + suspend fun saveContext(context: String) { datastore.edit { preferences -> preferences[TERMINATION_FLOW_CONTEXT_KEY] = context @@ -25,6 +39,7 @@ class TerminationFlowContextStorage( } companion object { - private val TERMINATION_FLOW_CONTEXT_KEY = stringPreferencesKey("CLAIM_FLOW_CONTEXT_KEY") + private val TERMINATION_FLOW_CONTEXT_KEY = stringPreferencesKey("CLAIM_FLOW_CONTEXT_KEY") // todo: Stelios, should it be CLAIM_FLOW here? + private val TERMINATION_FLOW_CONTRACT_ID_KEY = stringPreferencesKey("TERMINATION_FLOW_CONTRACT_ID_KEY") } } diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationSurveyOption.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationSurveyOption.kt index af962c5c0c..5a5142a155 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationSurveyOption.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminationSurveyOption.kt @@ -24,6 +24,26 @@ internal sealed interface SurveyOptionSuggestion { override val description: String, override val buttonTitle: String, ) : Action + + @Serializable + data class UpgradeCoverageByChangingTier( + override val description: String, + override val buttonTitle: String, + ) : Action + + @Serializable + data class DowngradePriceByChangingTier( + override val description: String, + override val buttonTitle: String, + ) : Action + + @Serializable // adding for filtering. may be useful in the future for old clients? + data object UnknownAction : Action { + override val description: String + get() = "" + override val buttonTitle: String + get() = "" + } } @Serializable diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/di/TerminateInsuranceModule.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/di/TerminateInsuranceModule.kt index f5380f490c..513cf12e5c 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/di/TerminateInsuranceModule.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/di/TerminateInsuranceModule.kt @@ -3,6 +3,7 @@ package com.hedvig.android.feature.terminateinsurance.di import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient +import com.hedvig.android.data.changetier.data.ChangeTierRepository import com.hedvig.android.data.termination.data.GetTerminatableContractsUseCase import com.hedvig.android.feature.terminateinsurance.data.TerminateInsuranceRepository import com.hedvig.android.feature.terminateinsurance.data.TerminateInsuranceRepositoryImpl @@ -14,6 +15,7 @@ import com.hedvig.android.feature.terminateinsurance.step.choose.ChooseInsurance import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyViewModel import com.hedvig.android.feature.terminateinsurance.step.terminationdate.TerminationDateViewModel import com.hedvig.android.feature.terminateinsurance.step.terminationreview.TerminationConfirmationViewModel +import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.language.LanguageService import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -30,6 +32,7 @@ val terminateInsuranceModule = module { TerminationSurveyViewModel( options = options, terminateInsuranceRepository = get(), + changeTierRepository = get(), ) } viewModel { (parameters: TerminationDateParameters) -> @@ -49,6 +52,7 @@ val terminateInsuranceModule = module { single { TerminateInsuranceRepositoryImpl( apolloClient = get(), + featureManager = get(), terminationFlowContextStorage = get(), ) } diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/navigation/TerminateInsuranceGraph.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/navigation/TerminateInsuranceGraph.kt index 37961df7c6..ef8e1df5b9 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/navigation/TerminateInsuranceGraph.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/navigation/TerminateInsuranceGraph.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.navDeepLink import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleIntent import com.hedvig.android.data.termination.data.TerminatableInsurance import com.hedvig.android.feature.terminateinsurance.data.toTerminateInsuranceDestination import com.hedvig.android.feature.terminateinsurance.step.choose.ChooseInsuranceToTerminateDestination @@ -42,6 +43,7 @@ fun NavGraphBuilder.terminateInsuranceGraph( openPlayStore: () -> Unit, navigateToInsurances: (NavOptionsBuilder.() -> Unit) -> Unit, closeTerminationFlow: () -> Unit, + redirectToChangeTierFlow: (NavBackStackEntry, Pair) -> Unit, ) { navdestination { backStackEntry -> TerminationFailureDestination( @@ -130,6 +132,9 @@ fun NavGraphBuilder.terminateInsuranceGraph( }, navigateToMovingFlow = { navigateToMovingFlow(backStackEntry) }, openUrl = openUrl, + redirectToChangeTierFlow = { intent -> + redirectToChangeTierFlow(backStackEntry, intent) + }, ) } @@ -151,6 +156,9 @@ fun NavGraphBuilder.terminateInsuranceGraph( }, navigateToMovingFlow = { navigateToMovingFlow(backStackEntry) }, openUrl = openUrl, + redirectToChangeTierFlow = { intent -> + redirectToChangeTierFlow(backStackEntry, intent) + }, ) } diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt index ee61673fdb..8d0fa85d9b 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleIntent import com.hedvig.android.design.system.hedvig.ChosenState.Chosen import com.hedvig.android.design.system.hedvig.ChosenState.NotChosen import com.hedvig.android.design.system.hedvig.EmptyState @@ -39,9 +40,16 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplay import com.hedvig.android.design.system.hedvig.freetext.FreeTextOverlay import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Action.DowngradePriceByChangingTier +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Action.UnknownAction +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Action.UpdateAddress +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Action.UpgradeCoverageByChangingTier +import com.hedvig.android.feature.terminateinsurance.data.SurveyOptionSuggestion.Redirect import com.hedvig.android.feature.terminateinsurance.data.TerminateInsuranceStep import com.hedvig.android.feature.terminateinsurance.data.TerminationReason import com.hedvig.android.feature.terminateinsurance.data.TerminationSurveyOption +import com.hedvig.android.feature.terminateinsurance.step.survey.ErrorReason.EMPTY_QUOTES +import com.hedvig.android.feature.terminateinsurance.step.survey.ErrorReason.GENERAL import com.hedvig.android.feature.terminateinsurance.ui.TerminationScaffold import hedvig.resources.R @@ -54,8 +62,16 @@ internal fun TerminationSurveyDestination( openUrl: (String) -> Unit, navigateToNextStep: (step: TerminateInsuranceStep) -> Unit, navigateToSubOptions: ((List) -> Unit)?, + redirectToChangeTierFlow: (Pair) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(uiState.intentAndIdToRedirectToChangeTierFlow) { + val intent = uiState.intentAndIdToRedirectToChangeTierFlow + if (intent != null) { + viewModel.emit(TerminationSurveyEvent.ClearNextStep) + redirectToChangeTierFlow(intent) + } + } LaunchedEffect(uiState.nextNavigationStep) { val nextStep = uiState.nextNavigationStep if (nextStep != null) { @@ -93,6 +109,12 @@ internal fun TerminationSurveyDestination( viewModel.emit(TerminationSurveyEvent.ShowFullScreenEditText(it)) }, openUrl = openUrl, + tryToDowngradePrice = { + viewModel.emit(TerminationSurveyEvent.TryToDowngradePrice) + }, + tryToUpgradeCoverage = { + viewModel.emit(TerminationSurveyEvent.TryToUpgradeCoverage) + }, ) } @@ -108,6 +130,8 @@ private fun TerminationSurveyScreen( onLaunchFullScreenEditText: (option: TerminationSurveyOption) -> Unit, changeFeedbackForSelectedReason: (feedback: String?) -> Unit, onContinueClick: () -> Unit, + tryToUpgradeCoverage: () -> Unit, + tryToDowngradePrice: () -> Unit, ) { FreeTextOverlay( freeTextMaxLength = 2000, @@ -136,7 +160,7 @@ private fun TerminationSurveyScreen( Spacer(Modifier.weight(1f)) Spacer(Modifier.height(16.dp)) AnimatedVisibility( - visible = uiState.errorWhileLoadingNextStep, + visible = uiState.errorWhileLoadingNextStep != null, enter = fadeIn(), exit = fadeOut(), ) { @@ -144,14 +168,24 @@ private fun TerminationSurveyScreen( Modifier.weight(1f), verticalArrangement = Arrangement.Center, ) { + val subTitle = when (uiState.errorWhileLoadingNextStep) { + GENERAL -> stringResource(R.string.GENERAL_ERROR_BODY) + EMPTY_QUOTES -> stringResource(R.string.TERMINATION_NO_TIER_QUOTES_SUBTITLE) + null -> "" + } + val title = when (uiState.errorWhileLoadingNextStep) { + GENERAL -> stringResource(R.string.GENERAL_ERROR_BODY) + EMPTY_QUOTES -> stringResource(R.string.TERMINATION_NO_TIER_QUOTES_TITLE) + null -> "" + } EmptyState( modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth() .wrapContentWidth(), - text = stringResource(R.string.something_went_wrong), + text = title, iconStyle = ERROR, - description = null, + description = subTitle, ) Spacer(Modifier.height(16.dp)) } @@ -172,19 +206,35 @@ private fun TerminationSurveyScreen( AnimatedVisibility(visible = reason.surveyOption == uiState.selectedOption) { Column { val suggestion = reason.surveyOption.suggestion - if (suggestion != null) { + if (suggestion != null && suggestion != UnknownAction) { val text = suggestion.description val buttonText = suggestion.buttonTitle val onSuggestionButtonClick: () -> Unit = when (suggestion) { - is SurveyOptionSuggestion.Action.UpdateAddress -> { + is UpdateAddress -> { { navigateToMovingFlow() } } - is SurveyOptionSuggestion.Redirect -> { + is Redirect -> { { openUrl(suggestion.url) } } + + is DowngradePriceByChangingTier -> { + { + tryToDowngradePrice() + } + } + is UpgradeCoverageByChangingTier -> { + { + tryToUpgradeCoverage() + } + } + + UnknownAction -> { + {} + } } HedvigNotificationCard( + buttonLoading = uiState.actionButtonLoading, modifier = Modifier.padding(horizontal = 16.dp), message = text, priority = NotificationPriority.Campaign, @@ -249,6 +299,8 @@ private fun ShowSurveyScreenPreview( onCloseFullScreenEditText = {}, onLaunchFullScreenEditText = {}, openUrl = {}, + tryToDowngradePrice = {}, + tryToUpgradeCoverage = {}, ) } } @@ -272,7 +324,7 @@ private class ShowSurveyUiStateProvider : TerminationSurveyState( nextNavigationStep = null, navigationStepLoadingForReason = null, - errorWhileLoadingNextStep = true, + errorWhileLoadingNextStep = null, selectedOption = previewReason2.surveyOption, reasons = listOf(previewReason1, previewReason2, previewReason3), ), diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyViewModel.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyViewModel.kt index a8d86204c8..2b79338b32 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyViewModel.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyViewModel.kt @@ -7,10 +7,26 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource.TERMINATION_BETTER_COVERAGE +import com.hedvig.android.data.changetier.data.ChangeTierCreateSource.TERMINATION_BETTER_PRICE +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleIntent +import com.hedvig.android.data.changetier.data.ChangeTierRepository import com.hedvig.android.feature.terminateinsurance.data.TerminateInsuranceRepository import com.hedvig.android.feature.terminateinsurance.data.TerminateInsuranceStep import com.hedvig.android.feature.terminateinsurance.data.TerminationReason import com.hedvig.android.feature.terminateinsurance.data.TerminationSurveyOption +import com.hedvig.android.feature.terminateinsurance.step.survey.ErrorReason.EMPTY_QUOTES +import com.hedvig.android.feature.terminateinsurance.step.survey.ErrorReason.GENERAL +import com.hedvig.android.feature.terminateinsurance.step.survey.SurveyNavigationStep.NavigateToSubOptions +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.ChangeFeedbackForSelectedReason +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.ClearNextStep +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.CloseFullScreenEditText +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.Continue +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.SelectOption +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.ShowFullScreenEditText +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.TryToDowngradePrice +import com.hedvig.android.feature.terminateinsurance.step.survey.TerminationSurveyEvent.TryToUpgradeCoverage import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import com.hedvig.android.molecule.android.MoleculeViewModel @@ -20,19 +36,26 @@ import com.hedvig.android.molecule.public.MoleculePresenterScope internal class TerminationSurveyViewModel( options: List, terminateInsuranceRepository: TerminateInsuranceRepository, + changeTierRepository: ChangeTierRepository, ) : MoleculeViewModel( initialState = TerminationSurveyState(), - presenter = TerminationSurveyPresenter(options, terminateInsuranceRepository), + presenter = TerminationSurveyPresenter( + options, + terminateInsuranceRepository, + changeTierRepository, + ), ) internal class TerminationSurveyPresenter( private val options: List, private val terminateInsuranceRepository: TerminateInsuranceRepository, + private val changeTierRepository: ChangeTierRepository, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present( lastState: TerminationSurveyState, ): TerminationSurveyState { + var loadBetterQuotesSource by remember { mutableStateOf(null) } var loadNextStep by remember { mutableStateOf(false) } val currentReasonsWithFeedback = remember { val initialReasons = ( @@ -57,7 +80,7 @@ internal class TerminationSurveyPresenter( CollectEvents { event -> when (event) { - is TerminationSurveyEvent.ChangeFeedbackForSelectedReason -> { + is ChangeFeedbackForSelectedReason -> { showFullScreenTextField = null val selectedOption = currentState.selectedOption selectedOption?.let { selected -> @@ -65,29 +88,75 @@ internal class TerminationSurveyPresenter( } } - is TerminationSurveyEvent.SelectOption -> { - currentState = currentState.copy(selectedOption = event.option, errorWhileLoadingNextStep = false) + is SelectOption -> { + currentState = currentState.copy(selectedOption = event.option, errorWhileLoadingNextStep = null) } - is TerminationSurveyEvent.Continue -> { + is Continue -> { val selectedOption = currentState.selectedOption ?: return@CollectEvents - currentState = currentState.copy(errorWhileLoadingNextStep = false) + currentState = currentState.copy(errorWhileLoadingNextStep = null) if (selectedOption.subOptions.isNotEmpty()) { - currentState = currentState.copy(nextNavigationStep = SurveyNavigationStep.NavigateToSubOptions) + currentState = currentState.copy(nextNavigationStep = NavigateToSubOptions) } else { loadNextStep = true } } - TerminationSurveyEvent.ClearNextStep -> { - currentState = currentState.copy(nextNavigationStep = null) + ClearNextStep -> { + currentState = currentState.copy(nextNavigationStep = null, intentAndIdToRedirectToChangeTierFlow = null) } - is TerminationSurveyEvent.ShowFullScreenEditText -> { + is ShowFullScreenEditText -> { showFullScreenTextField = TerminationReason(event.option, currentReasonsWithFeedback[event.option]) } - TerminationSurveyEvent.CloseFullScreenEditText -> showFullScreenTextField = null + CloseFullScreenEditText -> showFullScreenTextField = null + + TryToDowngradePrice -> { + loadBetterQuotesSource = TERMINATION_BETTER_PRICE + } + + TryToUpgradeCoverage -> { + loadBetterQuotesSource = TERMINATION_BETTER_COVERAGE + } + } + } + + LaunchedEffect(loadBetterQuotesSource) { + val source = loadBetterQuotesSource + if (source != null) { + currentState = currentState.copy(actionButtonLoading = true, errorWhileLoadingNextStep = null) + val insuranceId = terminateInsuranceRepository.getContractId() + val result = + changeTierRepository.startChangeTierIntentAndGetQuotesId(insuranceId = insuranceId, source = source) + result.fold( + ifLeft = { errorMessage -> + logcat(LogPriority.ERROR) { + "Received error while creating changeTierDeductibleIntent from termination flow" + } + currentState = currentState.copy( + actionButtonLoading = false, + errorWhileLoadingNextStep = GENERAL, + ) + loadBetterQuotesSource = null + }, + ifRight = { changeTierIntent -> + if (changeTierIntent.quotes.isEmpty()) { + currentState = currentState.copy( + actionButtonLoading = false, + errorWhileLoadingNextStep = EMPTY_QUOTES, + ) + loadBetterQuotesSource = null + } else { + currentState = currentState.copy( + errorWhileLoadingNextStep = null, + actionButtonLoading = false, + intentAndIdToRedirectToChangeTierFlow = insuranceId to changeTierIntent, + ) + loadBetterQuotesSource = null + } + }, + ) } } @@ -104,7 +173,7 @@ internal class TerminationSurveyPresenter( loadNextStep = false currentState.copy( navigationStepLoadingForReason = null, - errorWhileLoadingNextStep = true, + errorWhileLoadingNextStep = GENERAL, ) }, ifRight = { step -> @@ -114,7 +183,7 @@ internal class TerminationSurveyPresenter( loadNextStep = false currentState.copy( navigationStepLoadingForReason = null, - errorWhileLoadingNextStep = false, + errorWhileLoadingNextStep = null, nextNavigationStep = SurveyNavigationStep.NavigateToNextTerminationStep(step), ) }, @@ -136,6 +205,10 @@ internal sealed interface TerminationSurveyEvent { data object Continue : TerminationSurveyEvent + data object TryToDowngradePrice : TerminationSurveyEvent + + data object TryToUpgradeCoverage : TerminationSurveyEvent + data class ShowFullScreenEditText(val option: TerminationSurveyOption) : TerminationSurveyEvent data object CloseFullScreenEditText : TerminationSurveyEvent @@ -154,7 +227,9 @@ internal data class TerminationSurveyState( val nextNavigationStep: SurveyNavigationStep? = null, // this one is not Boolean entirely for the sake of more convenient testing val navigationStepLoadingForReason: TerminationReason? = null, - val errorWhileLoadingNextStep: Boolean = false, + val errorWhileLoadingNextStep: ErrorReason? = null, + val intentAndIdToRedirectToChangeTierFlow: Pair? = null, + val actionButtonLoading: Boolean = false, ) { val continueAllowed: Boolean = selectedOption != null && selectedOption.suggestion == null } @@ -164,3 +239,8 @@ internal sealed interface SurveyNavigationStep { data object NavigateToSubOptions : SurveyNavigationStep } + +internal enum class ErrorReason { + GENERAL, + EMPTY_QUOTES, +} diff --git a/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt b/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt index 581c69e84c..38f6af3278 100644 --- a/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt +++ b/app/feature/feature-terminate-insurance/src/test/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyPresenterTest.kt @@ -216,4 +216,8 @@ private class FakeTerminateInsuranceRepository : TerminateInsuranceRepository { override suspend fun confirmDeletion(): Either = terminationFlowTurbine.awaitItem() + + override suspend fun getContractId(): String { + TODO("Not yet implemented") + } } diff --git a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt index 02edcf6e93..dc4ada62d4 100644 --- a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -16,4 +16,5 @@ enum class Feature( ), EDIT_COINSURED("Let member edit co insured"), HELP_CENTER("Enable the help center screens"), + TIER("Let members change tier"), } diff --git a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index 90ec298d79..738dafb02c 100644 --- a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -20,6 +20,8 @@ internal class UnleashFeatureFlagProvider( Feature.UPDATE_NECESSARY -> hedvigUnleashClient.client.isEnabled("update_necessary", false) Feature.EDIT_COINSURED -> hedvigUnleashClient.client.isEnabled("edit_coinsured", false) Feature.HELP_CENTER -> hedvigUnleashClient.client.isEnabled("help_center", true) + Feature.TIER -> hedvigUnleashClient.client.isEnabled("enable_tiers", false) + // todo: tier default false for now, maybe change later? } }.distinctUntilChanged() } diff --git a/hedvig-lint/lint-baseline/lint-baseline-data-changetier.xml b/hedvig-lint/lint-baseline/lint-baseline-data-changetier.xml new file mode 100644 index 0000000000..0f8220a4b8 --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-data-changetier.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/hedvig-lint/lint-baseline/lint-baseline-feature-choose-tier.xml b/hedvig-lint/lint-baseline/lint-baseline-feature-choose-tier.xml new file mode 100644 index 0000000000..2c8fe410f8 --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-feature-choose-tier.xml @@ -0,0 +1,4 @@ + + + +